blob: dbb09aec43a7e5c5e056ba3b9ebdf22a80dc9a40 [file] [log] [blame]
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001import { css, html, LitElement } from "lit";
2import { unsafeHTML } from "lit/directives/unsafe-html.js";
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07003import { customElement, property } from "lit/decorators.js";
Sean McCulloughd9f13372025-04-21 15:08:49 -07004import { ToolCall } from "../types";
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07005import { marked, MarkedOptions } from "marked";
6
7function renderMarkdown(markdownContent: string): string {
8 try {
9 // Set markdown options for proper code block highlighting and safety
10 const markedOptions: MarkedOptions = {
11 gfm: true, // GitHub Flavored Markdown
12 breaks: true, // Convert newlines to <br>
13 async: false,
14 // DOMPurify is recommended for production, but not included in this implementation
15 };
16 return marked.parse(markdownContent, markedOptions) as string;
17 } catch (error) {
18 console.error("Error rendering markdown:", error);
19 // Fallback to plain text if markdown parsing fails
20 return markdownContent;
21 }
22}
23
24@customElement("sketch-tool-card")
25export class SketchToolCard extends LitElement {
26 @property()
27 toolCall: ToolCall;
28
29 @property()
30 open: boolean;
31
32 static styles = css`
33 .tool-call {
34 display: flex;
35 align-items: center;
36 gap: 8px;
37 white-space: nowrap;
38 }
39
40 .tool-call-status {
41 margin-right: 4px;
42 text-align: center;
43 }
44
45 .tool-call-status.spinner {
46 animation: spin 1s infinite linear;
47 display: inline-block;
48 width: 1em;
49 }
50
51 @keyframes spin {
52 0% {
53 transform: rotate(0deg);
54 }
55 100% {
56 transform: rotate(360deg);
57 }
58 }
59
60 .title {
61 font-style: italic;
62 }
63
64 .cancel-button {
65 background: rgb(76, 175, 80);
66 color: white;
67 border: none;
68 padding: 4px 10px;
69 border-radius: 4px;
70 cursor: pointer;
71 font-size: 12px;
72 margin: 5px;
73 }
74
75 .cancel-button:hover {
76 background: rgb(200, 35, 51) !important;
77 }
78
79 .codereview-OK {
80 color: green;
81 }
82
83 details {
84 border-radius: 4px;
85 padding: 0.25em;
86 margin: 0.25em;
87 display: flex;
88 flex-direction: column;
89 align-items: start;
90 }
91
92 details summary {
93 list-style: none;
94 &::before {
95 cursor: hand;
96 font-family: monospace;
97 content: "+";
98 color: white;
99 background-color: darkgray;
100 border-radius: 1em;
101 padding-left: 0.5em;
102 margin: 0.25em;
103 min-width: 1em;
104 }
105 [open] &::before {
106 content: "-";
107 }
108 }
109
110 details summary:hover {
111 list-style: none;
112 &::before {
113 background-color: gray;
114 }
115 }
116 summary {
117 display: flex;
118 flex-direction: row;
119 flex-wrap: nowrap;
120 justify-content: flex-start;
121 align-items: baseline;
122 }
123
124 summary .tool-name {
125 font-family: monospace;
126 color: white;
127 background: rgb(124 145 160);
128 border-radius: 4px;
129 padding: 0.25em;
130 margin: 0.25em;
131 white-space: pre;
132 }
133
134 .summary-text {
135 padding: 0.25em;
136 display: flex;
137 max-width: 50%;
138 overflow: hidden;
139 text-overflow: ellipsis;
140 }
141
142 details[open] .summary-text {
143 /*display: none;*/
144 }
145
146 .tool-error-message {
147 font-style: italic;
148 color: #aa0909;
149 }
Sean McCullough2deac842025-04-21 18:17:57 -0700150
151 .elapsed {
152 font-size: 10px;
153 color: #888;
154 font-style: italic;
155 margin-left: 3px;
156 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700157 `;
158
159 constructor() {
160 super();
161 }
162
163 connectedCallback() {
164 super.connectedCallback();
165 }
166
167 disconnectedCallback() {
168 super.disconnectedCallback();
169 }
170
171 _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
172 console.log("cancelToolCall", tool_call_id, button);
173 button.innerText = "Cancelling";
174 button.disabled = true;
175 try {
176 const response = await fetch("cancel", {
177 method: "POST",
178 headers: {
179 "Content-Type": "application/json",
180 },
181 body: JSON.stringify({
182 tool_call_id: tool_call_id,
183 reason: "user requested cancellation",
184 }),
185 });
186 if (response.ok) {
187 console.log("cancel", tool_call_id, response);
188 button.parentElement.removeChild(button);
189 } else {
190 button.innerText = "Cancel";
191 console.log(`error trying to cancel ${tool_call_id}: `, response);
192 }
193 } catch (e) {
194 console.error("cancel", tool_call_id, e);
195 }
196 };
197
198 render() {
199 const toolCallStatus = this.toolCall?.result_message
200 ? this.toolCall?.result_message.tool_error
201 ? html`❌
202 <span class="tool-error-message"
Sean McCullough2deac842025-04-21 18:17:57 -0700203 >${this.toolCall?.result_message.tool_result}</span
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700204 >`
205 : ""
206 : "⏳";
207
208 const cancelButton = this.toolCall?.result_message
209 ? ""
210 : html`<button
211 class="cancel-button"
212 title="Cancel this operation"
213 @click=${(e: Event) => {
214 e.stopPropagation();
215 const button = e.target as HTMLButtonElement;
216 this._cancelToolCall(this.toolCall?.tool_call_id, button);
217 }}
218 >
219 Cancel
220 </button>`;
221
222 const status = html`<span
223 class="tool-call-status ${this.toolCall?.result_message ? "" : "spinner"}"
224 >${toolCallStatus}</span
225 >`;
226
Sean McCullough2deac842025-04-21 18:17:57 -0700227 const elapsed = html`${this.toolCall?.result_message?.elapsed
228 ? html`<span class="elapsed"
229 >${(this.toolCall?.result_message?.elapsed / 1e9).toFixed(2)}s
230 elapsed</span
231 >`
232 : ""}`;
233
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700234 const ret = html`<div class="tool-call">
235 <details ?open=${this.open}>
236 <summary>
237 <span class="tool-name">${this.toolCall?.name}</span>
238 <span class="summary-text"><slot name="summary"></slot></span>
Sean McCullough2deac842025-04-21 18:17:57 -0700239 ${status} ${cancelButton} ${elapsed}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700240 </summary>
241 <slot name="input"></slot>
242 <slot name="result"></slot>
243 </details>
244 </div> `;
245 if (true) {
246 return ret;
247 }
248 }
249}
250
251@customElement("sketch-tool-card-bash")
252export class SketchToolCardBash extends LitElement {
253 @property()
254 toolCall: ToolCall;
255
256 @property()
257 open: boolean;
258
259 static styles = css`
260 pre {
Philip Zeyligera54c6a32025-04-23 02:13:36 +0000261 background: rgb(236, 236, 236);
262 color: black;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700263 padding: 0.5em;
264 border-radius: 4px;
265 }
266 .summary-text {
267 overflow: hidden;
268 text-overflow: ellipsis;
269 font-family: monospace;
270 }
271 .input {
272 display: flex;
273 }
274 .input pre {
275 width: 100%;
276 margin-bottom: 0;
277 border-radius: 4px 4px 0 0;
278 }
279 .result pre {
280 margin-top: 0;
Philip Zeyligera54c6a32025-04-23 02:13:36 +0000281 color: #555;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700282 border-radius: 0 0 4px 4px;
283 }
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000284 .background-badge {
285 display: inline-block;
286 background-color: #6200ea;
287 color: white;
288 font-size: 10px;
289 font-weight: bold;
290 padding: 2px 6px;
291 border-radius: 10px;
292 margin-left: 8px;
293 vertical-align: middle;
294 }
295 .command-wrapper {
296 display: flex;
297 align-items: center;
298 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700299 `;
300
301 constructor() {
302 super();
303 }
304
305 connectedCallback() {
306 super.connectedCallback();
307 }
308
309 disconnectedCallback() {
310 super.disconnectedCallback();
311 }
312
313 render() {
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000314 const inputData = JSON.parse(this.toolCall?.input || "{}");
315 const isBackground = inputData?.background === true;
316 const backgroundIcon = isBackground ? "🔄 " : "";
317
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700318 return html`
319 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000320 <span slot="summary" class="summary-text">
321 <div class="command-wrapper">
322 🖥️ ${backgroundIcon}${inputData?.command}
323 </div>
324 </span>
325 <div slot="input" class="input">
326 <pre>🖥️ ${backgroundIcon}${inputData?.command}</pre>
327 </div>
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700328 ${
329 this.toolCall?.result_message
330 ? html` ${this.toolCall?.result_message.tool_result
331 ? html`<div slot="result" class="result">
332 <pre class="tool-call-result">
333${this.toolCall?.result_message.tool_result}</pre
334 >
335 </div>`
336 : ""}`
337 : ""
338 }</div>
339 </sketch-tool-card>`;
340 }
341}
342
343@customElement("sketch-tool-card-codereview")
344export class SketchToolCardCodeReview extends LitElement {
345 @property()
346 toolCall: ToolCall;
347
348 @property()
349 open: boolean;
350
351 static styles = css``;
352
353 constructor() {
354 super();
355 }
356
357 connectedCallback() {
358 super.connectedCallback();
359 }
360
361 disconnectedCallback() {
362 super.disconnectedCallback();
363 }
364 render() {
365 return html` <sketch-tool-card
366 .open=${this.open}
367 .toolCall=${this.toolCall}
368 >
369 <span slot="summary" class="summary-text">
370 ${this.toolCall?.result_message?.tool_result == "OK" ? "✔️" : "⛔"}
371 </span>
372 <div slot="result">
373 <pre>${this.toolCall?.result_message?.tool_result}</pre>
374 </div>
375 </sketch-tool-card>`;
376 }
377}
378
379@customElement("sketch-tool-card-done")
380export class SketchToolCardDone extends LitElement {
381 @property()
382 toolCall: ToolCall;
383
384 @property()
385 open: boolean;
386
387 static styles = css``;
388
389 constructor() {
390 super();
391 }
392
393 connectedCallback() {
394 super.connectedCallback();
395 }
396
397 disconnectedCallback() {
398 super.disconnectedCallback();
399 }
400
401 render() {
402 const doneInput = JSON.parse(this.toolCall.input);
403 return html` <sketch-tool-card
404 .open=${this.open}
405 .toolCall=${this.toolCall}
406 >
407 <span slot="summary" class="summary-text"> </span>
408 <div slot="result">
409 ${Object.keys(doneInput.checklist_items).map((key) => {
410 const item = doneInput.checklist_items[key];
411 let statusIcon = "⛔";
412 if (item.status == "yes") {
413 statusIcon = "👍";
414 } else if (item.status == "not applicable") {
415 statusIcon = "🤷‍♂️";
416 }
417 return html`<div>
418 <span>${statusIcon}</span> ${key}:${item.status}
419 </div>`;
420 })}
421 </div>
422 </sketch-tool-card>`;
423 }
424}
425
426@customElement("sketch-tool-card-patch")
427export class SketchToolCardPatch extends LitElement {
428 @property()
429 toolCall: ToolCall;
430
431 @property()
432 open: boolean;
433
434 static styles = css`
435 .summary-text {
436 color: #555;
437 font-family: monospace;
438 overflow: hidden;
439 text-overflow: ellipsis;
440 white-space: nowrap;
441 border-radius: 3px;
442 }
443 `;
444
445 constructor() {
446 super();
447 }
448
449 connectedCallback() {
450 super.connectedCallback();
451 }
452
453 disconnectedCallback() {
454 super.disconnectedCallback();
455 }
456
457 render() {
458 const patchInput = JSON.parse(this.toolCall?.input);
459 return html` <sketch-tool-card
460 .open=${this.open}
461 .toolCall=${this.toolCall}
462 >
463 <span slot="summary" class="summary-text">
464 ${patchInput?.path}: ${patchInput.patches.length}
465 edit${patchInput.patches.length > 1 ? "s" : ""}
466 </span>
467 <div slot="input">
468 ${patchInput.patches.map((patch) => {
469 return html` Patch operation: <b>${patch.operation}</b>
470 <pre>${patch.newText}</pre>`;
471 })}
472 </div>
473 <div slot="result">
474 <pre>${this.toolCall?.result_message?.tool_result}</pre>
475 </div>
476 </sketch-tool-card>`;
477 }
478}
479
480@customElement("sketch-tool-card-think")
481export class SketchToolCardThink extends LitElement {
482 @property()
483 toolCall: ToolCall;
484
485 @property()
486 open: boolean;
487
488 static styles = css`
489 .thought-bubble {
490 overflow-x: auto;
491 margin-bottom: 3px;
492 font-family: monospace;
493 padding: 3px 5px;
494 background: rgb(236, 236, 236);
495 border-radius: 6px;
496 user-select: text;
497 cursor: text;
498 -webkit-user-select: text;
499 -moz-user-select: text;
500 -ms-user-select: text;
501 font-size: 13px;
502 line-height: 1.3;
503 }
504 .summary-text {
505 overflow: hidden;
506 text-overflow: ellipsis;
507 font-family: monospace;
508 max-width: 50%;
509 }
510 `;
511
512 constructor() {
513 super();
514 }
515
516 connectedCallback() {
517 super.connectedCallback();
518 }
519
520 disconnectedCallback() {
521 super.disconnectedCallback();
522 }
523
524 render() {
525 return html`
526 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
527 <span slot="summary" class="summary-text"
528 >${JSON.parse(this.toolCall?.input)?.thoughts}</span
529 >
530 <div slot="input" class="thought-bubble">
531 <div class="markdown-content">
532 ${unsafeHTML(
533 renderMarkdown(JSON.parse(this.toolCall?.input)?.thoughts),
534 )}
535 </div>
536 </div>
537 </sketch-tool-card>
538 `;
539 }
540}
541
542@customElement("sketch-tool-card-title")
543export class SketchToolCardTitle extends LitElement {
544 @property()
545 toolCall: ToolCall;
546
547 @property()
548 open: boolean;
549
550 static styles = css`
551 .summary-text {
552 font-style: italic;
553 }
554 `;
555 constructor() {
556 super();
557 }
558
559 connectedCallback() {
560 super.connectedCallback();
561 }
562
563 disconnectedCallback() {
564 super.disconnectedCallback();
565 }
566
567 render() {
568 return html`
569 <span class="summary-text"
570 >I've set the title of this sketch to
571 <b>"${JSON.parse(this.toolCall?.input)?.title}"</b></span
572 >
573 `;
574 }
575}
576
577@customElement("sketch-tool-card-generic")
578export class SketchToolCardGeneric extends LitElement {
579 @property()
580 toolCall: ToolCall;
581
582 @property()
583 open: boolean;
584
585 constructor() {
586 super();
587 }
588
589 connectedCallback() {
590 super.connectedCallback();
591 }
592
593 disconnectedCallback() {
594 super.disconnectedCallback();
595 }
596
597 render() {
598 return html` <sketch-tool-card
599 .open=${this.open}
600 .toolCall=${this.toolCall}
601 >
602 <span slot="summary" class="summary-text">${this.toolCall?.input}</span>
603 <div slot="input">
604 Input:
605 <pre>${this.toolCall?.input}</pre>
606 </div>
607 <div slot="result">
608 Result:
609 ${this.toolCall?.result_message
610 ? html` ${this.toolCall?.result_message.tool_result
611 ? html`<pre>${this.toolCall?.result_message.tool_result}</pre>`
612 : ""}`
613 : ""}
614 </div>
615 </sketch-tool-card>`;
616 }
617}
618
619declare global {
620 interface HTMLElementTagNameMap {
621 "sketch-tool-card": SketchToolCard;
622 "sketch-tool-card-generic": SketchToolCardGeneric;
623 "sketch-tool-card-bash": SketchToolCardBash;
624 "sketch-tool-card-codereview": SketchToolCardCodeReview;
625 "sketch-tool-card-done": SketchToolCardDone;
626 "sketch-tool-card-patch": SketchToolCardPatch;
627 "sketch-tool-card-think": SketchToolCardThink;
628 "sketch-tool-card-title": SketchToolCardTitle;
629 }
630}