blob: 7471dedcdd440db035004af1a8ea3e7d3ecff2fc [file] [log] [blame]
Sean McCullough71941bd2025-04-18 13:31:48 -07001import { css, html, LitElement } from "lit";
2import { repeat } from "lit/directives/repeat.js";
3import { customElement, property } from "lit/decorators.js";
4import { State, TimelineMessage } from "../types";
5import "./sketch-timeline-message";
Sean McCullough86b56862025-04-18 13:04:03 -07006
Sean McCullough71941bd2025-04-18 13:31:48 -07007@customElement("sketch-timeline")
Sean McCullough86b56862025-04-18 13:04:03 -07008export class SketchTimeline extends LitElement {
9 @property()
10 messages: TimelineMessage[] = [];
11
12 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
13 // Note that these styles only apply to the scope of this web component's
14 // shadow DOM node, so they won't leak out or collide with CSS declared in
15 // other components or the containing web page (...unless you want it to do that).
16 static styles = css`
Sean McCullough71941bd2025-04-18 13:31:48 -070017 /* Hide views initially to prevent flash of content */
18 .timeline-container .timeline,
19 .timeline-container .diff-view,
20 .timeline-container .chart-view,
21 .timeline-container .terminal-view {
22 visibility: hidden;
23 }
Sean McCullough86b56862025-04-18 13:04:03 -070024
Sean McCullough71941bd2025-04-18 13:31:48 -070025 /* Will be set by JavaScript once we know which view to display */
26 .timeline-container.view-initialized .timeline,
27 .timeline-container.view-initialized .diff-view,
28 .timeline-container.view-initialized .chart-view,
29 .timeline-container.view-initialized .terminal-view {
30 visibility: visible;
31 }
32
33 .timeline-container {
34 width: 100%;
35 position: relative;
36 }
37
38 /* Timeline styles that should remain unchanged */
39 .timeline {
40 position: relative;
41 margin: 10px 0;
42 scroll-behavior: smooth;
43 }
44
45 .timeline::before {
46 content: "";
47 position: absolute;
48 top: 0;
49 bottom: 0;
50 left: 15px;
51 width: 2px;
52 background: #e0e0e0;
53 border-radius: 1px;
54 }
55
56 /* Hide the timeline vertical line when there are no messages */
57 .timeline.empty::before {
58 display: none;
59 }
Sean McCullough86b56862025-04-18 13:04:03 -070060 `;
61
62 constructor() {
63 super();
Sean McCullough71941bd2025-04-18 13:31:48 -070064
Sean McCullough86b56862025-04-18 13:04:03 -070065 // Binding methods
66 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
67 }
Sean McCullough71941bd2025-04-18 13:31:48 -070068
Sean McCullough86b56862025-04-18 13:04:03 -070069 /**
70 * Handle showCommitDiff event
71 */
72 private _handleShowCommitDiff(event: CustomEvent) {
73 const { commitHash } = event.detail;
74 if (commitHash) {
75 // Bubble up the event to the app shell
Sean McCullough71941bd2025-04-18 13:31:48 -070076 const newEvent = new CustomEvent("show-commit-diff", {
Sean McCullough86b56862025-04-18 13:04:03 -070077 detail: { commitHash },
78 bubbles: true,
Sean McCullough71941bd2025-04-18 13:31:48 -070079 composed: true,
Sean McCullough86b56862025-04-18 13:04:03 -070080 });
81 this.dispatchEvent(newEvent);
82 }
83 }
84
85 // See https://lit.dev/docs/components/lifecycle/
86 connectedCallback() {
87 super.connectedCallback();
Sean McCullough71941bd2025-04-18 13:31:48 -070088
Sean McCullough86b56862025-04-18 13:04:03 -070089 // Listen for showCommitDiff events from the renderer
Sean McCullough71941bd2025-04-18 13:31:48 -070090 document.addEventListener(
91 "showCommitDiff",
92 this._handleShowCommitDiff as EventListener,
93 );
Sean McCullough86b56862025-04-18 13:04:03 -070094 }
95
96 // See https://lit.dev/docs/components/lifecycle/
97 disconnectedCallback() {
98 super.disconnectedCallback();
Sean McCullough71941bd2025-04-18 13:31:48 -070099
Sean McCullough86b56862025-04-18 13:04:03 -0700100 // Remove event listeners
Sean McCullough71941bd2025-04-18 13:31:48 -0700101 document.removeEventListener(
102 "showCommitDiff",
103 this._handleShowCommitDiff as EventListener,
104 );
Sean McCullough86b56862025-04-18 13:04:03 -0700105 }
106
107 messageKey(message: TimelineMessage): string {
108 // If the message has tool calls, and any of the tool_calls get a response, we need to
109 // re-render that message.
Sean McCullough71941bd2025-04-18 13:31:48 -0700110 const toolCallResponses = message.tool_calls
111 ?.filter((tc) => tc.result_message)
112 .map((tc) => tc.tool_call_id)
113 .join("-");
Sean McCullough86b56862025-04-18 13:04:03 -0700114 return `message-${message.idx}-${toolCallResponses}`;
115 }
116
117 render() {
118 return html`
Sean McCullough71941bd2025-04-18 13:31:48 -0700119 <div class="timeline-container">
120 ${repeat(this.messages, this.messageKey, (message, index) => {
121 let previousMessage: TimelineMessage;
122 if (index > 0) {
123 previousMessage = this.messages[index - 1];
124 }
125 return html`<sketch-timeline-message
126 .message=${message}
127 .previousMessage=${previousMessage}
128 ></sketch-timeline-message>`;
129 })}
130 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700131 `;
132 }
133}
134
135declare global {
136 interface HTMLElementTagNameMap {
137 "sketch-timeline": SketchTimeline;
138 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700139}