blob: a6c252ae3f6a812d7c5225c7234a7657a7c5d719 [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";
Philip Zeyligerbe7802a2025-06-04 20:15:25 +00004import { ToolCall, MultipleChoiceOption, MultipleChoiceParams, State } from "../types";
Philip Zeyliger53ab2452025-06-04 17:49:33 +00005import { marked, MarkedOptions, Renderer } from "marked";
6import DOMPurify from "dompurify";
Pokey Rule7ac5ed02025-05-07 15:26:10 +01007
Philip Zeyliger53ab2452025-06-04 17:49:33 +00008// Shared utility function for markdown rendering with DOMPurify sanitization
Pokey Rule7ac5ed02025-05-07 15:26:10 +01009function renderMarkdown(markdownContent: string): string {
10 try {
Philip Zeyliger53ab2452025-06-04 17:49:33 +000011 // Parse markdown with default settings
12 const htmlOutput = marked.parse(markdownContent, {
Pokey Rule7ac5ed02025-05-07 15:26:10 +010013 gfm: true,
14 breaks: true,
15 async: false,
16 }) as string;
Philip Zeyliger53ab2452025-06-04 17:49:33 +000017
18 // Sanitize the output HTML with DOMPurify
19 return DOMPurify.sanitize(htmlOutput, {
20 // Allow common safe HTML elements
21 ALLOWED_TAGS: [
22 "p",
23 "br",
24 "strong",
25 "em",
26 "b",
27 "i",
28 "u",
29 "s",
30 "code",
31 "pre",
32 "h1",
33 "h2",
34 "h3",
35 "h4",
36 "h5",
37 "h6",
38 "ul",
39 "ol",
40 "li",
41 "blockquote",
42 "a",
43 ],
44 ALLOWED_ATTR: [
45 "href",
46 "title",
47 "target",
48 "rel", // For links
49 "class", // For basic styling
50 ],
51 // Keep content formatting
52 KEEP_CONTENT: true,
53 });
Pokey Rule7ac5ed02025-05-07 15:26:10 +010054 } catch (error) {
55 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +000056 // Fallback to sanitized plain text if markdown parsing fails
57 return DOMPurify.sanitize(markdownContent);
Pokey Rule7ac5ed02025-05-07 15:26:10 +010058 }
59}
60
61// Common styles shared across all tool cards
62const commonStyles = css`
Philip Zeyligere31d2a92025-05-11 15:22:35 -070063 :host {
64 display: block;
65 max-width: 100%;
66 width: 100%;
67 box-sizing: border-box;
68 overflow: hidden;
69 }
Pokey Rule7ac5ed02025-05-07 15:26:10 +010070 pre {
71 background: rgb(236, 236, 236);
72 color: black;
73 padding: 0.5em;
74 border-radius: 4px;
75 white-space: pre-wrap;
76 word-break: break-word;
77 max-width: 100%;
78 width: 100%;
79 box-sizing: border-box;
80 overflow-wrap: break-word;
81 }
82 .summary-text {
Philip Zeyligere31d2a92025-05-11 15:22:35 -070083 overflow: hidden !important;
84 text-overflow: ellipsis !important;
85 white-space: nowrap !important;
86 max-width: 100% !important;
87 width: 100% !important;
Pokey Rule7ac5ed02025-05-07 15:26:10 +010088 font-family: monospace;
Philip Zeyligere31d2a92025-05-11 15:22:35 -070089 display: block;
Pokey Rule7ac5ed02025-05-07 15:26:10 +010090 }
91`;
Pokey Rule5e8aead2025-05-06 16:21:57 +010092
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070093@customElement("sketch-tool-card")
94export class SketchToolCard extends LitElement {
Pokey Rule5e8aead2025-05-06 16:21:57 +010095 @property() toolCall: ToolCall;
96 @property() open: boolean;
97 @state() detailsVisible: boolean = false;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000098
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070099 static styles = css`
100 .tool-call {
101 display: flex;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000102 flex-direction: column;
103 width: 100%;
104 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000105 .tool-row {
106 display: flex;
107 width: 100%;
108 box-sizing: border-box;
109 padding: 6px 8px 6px 12px;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700110 align-items: center;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100111 gap: 8px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000112 cursor: pointer;
113 border-radius: 4px;
114 position: relative;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100115 overflow: hidden;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700116 flex-wrap: wrap;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000117 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000118 .tool-row:hover {
119 background-color: rgba(0, 0, 0, 0.02);
120 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000121 .tool-name {
122 font-family: monospace;
123 font-weight: 500;
124 color: #444;
125 background-color: rgba(0, 0, 0, 0.05);
126 border-radius: 3px;
127 padding: 2px 6px;
128 flex-shrink: 0;
129 min-width: 45px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000130 font-size: 12px;
131 text-align: center;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700132 white-space: nowrap;
133 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000134 .tool-success {
135 color: #5cb85c;
136 font-size: 14px;
137 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000138 .tool-error {
Josh Bleecher Snydere750ec92025-05-05 23:01:57 +0000139 color: #6c757d;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000140 font-size: 14px;
141 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000142 .tool-pending {
143 color: #f0ad4e;
144 font-size: 14px;
145 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000146 .summary-text {
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700147 white-space: normal;
148 overflow-wrap: break-word;
149 word-break: break-word;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000150 flex-grow: 1;
151 flex-shrink: 1;
152 color: #444;
153 font-family: monospace;
154 font-size: 12px;
155 padding: 0 4px;
156 min-width: 50px;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700157 max-width: calc(100% - 150px);
Pokey Rule5e8aead2025-05-06 16:21:57 +0100158 display: inline-block;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000159 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000160 .tool-status {
161 display: flex;
162 align-items: center;
163 gap: 12px;
164 margin-left: auto;
165 flex-shrink: 0;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100166 min-width: 120px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000167 justify-content: flex-end;
168 padding-right: 8px;
169 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700170 .tool-call-status {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000171 display: flex;
172 align-items: center;
173 justify-content: center;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700174 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700175 .tool-call-status.spinner {
176 animation: spin 1s infinite linear;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700177 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700178 @keyframes spin {
179 0% {
180 transform: rotate(0deg);
181 }
182 100% {
183 transform: rotate(360deg);
184 }
185 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000186 .elapsed {
187 font-size: 11px;
188 color: #777;
189 white-space: nowrap;
190 min-width: 40px;
191 text-align: right;
192 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000193 .tool-details {
194 padding: 8px;
195 background-color: rgba(0, 0, 0, 0.02);
196 margin-top: 1px;
197 border-top: 1px solid rgba(0, 0, 0, 0.05);
198 display: none;
199 font-family: monospace;
200 font-size: 12px;
201 color: #333;
202 border-radius: 0 0 4px 4px;
203 max-width: 100%;
204 width: 100%;
205 box-sizing: border-box;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100206 overflow: hidden;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000207 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000208 .tool-details.visible {
209 display: block;
210 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700211 .cancel-button {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700212 cursor: pointer;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000213 color: white;
214 background-color: #d9534f;
215 border: none;
216 border-radius: 3px;
217 font-size: 11px;
218 padding: 2px 6px;
219 white-space: nowrap;
220 min-width: 50px;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700221 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700222 .cancel-button:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000223 background-color: #c9302c;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700224 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000225 .cancel-button[disabled] {
226 background-color: #999;
227 cursor: not-allowed;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700228 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700229 `;
230
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700231 _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700232 button.innerText = "Cancelling";
233 button.disabled = true;
234 try {
235 const response = await fetch("cancel", {
236 method: "POST",
Pokey Rule5e8aead2025-05-06 16:21:57 +0100237 headers: { "Content-Type": "application/json" },
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700238 body: JSON.stringify({
239 tool_call_id: tool_call_id,
240 reason: "user requested cancellation",
241 }),
242 });
243 if (response.ok) {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700244 button.parentElement.removeChild(button);
245 } else {
246 button.innerText = "Cancel";
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700247 }
248 } catch (e) {
249 console.error("cancel", tool_call_id, e);
250 }
251 };
252
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000253 _toggleDetails(e: Event) {
254 e.stopPropagation();
255 this.detailsVisible = !this.detailsVisible;
256 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700257
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000258 render() {
Pokey Rule5e8aead2025-05-06 16:21:57 +0100259 // Status indicator based on result
260 let statusIcon = html`<span class="tool-call-status spinner tool-pending"
261 >⏳</span
262 >`;
263 if (this.toolCall?.result_message) {
264 statusIcon = this.toolCall?.result_message.tool_error
Josh Bleecher Snyderc3c20232025-05-07 05:46:04 -0700265 ? html`<span class="tool-call-status tool-error">〰️</span>`
Pokey Rule5e8aead2025-05-06 16:21:57 +0100266 : html`<span class="tool-call-status tool-success">✓</span>`;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000267 }
268
269 // Cancel button for pending operations
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700270 const cancelButton = this.toolCall?.result_message
271 ? ""
272 : html`<button
273 class="cancel-button"
274 title="Cancel this operation"
275 @click=${(e: Event) => {
276 e.stopPropagation();
Pokey Rule5e8aead2025-05-06 16:21:57 +0100277 this._cancelToolCall(
278 this.toolCall?.tool_call_id,
279 e.target as HTMLButtonElement,
280 );
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700281 }}
282 >
283 Cancel
284 </button>`;
285
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000286 // Elapsed time display
287 const elapsed = this.toolCall?.result_message?.elapsed
Sean McCullough2deac842025-04-21 18:17:57 -0700288 ? html`<span class="elapsed"
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000289 >${(this.toolCall?.result_message?.elapsed / 1e9).toFixed(1)}s</span
Sean McCullough2deac842025-04-21 18:17:57 -0700290 >`
Pokey Rule5e8aead2025-05-06 16:21:57 +0100291 : html`<span class="elapsed"></span>`;
Sean McCullough2deac842025-04-21 18:17:57 -0700292
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000293 // Initialize details visibility based on open property
294 if (this.open && !this.detailsVisible) {
295 this.detailsVisible = true;
296 }
297
298 return html`<div class="tool-call">
299 <div class="tool-row" @click=${this._toggleDetails}>
300 <span class="tool-name">${this.toolCall?.name}</span>
301 <span class="summary-text"><slot name="summary"></slot></span>
302 <div class="tool-status">${statusIcon} ${elapsed} ${cancelButton}</div>
303 </div>
304 <div class="tool-details ${this.detailsVisible ? "visible" : ""}">
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700305 <slot name="input"></slot>
306 <slot name="result"></slot>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000307 </div>
308 </div>`;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700309 }
310}
311
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100312@customElement("sketch-tool-card-bash")
313export class SketchToolCardBash extends LitElement {
314 @property() toolCall: ToolCall;
315 @property() open: boolean;
316
317 static styles = [
318 commonStyles,
319 css`
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700320 :host {
321 max-width: 100%;
322 display: block;
323 }
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100324 .input {
325 display: flex;
326 width: 100%;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700327 max-width: 100%;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100328 flex-direction: column;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700329 overflow-wrap: break-word;
330 word-break: break-word;
331 }
332 .command-wrapper {
333 max-width: 100%;
334 overflow: hidden;
335 text-overflow: ellipsis;
336 white-space: nowrap;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100337 }
338 .input pre {
339 width: 100%;
340 margin-bottom: 0;
341 border-radius: 4px 4px 0 0;
342 box-sizing: border-box;
343 }
344 .result pre {
345 margin-top: 0;
346 color: #555;
347 border-radius: 0 0 4px 4px;
348 width: 100%;
349 box-sizing: border-box;
350 }
351 .result pre.scrollable-on-hover {
352 max-height: 300px;
353 overflow-y: auto;
354 }
355 .tool-call-result-container {
356 width: 100%;
357 position: relative;
358 }
359 .background-badge {
360 display: inline-block;
361 background-color: #6200ea;
362 color: white;
363 font-size: 10px;
364 font-weight: bold;
365 padding: 2px 6px;
366 border-radius: 10px;
367 margin-left: 8px;
368 vertical-align: middle;
369 }
370 .command-wrapper {
371 display: inline-block;
372 max-width: 100%;
373 overflow: hidden;
374 text-overflow: ellipsis;
375 white-space: nowrap;
376 }
377 `,
378 ];
379
380 render() {
381 const inputData = JSON.parse(this.toolCall?.input || "{}");
382 const isBackground = inputData?.background === true;
383 const backgroundIcon = isBackground ? "🔄 " : "";
384
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700385 // Truncate the command if it's too long to display nicely
386 const command = inputData?.command || "";
387 const displayCommand =
388 command.length > 80 ? command.substring(0, 80) + "..." : command;
389
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100390 return html` <sketch-tool-card
391 .open=${this.open}
392 .toolCall=${this.toolCall}
393 >
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700394 <span
395 slot="summary"
396 class="summary-text"
397 style="display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
398 >
399 <div
400 class="command-wrapper"
401 style="max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
402 >
403 ${backgroundIcon}${displayCommand}
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100404 </div>
405 </span>
406 <div slot="input" class="input">
407 <div class="tool-call-result-container">
408 <pre>${backgroundIcon}${inputData?.command}</pre>
409 </div>
410 </div>
411 ${this.toolCall?.result_message?.tool_result
412 ? html`<div slot="result" class="result">
413 <div class="tool-call-result-container">
414 <pre class="tool-call-result">
415${this.toolCall?.result_message.tool_result}</pre
416 >
417 </div>
418 </div>`
419 : ""}
420 </sketch-tool-card>`;
421 }
422}
423
424@customElement("sketch-tool-card-codereview")
425export class SketchToolCardCodeReview extends LitElement {
426 @property() toolCall: ToolCall;
427 @property() open: boolean;
428
429 // Determine the status icon based on the content of the result message
430 getStatusIcon(resultText: string): string {
431 if (!resultText) return "";
432 if (resultText === "OK") return "✔️";
433 if (resultText.includes("# Errors")) return "⚠️";
434 if (resultText.includes("# Info")) return "ℹ️";
435 if (resultText.includes("uncommitted changes in repo")) return "🧹";
436 if (resultText.includes("no new commits have been added")) return "🐣";
437 if (resultText.includes("git repo is not clean")) return "🧼";
438 return "❓";
439 }
440
441 render() {
442 const resultText = this.toolCall?.result_message?.tool_result || "";
443 const statusIcon = this.getStatusIcon(resultText);
444
445 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
446 <span slot="summary" class="summary-text">${statusIcon}</span>
447 <div slot="result"><pre>${resultText}</pre></div>
448 </sketch-tool-card>`;
449 }
450}
451
452@customElement("sketch-tool-card-done")
453export class SketchToolCardDone extends LitElement {
454 @property() toolCall: ToolCall;
455 @property() open: boolean;
456
457 render() {
458 const doneInput = JSON.parse(this.toolCall.input);
459 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
460 <span slot="summary" class="summary-text"></span>
461 <div slot="result">
462 ${Object.keys(doneInput.checklist_items).map((key) => {
463 const item = doneInput.checklist_items[key];
Josh Bleecher Snyderfbbf83b2025-05-15 10:55:55 -0700464 let statusIcon = "〰️";
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100465 if (item.status == "yes") {
Josh Bleecher Snyderfbbf83b2025-05-15 10:55:55 -0700466 statusIcon = "✅";
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100467 } else if (item.status == "not applicable") {
Josh Bleecher Snyderfbbf83b2025-05-15 10:55:55 -0700468 statusIcon = "🤷";
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100469 }
470 return html`<div>
471 <span>${statusIcon}</span> ${key}:${item.status}
472 </div>`;
473 })}
474 </div>
475 </sketch-tool-card>`;
476 }
477}
478
479@customElement("sketch-tool-card-patch")
480export class SketchToolCardPatch extends LitElement {
481 @property() toolCall: ToolCall;
482 @property() open: boolean;
483
484 static styles = css`
485 .summary-text {
486 color: #555;
487 font-family: monospace;
488 overflow: hidden;
489 text-overflow: ellipsis;
490 white-space: nowrap;
491 border-radius: 3px;
492 }
493 `;
494
495 render() {
496 const patchInput = JSON.parse(this.toolCall?.input);
497 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
498 <span slot="summary" class="summary-text">
499 ${patchInput?.path}: ${patchInput.patches.length}
500 edit${patchInput.patches.length > 1 ? "s" : ""}
501 </span>
502 <div slot="input">
503 ${patchInput.patches.map((patch) => {
504 return html`Patch operation: <b>${patch.operation}</b>
505 <pre>${patch.newText}</pre>`;
506 })}
507 </div>
508 <div slot="result">
509 <pre>${this.toolCall?.result_message?.tool_result}</pre>
510 </div>
511 </sketch-tool-card>`;
512 }
513}
514
515@customElement("sketch-tool-card-think")
516export class SketchToolCardThink extends LitElement {
517 @property() toolCall: ToolCall;
518 @property() open: boolean;
519
520 static styles = css`
521 .thought-bubble {
522 overflow-x: auto;
523 margin-bottom: 3px;
524 font-family: monospace;
525 padding: 3px 5px;
526 background: rgb(236, 236, 236);
527 border-radius: 6px;
528 user-select: text;
529 cursor: text;
530 -webkit-user-select: text;
531 -moz-user-select: text;
532 -ms-user-select: text;
533 font-size: 13px;
534 line-height: 1.3;
535 }
536 .summary-text {
537 overflow: hidden;
538 text-overflow: ellipsis;
539 font-family: monospace;
540 }
541 `;
542
543 render() {
544 return html`
545 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
546 <span slot="summary" class="summary-text">
547 ${JSON.parse(this.toolCall?.input)?.thoughts?.split("\n")[0]}
548 </span>
549 <div slot="input" class="thought-bubble">
550 <div class="markdown-content">
551 ${unsafeHTML(
552 renderMarkdown(JSON.parse(this.toolCall?.input)?.thoughts),
553 )}
554 </div>
555 </div>
556 </sketch-tool-card>
557 `;
558 }
559}
560
561@customElement("sketch-tool-card-title")
562export class SketchToolCardTitle extends LitElement {
563 @property() toolCall: ToolCall;
564 @property() open: boolean;
565
566 static styles = css`
567 .summary-text {
568 font-style: italic;
569 }
570 pre {
571 display: inline;
572 font-family: monospace;
573 background: rgb(236, 236, 236);
574 padding: 2px 4px;
575 border-radius: 2px;
576 margin: 0;
577 }
578 `;
579
580 render() {
581 const inputData = JSON.parse(this.toolCall?.input || "{}");
582 return html`
583 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
584 <span slot="summary" class="summary-text">
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000585 Title: "${inputData.title}"
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100586 </span>
587 <div slot="input">
588 <div>Set title to: <b>${inputData.title}</b></div>
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000589 </div>
590 </sketch-tool-card>
591 `;
592 }
593}
594
595@customElement("sketch-tool-card-precommit")
596export class SketchToolCardPrecommit extends LitElement {
597 @property()
598 toolCall: ToolCall;
599
600 @property()
601 open: boolean;
602
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000603 @property()
604 state: State;
605
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000606 static styles = css`
607 .summary-text {
608 font-style: italic;
609 }
610 pre {
611 display: inline;
612 font-family: monospace;
613 background: rgb(236, 236, 236);
614 padding: 2px 4px;
615 border-radius: 2px;
616 margin: 0;
617 }
618 `;
619 constructor() {
620 super();
621 }
622
623 connectedCallback() {
624 super.connectedCallback();
625 }
626
627 disconnectedCallback() {
628 super.disconnectedCallback();
629 }
630
631 render() {
632 const inputData = JSON.parse(this.toolCall?.input || "{}");
633 return html`
634 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
635 <span slot="summary" class="summary-text">
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000636 Branch: ${this.state?.branch_prefix}${inputData.branch_name}
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000637 </span>
638 <div slot="input">
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000639 <div>Set branch to: <code>${this.state?.branch_prefix}${inputData.branch_name}</code></div>
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100640 </div>
641 </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;
955 "sketch-tool-card-title": SketchToolCardTitle;
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000956 "sketch-tool-card-precommit": SketchToolCardPrecommit;
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}