blob: dbce14c33cd89236867ef035eaa52914280a3cd2 [file] [log] [blame]
Sean McCullough71941bd2025-04-18 13:31:48 -07001import { css, html, LitElement } from "lit";
Sean McCullough2c5bba42025-04-20 19:33:17 -07002import { PropertyValues } from "lit";
Sean McCullough71941bd2025-04-18 13:31:48 -07003import { repeat } from "lit/directives/repeat.js";
Sean McCullough2c5bba42025-04-20 19:33:17 -07004import { customElement, property, state } from "lit/decorators.js";
Sean McCulloughd9f13372025-04-21 15:08:49 -07005import { AgentMessage } from "../types";
Sean McCullough71941bd2025-04-18 13:31:48 -07006import "./sketch-timeline-message";
Pokey Rule4097e532025-04-24 18:55:28 +01007import { Ref } from "lit/directives/ref";
Sean McCullough86b56862025-04-18 13:04:03 -07008
Sean McCullough71941bd2025-04-18 13:31:48 -07009@customElement("sketch-timeline")
Sean McCullough86b56862025-04-18 13:04:03 -070010export class SketchTimeline extends LitElement {
Pokey Rulee2a8c2f2025-04-23 15:09:25 +010011 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -070012 messages: AgentMessage[] = [];
Sean McCullough86b56862025-04-18 13:04:03 -070013
Sean McCullough2c5bba42025-04-20 19:33:17 -070014 // Track if we should scroll to the bottom
15 @state()
16 private scrollingState: "pinToLatest" | "floating" = "pinToLatest";
17
Pokey Rulee2a8c2f2025-04-23 15:09:25 +010018 @property({ attribute: false })
Pokey Rule4097e532025-04-24 18:55:28 +010019 scrollContainer: Ref<HTMLElement>;
Sean McCullough2c5bba42025-04-20 19:33:17 -070020
Sean McCullough86b56862025-04-18 13:04:03 -070021 static styles = css`
Sean McCullough71941bd2025-04-18 13:31:48 -070022 /* Hide views initially to prevent flash of content */
23 .timeline-container .timeline,
24 .timeline-container .diff-view,
25 .timeline-container .chart-view,
26 .timeline-container .terminal-view {
27 visibility: hidden;
28 }
Sean McCullough86b56862025-04-18 13:04:03 -070029
Sean McCullough71941bd2025-04-18 13:31:48 -070030 /* Will be set by JavaScript once we know which view to display */
31 .timeline-container.view-initialized .timeline,
32 .timeline-container.view-initialized .diff-view,
33 .timeline-container.view-initialized .chart-view,
34 .timeline-container.view-initialized .terminal-view {
35 visibility: visible;
36 }
37
38 .timeline-container {
39 width: 100%;
40 position: relative;
41 }
42
43 /* Timeline styles that should remain unchanged */
44 .timeline {
45 position: relative;
46 margin: 10px 0;
47 scroll-behavior: smooth;
48 }
49
50 .timeline::before {
51 content: "";
52 position: absolute;
53 top: 0;
54 bottom: 0;
55 left: 15px;
56 width: 2px;
57 background: #e0e0e0;
58 border-radius: 1px;
59 }
60
61 /* Hide the timeline vertical line when there are no messages */
62 .timeline.empty::before {
63 display: none;
64 }
Sean McCullough2c5bba42025-04-20 19:33:17 -070065
66 #scroll-container {
67 overflow: auto;
68 padding-left: 1em;
69 }
70 #jump-to-latest {
71 display: none;
72 position: fixed;
73 bottom: 100px;
74 right: 0;
75 background: rgb(33, 150, 243);
76 color: white;
77 border-radius: 8px;
78 padding: 0.5em;
79 margin: 0.5em;
80 font-size: x-large;
81 opacity: 0.5;
82 cursor: pointer;
83 }
84 #jump-to-latest:hover {
85 opacity: 1;
86 }
87 #jump-to-latest.floating {
88 display: block;
89 }
Philip Zeyliger5cf49262025-04-29 18:35:55 +000090
91 /* Welcome box styles for the empty chat state */
92 .welcome-box {
93 margin: 2rem auto;
94 max-width: 80%;
95 padding: 2rem;
96 border: 2px solid #e0e0e0;
97 border-radius: 8px;
98 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
99 background-color: #ffffff;
100 text-align: center;
101 }
102
103 .welcome-box-title {
104 font-size: 1.5rem;
105 font-weight: 600;
106 margin-bottom: 1.5rem;
107 color: #333;
108 }
109
110 .welcome-box-content {
111 color: #666; /* Slightly grey font color */
112 line-height: 1.6;
113 font-size: 1rem;
114 }
Sean McCullough86b56862025-04-18 13:04:03 -0700115 `;
116
117 constructor() {
118 super();
Sean McCullough71941bd2025-04-18 13:31:48 -0700119
Sean McCullough86b56862025-04-18 13:04:03 -0700120 // Binding methods
121 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
Sean McCullough2c5bba42025-04-20 19:33:17 -0700122 this._handleScroll = this._handleScroll.bind(this);
123 }
124
125 /**
126 * Scroll to the bottom of the timeline
127 */
128 private scrollToBottom(): void {
Pokey Rule4097e532025-04-24 18:55:28 +0100129 this.scrollContainer.value?.scrollTo({
130 top: this.scrollContainer.value?.scrollHeight,
Sean McCullough2c5bba42025-04-20 19:33:17 -0700131 behavior: "smooth",
132 });
133 }
134
135 /**
136 * Called after the component's properties have been updated
137 */
138 updated(changedProperties: PropertyValues): void {
139 // If messages have changed, scroll to bottom if needed
140 if (changedProperties.has("messages") && this.messages.length > 0) {
141 if (this.scrollingState == "pinToLatest") {
142 setTimeout(() => this.scrollToBottom(), 50);
143 }
144 }
145 if (changedProperties.has("scrollContainer")) {
Pokey Rule4097e532025-04-24 18:55:28 +0100146 this.scrollContainer.value?.addEventListener(
147 "scroll",
148 this._handleScroll,
149 );
Sean McCullough2c5bba42025-04-20 19:33:17 -0700150 }
Sean McCullough86b56862025-04-18 13:04:03 -0700151 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700152
Sean McCullough86b56862025-04-18 13:04:03 -0700153 /**
154 * Handle showCommitDiff event
155 */
156 private _handleShowCommitDiff(event: CustomEvent) {
157 const { commitHash } = event.detail;
158 if (commitHash) {
159 // Bubble up the event to the app shell
Sean McCullough71941bd2025-04-18 13:31:48 -0700160 const newEvent = new CustomEvent("show-commit-diff", {
Sean McCullough86b56862025-04-18 13:04:03 -0700161 detail: { commitHash },
162 bubbles: true,
Sean McCullough71941bd2025-04-18 13:31:48 -0700163 composed: true,
Sean McCullough86b56862025-04-18 13:04:03 -0700164 });
165 this.dispatchEvent(newEvent);
166 }
167 }
168
Sean McCullough2c5bba42025-04-20 19:33:17 -0700169 private _handleScroll(event) {
170 const isAtBottom =
171 Math.abs(
Pokey Rule4097e532025-04-24 18:55:28 +0100172 this.scrollContainer.value.scrollHeight -
173 this.scrollContainer.value.clientHeight -
174 this.scrollContainer.value.scrollTop,
Sean McCullough2c5bba42025-04-20 19:33:17 -0700175 ) <= 1;
176 if (isAtBottom) {
177 this.scrollingState = "pinToLatest";
178 } else {
179 // TODO: does scroll direction matter here?
180 this.scrollingState = "floating";
181 }
182 }
183
Sean McCullough86b56862025-04-18 13:04:03 -0700184 // See https://lit.dev/docs/components/lifecycle/
185 connectedCallback() {
186 super.connectedCallback();
Sean McCullough71941bd2025-04-18 13:31:48 -0700187
Sean McCullough86b56862025-04-18 13:04:03 -0700188 // Listen for showCommitDiff events from the renderer
Sean McCullough71941bd2025-04-18 13:31:48 -0700189 document.addEventListener(
190 "showCommitDiff",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700191 this._handleShowCommitDiff as EventListener,
Sean McCullough71941bd2025-04-18 13:31:48 -0700192 );
Pokey Rule4097e532025-04-24 18:55:28 +0100193
194 this.scrollContainer.value?.addEventListener("scroll", this._handleScroll);
Sean McCullough86b56862025-04-18 13:04:03 -0700195 }
196
197 // See https://lit.dev/docs/components/lifecycle/
198 disconnectedCallback() {
199 super.disconnectedCallback();
Sean McCullough71941bd2025-04-18 13:31:48 -0700200
Sean McCullough86b56862025-04-18 13:04:03 -0700201 // Remove event listeners
Sean McCullough71941bd2025-04-18 13:31:48 -0700202 document.removeEventListener(
203 "showCommitDiff",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700204 this._handleShowCommitDiff as EventListener,
Sean McCullough71941bd2025-04-18 13:31:48 -0700205 );
Sean McCullough2c5bba42025-04-20 19:33:17 -0700206
Pokey Rule4097e532025-04-24 18:55:28 +0100207 this.scrollContainer.value?.removeEventListener(
208 "scroll",
209 this._handleScroll,
210 );
Sean McCullough86b56862025-04-18 13:04:03 -0700211 }
212
Sean McCulloughd9f13372025-04-21 15:08:49 -0700213 // messageKey uniquely identifes a AgentMessage based on its ID and tool_calls, so
Sean McCullough2c5bba42025-04-20 19:33:17 -0700214 // that we only re-render <sketch-message> elements that we need to re-render.
Sean McCulloughd9f13372025-04-21 15:08:49 -0700215 messageKey(message: AgentMessage): string {
Sean McCullough86b56862025-04-18 13:04:03 -0700216 // If the message has tool calls, and any of the tool_calls get a response, we need to
217 // re-render that message.
Sean McCullough71941bd2025-04-18 13:31:48 -0700218 const toolCallResponses = message.tool_calls
219 ?.filter((tc) => tc.result_message)
220 .map((tc) => tc.tool_call_id)
221 .join("-");
Sean McCullough86b56862025-04-18 13:04:03 -0700222 return `message-${message.idx}-${toolCallResponses}`;
223 }
224
225 render() {
Philip Zeyliger5cf49262025-04-29 18:35:55 +0000226 // Check if messages array is empty and render welcome box if it is
227 if (this.messages.length === 0) {
228 return html`
229 <div id="scroll-container">
230 <div class="welcome-box">
231 <h2 class="welcome-box-title">How to use Sketch</h2>
232 <p class="welcome-box-content">
Autoformatterdc507a32025-04-29 18:38:53 +0000233 Sketch is an agentic coding assistant. Ask it, in the chat box
234 below, to implement a task or answer a question. It will create
235 commits, which you can review and leave comments on in the Diff
236 tab. When you're done, you'll find the changes ready to go in a
237 <code>sketch/*</code> branch in your repo!
Philip Zeyliger5cf49262025-04-29 18:35:55 +0000238 </p>
239 </div>
240 </div>
241 `;
242 }
243
244 // Otherwise render the regular timeline with messages
Sean McCullough86b56862025-04-18 13:04:03 -0700245 return html`
Sean McCullough2c5bba42025-04-20 19:33:17 -0700246 <div id="scroll-container">
247 <div class="timeline-container">
248 ${repeat(this.messages, this.messageKey, (message, index) => {
Sean McCulloughd9f13372025-04-21 15:08:49 -0700249 let previousMessage: AgentMessage;
Sean McCullough2c5bba42025-04-20 19:33:17 -0700250 if (index > 0) {
251 previousMessage = this.messages[index - 1];
252 }
253 return html`<sketch-timeline-message
254 .message=${message}
255 .previousMessage=${previousMessage}
Sean McCullough2deac842025-04-21 18:17:57 -0700256 .open=${index == this.messages.length - 1}
Sean McCullough2c5bba42025-04-20 19:33:17 -0700257 ></sketch-timeline-message>`;
258 })}
259 </div>
260 </div>
261 <div
262 id="jump-to-latest"
263 class="${this.scrollingState}"
264 @click=${this.scrollToBottom}
265 >
266
Sean McCullough71941bd2025-04-18 13:31:48 -0700267 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700268 `;
269 }
270}
271
272declare global {
273 interface HTMLElementTagNameMap {
274 "sketch-timeline": SketchTimeline;
275 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700276}