blob: 33fa050fcefad8231e8cdd935a3c4c67d198ca16 [file] [log] [blame]
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001import { css, html, LitElement } from "lit";
Pokey Rule7ac5ed02025-05-07 15:26:10 +01002import { unsafeHTML } from "lit/directives/unsafe-html.js";
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00003import { customElement, property, state } from "lit/decorators.js";
Autoformatter7e5fe3c2025-06-04 22:24:53 +00004import {
5 ToolCall,
6 MultipleChoiceOption,
7 MultipleChoiceParams,
8 State,
9} from "../types";
philip.zeyliger26bc6592025-06-30 20:15:30 -070010import { marked } from "marked";
Philip Zeyliger53ab2452025-06-04 17:49:33 +000011import DOMPurify from "dompurify";
Pokey Rule7ac5ed02025-05-07 15:26:10 +010012
Philip Zeyliger53ab2452025-06-04 17:49:33 +000013// Shared utility function for markdown rendering with DOMPurify sanitization
Pokey Rule7ac5ed02025-05-07 15:26:10 +010014function renderMarkdown(markdownContent: string): string {
15 try {
Philip Zeyliger53ab2452025-06-04 17:49:33 +000016 // Parse markdown with default settings
17 const htmlOutput = marked.parse(markdownContent, {
Pokey Rule7ac5ed02025-05-07 15:26:10 +010018 gfm: true,
19 breaks: true,
20 async: false,
21 }) as string;
Philip Zeyliger53ab2452025-06-04 17:49:33 +000022
23 // Sanitize the output HTML with DOMPurify
24 return DOMPurify.sanitize(htmlOutput, {
25 // Allow common safe HTML elements
26 ALLOWED_TAGS: [
27 "p",
28 "br",
29 "strong",
30 "em",
31 "b",
32 "i",
33 "u",
34 "s",
35 "code",
36 "pre",
37 "h1",
38 "h2",
39 "h3",
40 "h4",
41 "h5",
42 "h6",
43 "ul",
44 "ol",
45 "li",
46 "blockquote",
47 "a",
48 ],
49 ALLOWED_ATTR: [
50 "href",
51 "title",
52 "target",
53 "rel", // For links
54 "class", // For basic styling
55 ],
56 // Keep content formatting
57 KEEP_CONTENT: true,
58 });
Pokey Rule7ac5ed02025-05-07 15:26:10 +010059 } catch (error) {
60 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +000061 // Fallback to sanitized plain text if markdown parsing fails
62 return DOMPurify.sanitize(markdownContent);
Pokey Rule7ac5ed02025-05-07 15:26:10 +010063 }
64}
65
66// Common styles shared across all tool cards
67const commonStyles = css`
Philip Zeyligere31d2a92025-05-11 15:22:35 -070068 :host {
69 display: block;
70 max-width: 100%;
71 width: 100%;
72 box-sizing: border-box;
73 overflow: hidden;
74 }
Pokey Rule7ac5ed02025-05-07 15:26:10 +010075 pre {
76 background: rgb(236, 236, 236);
77 color: black;
78 padding: 0.5em;
79 border-radius: 4px;
80 white-space: pre-wrap;
81 word-break: break-word;
82 max-width: 100%;
83 width: 100%;
84 box-sizing: border-box;
85 overflow-wrap: break-word;
86 }
87 .summary-text {
Philip Zeyligere31d2a92025-05-11 15:22:35 -070088 overflow: hidden !important;
89 text-overflow: ellipsis !important;
90 white-space: nowrap !important;
91 max-width: 100% !important;
92 width: 100% !important;
Pokey Rule7ac5ed02025-05-07 15:26:10 +010093 font-family: monospace;
Philip Zeyligere31d2a92025-05-11 15:22:35 -070094 display: block;
Pokey Rule7ac5ed02025-05-07 15:26:10 +010095 }
96`;
Pokey Rule5e8aead2025-05-06 16:21:57 +010097
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070098@customElement("sketch-tool-card")
99export class SketchToolCard extends LitElement {
Pokey Rule5e8aead2025-05-06 16:21:57 +0100100 @property() toolCall: ToolCall;
101 @property() open: boolean;
102 @state() detailsVisible: boolean = false;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000103
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700104 static styles = css`
105 .tool-call {
106 display: flex;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000107 flex-direction: column;
108 width: 100%;
109 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000110 .tool-row {
111 display: flex;
112 width: 100%;
113 box-sizing: border-box;
114 padding: 6px 8px 6px 12px;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700115 align-items: center;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100116 gap: 8px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000117 cursor: pointer;
118 border-radius: 4px;
119 position: relative;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100120 overflow: hidden;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700121 flex-wrap: wrap;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000122 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000123 .tool-row:hover {
124 background-color: rgba(0, 0, 0, 0.02);
125 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000126 .tool-name {
127 font-family: monospace;
128 font-weight: 500;
129 color: #444;
130 background-color: rgba(0, 0, 0, 0.05);
131 border-radius: 3px;
132 padding: 2px 6px;
133 flex-shrink: 0;
134 min-width: 45px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000135 font-size: 12px;
136 text-align: center;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700137 white-space: nowrap;
138 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000139 .tool-success {
140 color: #5cb85c;
141 font-size: 14px;
142 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000143 .tool-error {
Josh Bleecher Snydere750ec92025-05-05 23:01:57 +0000144 color: #6c757d;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000145 font-size: 14px;
146 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000147 .tool-pending {
148 color: #f0ad4e;
149 font-size: 14px;
150 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000151 .summary-text {
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700152 white-space: normal;
153 overflow-wrap: break-word;
154 word-break: break-word;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000155 flex-grow: 1;
156 flex-shrink: 1;
157 color: #444;
158 font-family: monospace;
159 font-size: 12px;
160 padding: 0 4px;
161 min-width: 50px;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700162 max-width: calc(100% - 150px);
Pokey Rule5e8aead2025-05-06 16:21:57 +0100163 display: inline-block;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000164 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000165 .tool-status {
166 display: flex;
167 align-items: center;
168 gap: 12px;
169 margin-left: auto;
170 flex-shrink: 0;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100171 min-width: 120px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000172 justify-content: flex-end;
173 padding-right: 8px;
174 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700175 .tool-call-status {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000176 display: flex;
177 align-items: center;
178 justify-content: center;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700179 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700180 .tool-call-status.spinner {
181 animation: spin 1s infinite linear;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700182 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700183 @keyframes spin {
184 0% {
185 transform: rotate(0deg);
186 }
187 100% {
188 transform: rotate(360deg);
189 }
190 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000191 .elapsed {
192 font-size: 11px;
193 color: #777;
194 white-space: nowrap;
195 min-width: 40px;
196 text-align: right;
197 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000198 .tool-details {
199 padding: 8px;
200 background-color: rgba(0, 0, 0, 0.02);
201 margin-top: 1px;
202 border-top: 1px solid rgba(0, 0, 0, 0.05);
203 display: none;
204 font-family: monospace;
205 font-size: 12px;
206 color: #333;
207 border-radius: 0 0 4px 4px;
208 max-width: 100%;
209 width: 100%;
210 box-sizing: border-box;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100211 overflow: hidden;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000212 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000213 .tool-details.visible {
214 display: block;
215 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700216 .cancel-button {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700217 cursor: pointer;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000218 color: white;
219 background-color: #d9534f;
220 border: none;
221 border-radius: 3px;
222 font-size: 11px;
223 padding: 2px 6px;
224 white-space: nowrap;
225 min-width: 50px;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700226 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700227 .cancel-button:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000228 background-color: #c9302c;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700229 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000230 .cancel-button[disabled] {
231 background-color: #999;
232 cursor: not-allowed;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700233 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700234 `;
235
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700236 _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700237 button.innerText = "Cancelling";
238 button.disabled = true;
239 try {
240 const response = await fetch("cancel", {
241 method: "POST",
Pokey Rule5e8aead2025-05-06 16:21:57 +0100242 headers: { "Content-Type": "application/json" },
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700243 body: JSON.stringify({
244 tool_call_id: tool_call_id,
245 reason: "user requested cancellation",
246 }),
247 });
248 if (response.ok) {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700249 button.parentElement.removeChild(button);
250 } else {
251 button.innerText = "Cancel";
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700252 }
253 } catch (e) {
254 console.error("cancel", tool_call_id, e);
255 }
256 };
257
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000258 _toggleDetails(e: Event) {
259 e.stopPropagation();
260 this.detailsVisible = !this.detailsVisible;
261 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700262
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000263 render() {
Pokey Rule5e8aead2025-05-06 16:21:57 +0100264 // Status indicator based on result
265 let statusIcon = html`<span class="tool-call-status spinner tool-pending"
266 >⏳</span
267 >`;
268 if (this.toolCall?.result_message) {
269 statusIcon = this.toolCall?.result_message.tool_error
Josh Bleecher Snyderc3c20232025-05-07 05:46:04 -0700270 ? html`<span class="tool-call-status tool-error">〰️</span>`
Pokey Rule5e8aead2025-05-06 16:21:57 +0100271 : html`<span class="tool-call-status tool-success">✓</span>`;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000272 }
273
274 // Cancel button for pending operations
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700275 const cancelButton = this.toolCall?.result_message
276 ? ""
277 : html`<button
278 class="cancel-button"
279 title="Cancel this operation"
280 @click=${(e: Event) => {
281 e.stopPropagation();
Pokey Rule5e8aead2025-05-06 16:21:57 +0100282 this._cancelToolCall(
283 this.toolCall?.tool_call_id,
284 e.target as HTMLButtonElement,
285 );
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700286 }}
287 >
288 Cancel
289 </button>`;
290
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000291 // Elapsed time display
292 const elapsed = this.toolCall?.result_message?.elapsed
Sean McCullough2deac842025-04-21 18:17:57 -0700293 ? html`<span class="elapsed"
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000294 >${(this.toolCall?.result_message?.elapsed / 1e9).toFixed(1)}s</span
Sean McCullough2deac842025-04-21 18:17:57 -0700295 >`
Pokey Rule5e8aead2025-05-06 16:21:57 +0100296 : html`<span class="elapsed"></span>`;
Sean McCullough2deac842025-04-21 18:17:57 -0700297
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000298 // Initialize details visibility based on open property
299 if (this.open && !this.detailsVisible) {
300 this.detailsVisible = true;
301 }
302
303 return html`<div class="tool-call">
304 <div class="tool-row" @click=${this._toggleDetails}>
305 <span class="tool-name">${this.toolCall?.name}</span>
306 <span class="summary-text"><slot name="summary"></slot></span>
307 <div class="tool-status">${statusIcon} ${elapsed} ${cancelButton}</div>
308 </div>
309 <div class="tool-details ${this.detailsVisible ? "visible" : ""}">
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700310 <slot name="input"></slot>
311 <slot name="result"></slot>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000312 </div>
313 </div>`;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700314 }
315}
316
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100317@customElement("sketch-tool-card-bash")
318export class SketchToolCardBash extends LitElement {
319 @property() toolCall: ToolCall;
320 @property() open: boolean;
321
322 static styles = [
323 commonStyles,
324 css`
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700325 :host {
326 max-width: 100%;
327 display: block;
328 }
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100329 .input {
330 display: flex;
331 width: 100%;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700332 max-width: 100%;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100333 flex-direction: column;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700334 overflow-wrap: break-word;
335 word-break: break-word;
336 }
337 .command-wrapper {
338 max-width: 100%;
339 overflow: hidden;
340 text-overflow: ellipsis;
341 white-space: nowrap;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100342 }
343 .input pre {
344 width: 100%;
345 margin-bottom: 0;
346 border-radius: 4px 4px 0 0;
347 box-sizing: border-box;
348 }
349 .result pre {
350 margin-top: 0;
351 color: #555;
352 border-radius: 0 0 4px 4px;
353 width: 100%;
354 box-sizing: border-box;
355 }
356 .result pre.scrollable-on-hover {
357 max-height: 300px;
358 overflow-y: auto;
359 }
360 .tool-call-result-container {
361 width: 100%;
362 position: relative;
363 }
364 .background-badge {
365 display: inline-block;
366 background-color: #6200ea;
367 color: white;
368 font-size: 10px;
369 font-weight: bold;
370 padding: 2px 6px;
371 border-radius: 10px;
372 margin-left: 8px;
373 vertical-align: middle;
374 }
375 .command-wrapper {
376 display: inline-block;
377 max-width: 100%;
378 overflow: hidden;
379 text-overflow: ellipsis;
380 white-space: nowrap;
381 }
382 `,
383 ];
384
385 render() {
386 const inputData = JSON.parse(this.toolCall?.input || "{}");
387 const isBackground = inputData?.background === true;
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000388 const isSlowOk = inputData?.slow_ok === true;
389 const backgroundIcon = isBackground ? "🥷 " : "";
390 const slowIcon = isSlowOk ? "🐢 " : "";
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100391
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700392 // Truncate the command if it's too long to display nicely
393 const command = inputData?.command || "";
394 const displayCommand =
395 command.length > 80 ? command.substring(0, 80) + "..." : command;
396
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100397 return html` <sketch-tool-card
398 .open=${this.open}
399 .toolCall=${this.toolCall}
400 >
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700401 <span
402 slot="summary"
403 class="summary-text"
404 style="display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
405 >
406 <div
407 class="command-wrapper"
408 style="max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
409 >
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000410 ${backgroundIcon}${slowIcon}${displayCommand}
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100411 </div>
412 </span>
413 <div slot="input" class="input">
414 <div class="tool-call-result-container">
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000415 <pre>${backgroundIcon}${slowIcon}${inputData?.command}</pre>
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100416 </div>
417 </div>
418 ${this.toolCall?.result_message?.tool_result
419 ? html`<div slot="result" class="result">
420 <div class="tool-call-result-container">
421 <pre class="tool-call-result">
422${this.toolCall?.result_message.tool_result}</pre
423 >
424 </div>
425 </div>`
426 : ""}
427 </sketch-tool-card>`;
428 }
429}
430
431@customElement("sketch-tool-card-codereview")
432export class SketchToolCardCodeReview extends LitElement {
433 @property() toolCall: ToolCall;
434 @property() open: boolean;
435
436 // Determine the status icon based on the content of the result message
437 getStatusIcon(resultText: string): string {
438 if (!resultText) return "";
439 if (resultText === "OK") return "✔️";
440 if (resultText.includes("# Errors")) return "⚠️";
441 if (resultText.includes("# Info")) return "ℹ️";
442 if (resultText.includes("uncommitted changes in repo")) return "🧹";
443 if (resultText.includes("no new commits have been added")) return "🐣";
444 if (resultText.includes("git repo is not clean")) return "🧼";
445 return "❓";
446 }
447
448 render() {
449 const resultText = this.toolCall?.result_message?.tool_result || "";
450 const statusIcon = this.getStatusIcon(resultText);
451
452 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
453 <span slot="summary" class="summary-text">${statusIcon}</span>
454 <div slot="result"><pre>${resultText}</pre></div>
455 </sketch-tool-card>`;
456 }
457}
458
459@customElement("sketch-tool-card-done")
460export class SketchToolCardDone extends LitElement {
461 @property() toolCall: ToolCall;
462 @property() open: boolean;
463
464 render() {
465 const doneInput = JSON.parse(this.toolCall.input);
466 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
467 <span slot="summary" class="summary-text"></span>
468 <div slot="result">
469 ${Object.keys(doneInput.checklist_items).map((key) => {
470 const item = doneInput.checklist_items[key];
Josh Bleecher Snyderfbbf83b2025-05-15 10:55:55 -0700471 let statusIcon = "〰️";
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100472 if (item.status == "yes") {
Josh Bleecher Snyderfbbf83b2025-05-15 10:55:55 -0700473 statusIcon = "✅";
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100474 } else if (item.status == "not applicable") {
Josh Bleecher Snyderfbbf83b2025-05-15 10:55:55 -0700475 statusIcon = "🤷";
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100476 }
477 return html`<div>
478 <span>${statusIcon}</span> ${key}:${item.status}
479 </div>`;
480 })}
481 </div>
482 </sketch-tool-card>`;
483 }
484}
485
486@customElement("sketch-tool-card-patch")
487export class SketchToolCardPatch extends LitElement {
488 @property() toolCall: ToolCall;
489 @property() open: boolean;
490
491 static styles = css`
492 .summary-text {
493 color: #555;
494 font-family: monospace;
495 overflow: hidden;
496 text-overflow: ellipsis;
497 white-space: nowrap;
498 border-radius: 3px;
499 }
500 `;
501
502 render() {
503 const patchInput = JSON.parse(this.toolCall?.input);
504 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
505 <span slot="summary" class="summary-text">
506 ${patchInput?.path}: ${patchInput.patches.length}
507 edit${patchInput.patches.length > 1 ? "s" : ""}
508 </span>
509 <div slot="input">
510 ${patchInput.patches.map((patch) => {
511 return html`Patch operation: <b>${patch.operation}</b>
512 <pre>${patch.newText}</pre>`;
513 })}
514 </div>
515 <div slot="result">
516 <pre>${this.toolCall?.result_message?.tool_result}</pre>
517 </div>
518 </sketch-tool-card>`;
519 }
520}
521
522@customElement("sketch-tool-card-think")
523export class SketchToolCardThink extends LitElement {
524 @property() toolCall: ToolCall;
525 @property() open: boolean;
526
527 static styles = css`
528 .thought-bubble {
529 overflow-x: auto;
530 margin-bottom: 3px;
531 font-family: monospace;
532 padding: 3px 5px;
533 background: rgb(236, 236, 236);
534 border-radius: 6px;
535 user-select: text;
536 cursor: text;
537 -webkit-user-select: text;
538 -moz-user-select: text;
539 -ms-user-select: text;
540 font-size: 13px;
541 line-height: 1.3;
542 }
543 .summary-text {
544 overflow: hidden;
545 text-overflow: ellipsis;
546 font-family: monospace;
547 }
548 `;
549
550 render() {
551 return html`
552 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
553 <span slot="summary" class="summary-text">
554 ${JSON.parse(this.toolCall?.input)?.thoughts?.split("\n")[0]}
555 </span>
556 <div slot="input" class="thought-bubble">
557 <div class="markdown-content">
558 ${unsafeHTML(
559 renderMarkdown(JSON.parse(this.toolCall?.input)?.thoughts),
560 )}
561 </div>
562 </div>
563 </sketch-tool-card>
564 `;
565 }
566}
567
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700568@customElement("sketch-tool-card-set-slug")
569export class SketchToolCardSetSlug extends LitElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100570 @property() toolCall: ToolCall;
571 @property() open: boolean;
572
573 static styles = css`
574 .summary-text {
575 font-style: italic;
576 }
577 pre {
578 display: inline;
579 font-family: monospace;
580 background: rgb(236, 236, 236);
581 padding: 2px 4px;
582 border-radius: 2px;
583 margin: 0;
584 }
585 `;
586
587 render() {
588 const inputData = JSON.parse(this.toolCall?.input || "{}");
589 return html`
590 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
591 <span slot="summary" class="summary-text">
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700592 Slug: "${inputData.slug}"
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100593 </span>
594 <div slot="input">
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700595 <div>Set slug to: <b>${inputData.slug}</b></div>
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000596 </div>
597 </sketch-tool-card>
598 `;
599 }
600}
601
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700602@customElement("sketch-tool-card-commit-message-style")
603export class SketchToolCardCommitMessageStyle extends LitElement {
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000604 @property()
605 toolCall: ToolCall;
606
607 @property()
608 open: boolean;
609
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000610 @property()
611 state: State;
612
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000613 static styles = css`
614 .summary-text {
615 font-style: italic;
616 }
617 pre {
618 display: inline;
619 font-family: monospace;
620 background: rgb(236, 236, 236);
621 padding: 2px 4px;
622 border-radius: 2px;
623 margin: 0;
624 }
625 `;
626 constructor() {
627 super();
628 }
629
630 connectedCallback() {
631 super.connectedCallback();
632 }
633
634 disconnectedCallback() {
635 super.disconnectedCallback();
636 }
637
638 render() {
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000639 return html`
640 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100641 </sketch-tool-card>
642 `;
643 }
644}
645
646@customElement("sketch-tool-card-multiple-choice")
647export class SketchToolCardMultipleChoice extends LitElement {
648 @property() toolCall: ToolCall;
649 @property() open: boolean;
650 @property() selectedOption: MultipleChoiceOption = null;
651
652 static styles = css`
653 .options-container {
654 display: flex;
655 flex-direction: row;
656 flex-wrap: wrap;
657 gap: 8px;
658 margin: 10px 0;
659 }
660 .option {
661 display: inline-flex;
662 align-items: center;
663 padding: 8px 12px;
664 border-radius: 4px;
665 background-color: #f5f5f5;
666 cursor: pointer;
667 transition: all 0.2s;
668 border: 1px solid transparent;
669 user-select: none;
670 }
671 .option:hover {
672 background-color: #e0e0e0;
673 border-color: #ccc;
674 transform: translateY(-1px);
675 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
676 }
677 .option:active {
678 transform: translateY(0);
679 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
680 background-color: #d5d5d5;
681 }
682 .option.selected {
683 background-color: #e3f2fd;
684 border-color: #2196f3;
685 border-width: 1px;
686 border-style: solid;
687 }
688 .option-checkmark {
689 margin-left: 6px;
690 color: #2196f3;
691 }
692 .summary-text {
693 font-style: italic;
694 padding: 0.5em;
695 }
696 .summary-text strong {
697 font-style: normal;
698 color: #2196f3;
699 font-weight: 600;
700 }
701 `;
702
703 connectedCallback() {
704 super.connectedCallback();
705 this.updateSelectedOption();
706 }
707
708 updated(changedProps) {
709 if (changedProps.has("toolCall")) {
710 this.updateSelectedOption();
711 }
712 }
713
714 updateSelectedOption() {
715 if (this.toolCall?.result_message?.tool_result) {
716 try {
717 this.selectedOption = JSON.parse(
718 this.toolCall.result_message.tool_result,
719 ).selected;
720 } catch (e) {
721 console.error("Error parsing result:", e);
722 }
723 } else {
724 this.selectedOption = null;
725 }
726 }
727
728 async handleOptionClick(choice) {
729 this.selectedOption = this.selectedOption === choice ? null : choice;
730
731 const event = new CustomEvent("multiple-choice-selected", {
732 detail: {
733 responseText: this.selectedOption.responseText,
734 toolCall: this.toolCall,
735 },
736 bubbles: true,
737 composed: true,
738 });
739 this.dispatchEvent(event);
740 }
741
742 render() {
743 let choices = [];
744 let question = "";
745 try {
746 const inputData = JSON.parse(
747 this.toolCall?.input || "{}",
748 ) as MultipleChoiceParams;
749 choices = inputData.responseOptions || [];
750 question = inputData.question || "Please select an option:";
751 } catch (e) {
752 console.error("Error parsing multiple-choice input:", e);
753 }
754
755 const summaryContent =
756 this.selectedOption !== null
757 ? html`<span class="summary-text">
758 ${question}: <strong>${this.selectedOption.caption}</strong>
759 </span>`
760 : html`<span class="summary-text">${question}</span>`;
761
762 return html`
763 <div class="multiple-choice-card">
764 ${summaryContent}
765 <div class="options-container">
766 ${choices.map((choice) => {
767 const isSelected =
768 this.selectedOption !== null && this.selectedOption === choice;
769 return html`
770 <div
771 class="option ${isSelected ? "selected" : ""}"
772 @click=${() => this.handleOptionClick(choice)}
773 title="${choice.responseText}"
774 >
775 <span class="option-label">${choice.caption}</span>
776 ${isSelected
777 ? html`<span class="option-checkmark">✓</span>`
778 : ""}
779 </div>
780 `;
781 })}
782 </div>
783 </div>
784 `;
785 }
786}
787
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700788@customElement("sketch-tool-card-todo-write")
789export class SketchToolCardTodoWrite extends LitElement {
790 @property() toolCall: ToolCall;
791 @property() open: boolean;
792
793 static styles = css`
794 .summary-text {
795 font-style: italic;
796 color: #666;
797 }
798 `;
799
800 render() {
801 const inputData = JSON.parse(this.toolCall?.input || "{}");
802 const tasks = inputData.tasks || [];
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000803
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700804 // Generate circles based on task status
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000805 const circles = tasks
806 .map((task) => {
807 switch (task.status) {
808 case "completed":
809 return "●"; // full circle
810 case "in-progress":
811 return "◐"; // half circle
812 case "queued":
813 default:
814 return "○"; // empty circle
815 }
816 })
817 .join(" ");
818
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700819 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000820 <span slot="summary" class="summary-text"> ${circles} </span>
821 <div slot="result">
822 <pre>${this.toolCall?.result_message?.tool_result}</pre>
823 </div>
824 </sketch-tool-card>`;
825 }
826}
827
828@customElement("sketch-tool-card-keyword-search")
829export class SketchToolCardKeywordSearch extends LitElement {
830 @property() toolCall: ToolCall;
831 @property() open: boolean;
832
833 static styles = css`
834 .summary-container {
835 display: flex;
836 flex-direction: column;
837 gap: 2px;
838 width: 100%;
839 max-width: 100%;
840 overflow: hidden;
841 }
842 .query-line {
843 color: #333;
844 font-family: inherit;
845 font-size: 12px;
846 font-weight: normal;
847 white-space: normal;
848 word-wrap: break-word;
849 word-break: break-word;
850 overflow-wrap: break-word;
851 line-height: 1.2;
852 }
853 .keywords-line {
854 color: #666;
855 font-family: inherit;
856 font-size: 11px;
857 font-weight: normal;
858 white-space: normal;
859 word-wrap: break-word;
860 word-break: break-word;
861 overflow-wrap: break-word;
862 line-height: 1.2;
863 margin-top: 1px;
864 }
865 `;
866
867 render() {
868 const inputData = JSON.parse(this.toolCall?.input || "{}");
869 const query = inputData.query || "";
870 const searchTerms = inputData.search_terms || [];
871
872 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
873 <div slot="summary" class="summary-container">
874 <div class="query-line">🔍 ${query}</div>
875 <div class="keywords-line">🗝️ ${searchTerms.join(", ")}</div>
876 </div>
877 <div slot="input">
878 <div><strong>Query:</strong> ${query}</div>
879 <div><strong>Search terms:</strong> ${searchTerms.join(", ")}</div>
880 </div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700881 <div slot="result">
882 <pre>${this.toolCall?.result_message?.tool_result}</pre>
883 </div>
884 </sketch-tool-card>`;
885 }
886}
887
888@customElement("sketch-tool-card-todo-read")
889export class SketchToolCardTodoRead extends LitElement {
890 @property() toolCall: ToolCall;
891 @property() open: boolean;
892
893 static styles = css`
894 .summary-text {
895 font-style: italic;
896 color: #666;
897 }
898 `;
899
900 render() {
901 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000902 <span slot="summary" class="summary-text"> Read todo list </span>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700903 <div slot="result">
904 <pre>${this.toolCall?.result_message?.tool_result}</pre>
905 </div>
906 </sketch-tool-card>`;
907 }
908}
909
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100910@customElement("sketch-tool-card-generic")
911export class SketchToolCardGeneric extends LitElement {
912 @property() toolCall: ToolCall;
913 @property() open: boolean;
914
915 render() {
916 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700917 <span
918 slot="summary"
919 style="display: block; white-space: normal; word-break: break-word; overflow-wrap: break-word; max-width: 100%; width: 100%;"
920 >${this.toolCall?.input}</span
921 >
922 <div
923 slot="input"
924 style="max-width: 100%; overflow-wrap: break-word; word-break: break-word;"
925 >
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100926 Input:
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700927 <pre
928 style="max-width: 100%; white-space: pre-wrap; overflow-wrap: break-word; word-break: break-word;"
929 >
930${this.toolCall?.input}</pre
931 >
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100932 </div>
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700933 <div
934 slot="result"
935 style="max-width: 100%; overflow-wrap: break-word; word-break: break-word;"
936 >
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100937 Result:
938 ${this.toolCall?.result_message?.tool_result
939 ? html`<pre>${this.toolCall?.result_message.tool_result}</pre>`
940 : ""}
941 </div>
942 </sketch-tool-card>`;
943 }
944}
945
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700946declare global {
947 interface HTMLElementTagNameMap {
948 "sketch-tool-card": SketchToolCard;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100949 "sketch-tool-card-generic": SketchToolCardGeneric;
950 "sketch-tool-card-bash": SketchToolCardBash;
951 "sketch-tool-card-codereview": SketchToolCardCodeReview;
952 "sketch-tool-card-done": SketchToolCardDone;
953 "sketch-tool-card-patch": SketchToolCardPatch;
954 "sketch-tool-card-think": SketchToolCardThink;
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700955 "sketch-tool-card-set-slug": SketchToolCardSetSlug;
956 "sketch-tool-card-commit-message-style": SketchToolCardCommitMessageStyle;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100957 "sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700958 "sketch-tool-card-todo-write": SketchToolCardTodoWrite;
959 "sketch-tool-card-todo-read": SketchToolCardTodoRead;
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000960 "sketch-tool-card-keyword-search": SketchToolCardKeywordSearch;
Philip Zeyliger84a8ae62025-05-13 16:36:01 -0700961 // TODO: We haven't implemented this for browser tools.
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700962 }
963}