blob: 10fc6c0b7c4e3b5e300104f95161911605c10b9a [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";
Pokey Rule7ac5ed02025-05-07 15:26:10 +01004import { ToolCall, MultipleChoiceOption, MultipleChoiceParams } 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
603 static styles = css`
604 .summary-text {
605 font-style: italic;
606 }
607 pre {
608 display: inline;
609 font-family: monospace;
610 background: rgb(236, 236, 236);
611 padding: 2px 4px;
612 border-radius: 2px;
613 margin: 0;
614 }
615 `;
616 constructor() {
617 super();
618 }
619
620 connectedCallback() {
621 super.connectedCallback();
622 }
623
624 disconnectedCallback() {
625 super.disconnectedCallback();
626 }
627
628 render() {
629 const inputData = JSON.parse(this.toolCall?.input || "{}");
630 return html`
631 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
632 <span slot="summary" class="summary-text">
633 Branch: sketch/${inputData.branch_name}
634 </span>
635 <div slot="input">
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100636 <div>Set branch to: <code>sketch/${inputData.branch_name}</code></div>
637 </div>
638 </sketch-tool-card>
639 `;
640 }
641}
642
643@customElement("sketch-tool-card-multiple-choice")
644export class SketchToolCardMultipleChoice extends LitElement {
645 @property() toolCall: ToolCall;
646 @property() open: boolean;
647 @property() selectedOption: MultipleChoiceOption = null;
648
649 static styles = css`
650 .options-container {
651 display: flex;
652 flex-direction: row;
653 flex-wrap: wrap;
654 gap: 8px;
655 margin: 10px 0;
656 }
657 .option {
658 display: inline-flex;
659 align-items: center;
660 padding: 8px 12px;
661 border-radius: 4px;
662 background-color: #f5f5f5;
663 cursor: pointer;
664 transition: all 0.2s;
665 border: 1px solid transparent;
666 user-select: none;
667 }
668 .option:hover {
669 background-color: #e0e0e0;
670 border-color: #ccc;
671 transform: translateY(-1px);
672 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
673 }
674 .option:active {
675 transform: translateY(0);
676 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
677 background-color: #d5d5d5;
678 }
679 .option.selected {
680 background-color: #e3f2fd;
681 border-color: #2196f3;
682 border-width: 1px;
683 border-style: solid;
684 }
685 .option-checkmark {
686 margin-left: 6px;
687 color: #2196f3;
688 }
689 .summary-text {
690 font-style: italic;
691 padding: 0.5em;
692 }
693 .summary-text strong {
694 font-style: normal;
695 color: #2196f3;
696 font-weight: 600;
697 }
698 `;
699
700 connectedCallback() {
701 super.connectedCallback();
702 this.updateSelectedOption();
703 }
704
705 updated(changedProps) {
706 if (changedProps.has("toolCall")) {
707 this.updateSelectedOption();
708 }
709 }
710
711 updateSelectedOption() {
712 if (this.toolCall?.result_message?.tool_result) {
713 try {
714 this.selectedOption = JSON.parse(
715 this.toolCall.result_message.tool_result,
716 ).selected;
717 } catch (e) {
718 console.error("Error parsing result:", e);
719 }
720 } else {
721 this.selectedOption = null;
722 }
723 }
724
725 async handleOptionClick(choice) {
726 this.selectedOption = this.selectedOption === choice ? null : choice;
727
728 const event = new CustomEvent("multiple-choice-selected", {
729 detail: {
730 responseText: this.selectedOption.responseText,
731 toolCall: this.toolCall,
732 },
733 bubbles: true,
734 composed: true,
735 });
736 this.dispatchEvent(event);
737 }
738
739 render() {
740 let choices = [];
741 let question = "";
742 try {
743 const inputData = JSON.parse(
744 this.toolCall?.input || "{}",
745 ) as MultipleChoiceParams;
746 choices = inputData.responseOptions || [];
747 question = inputData.question || "Please select an option:";
748 } catch (e) {
749 console.error("Error parsing multiple-choice input:", e);
750 }
751
752 const summaryContent =
753 this.selectedOption !== null
754 ? html`<span class="summary-text">
755 ${question}: <strong>${this.selectedOption.caption}</strong>
756 </span>`
757 : html`<span class="summary-text">${question}</span>`;
758
759 return html`
760 <div class="multiple-choice-card">
761 ${summaryContent}
762 <div class="options-container">
763 ${choices.map((choice) => {
764 const isSelected =
765 this.selectedOption !== null && this.selectedOption === choice;
766 return html`
767 <div
768 class="option ${isSelected ? "selected" : ""}"
769 @click=${() => this.handleOptionClick(choice)}
770 title="${choice.responseText}"
771 >
772 <span class="option-label">${choice.caption}</span>
773 ${isSelected
774 ? html`<span class="option-checkmark">✓</span>`
775 : ""}
776 </div>
777 `;
778 })}
779 </div>
780 </div>
781 `;
782 }
783}
784
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700785@customElement("sketch-tool-card-todo-write")
786export class SketchToolCardTodoWrite extends LitElement {
787 @property() toolCall: ToolCall;
788 @property() open: boolean;
789
790 static styles = css`
791 .summary-text {
792 font-style: italic;
793 color: #666;
794 }
795 `;
796
797 render() {
798 const inputData = JSON.parse(this.toolCall?.input || "{}");
799 const tasks = inputData.tasks || [];
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000800
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700801 // Generate circles based on task status
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000802 const circles = tasks
803 .map((task) => {
804 switch (task.status) {
805 case "completed":
806 return "●"; // full circle
807 case "in-progress":
808 return "◐"; // half circle
809 case "queued":
810 default:
811 return "○"; // empty circle
812 }
813 })
814 .join(" ");
815
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700816 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000817 <span slot="summary" class="summary-text"> ${circles} </span>
818 <div slot="result">
819 <pre>${this.toolCall?.result_message?.tool_result}</pre>
820 </div>
821 </sketch-tool-card>`;
822 }
823}
824
825@customElement("sketch-tool-card-keyword-search")
826export class SketchToolCardKeywordSearch extends LitElement {
827 @property() toolCall: ToolCall;
828 @property() open: boolean;
829
830 static styles = css`
831 .summary-container {
832 display: flex;
833 flex-direction: column;
834 gap: 2px;
835 width: 100%;
836 max-width: 100%;
837 overflow: hidden;
838 }
839 .query-line {
840 color: #333;
841 font-family: inherit;
842 font-size: 12px;
843 font-weight: normal;
844 white-space: normal;
845 word-wrap: break-word;
846 word-break: break-word;
847 overflow-wrap: break-word;
848 line-height: 1.2;
849 }
850 .keywords-line {
851 color: #666;
852 font-family: inherit;
853 font-size: 11px;
854 font-weight: normal;
855 white-space: normal;
856 word-wrap: break-word;
857 word-break: break-word;
858 overflow-wrap: break-word;
859 line-height: 1.2;
860 margin-top: 1px;
861 }
862 `;
863
864 render() {
865 const inputData = JSON.parse(this.toolCall?.input || "{}");
866 const query = inputData.query || "";
867 const searchTerms = inputData.search_terms || [];
868
869 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
870 <div slot="summary" class="summary-container">
871 <div class="query-line">🔍 ${query}</div>
872 <div class="keywords-line">🗝️ ${searchTerms.join(", ")}</div>
873 </div>
874 <div slot="input">
875 <div><strong>Query:</strong> ${query}</div>
876 <div><strong>Search terms:</strong> ${searchTerms.join(", ")}</div>
877 </div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700878 <div slot="result">
879 <pre>${this.toolCall?.result_message?.tool_result}</pre>
880 </div>
881 </sketch-tool-card>`;
882 }
883}
884
885@customElement("sketch-tool-card-todo-read")
886export class SketchToolCardTodoRead extends LitElement {
887 @property() toolCall: ToolCall;
888 @property() open: boolean;
889
890 static styles = css`
891 .summary-text {
892 font-style: italic;
893 color: #666;
894 }
895 `;
896
897 render() {
898 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000899 <span slot="summary" class="summary-text"> Read todo list </span>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700900 <div slot="result">
901 <pre>${this.toolCall?.result_message?.tool_result}</pre>
902 </div>
903 </sketch-tool-card>`;
904 }
905}
906
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100907@customElement("sketch-tool-card-generic")
908export class SketchToolCardGeneric extends LitElement {
909 @property() toolCall: ToolCall;
910 @property() open: boolean;
911
912 render() {
913 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700914 <span
915 slot="summary"
916 style="display: block; white-space: normal; word-break: break-word; overflow-wrap: break-word; max-width: 100%; width: 100%;"
917 >${this.toolCall?.input}</span
918 >
919 <div
920 slot="input"
921 style="max-width: 100%; overflow-wrap: break-word; word-break: break-word;"
922 >
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100923 Input:
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700924 <pre
925 style="max-width: 100%; white-space: pre-wrap; overflow-wrap: break-word; word-break: break-word;"
926 >
927${this.toolCall?.input}</pre
928 >
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100929 </div>
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700930 <div
931 slot="result"
932 style="max-width: 100%; overflow-wrap: break-word; word-break: break-word;"
933 >
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100934 Result:
935 ${this.toolCall?.result_message?.tool_result
936 ? html`<pre>${this.toolCall?.result_message.tool_result}</pre>`
937 : ""}
938 </div>
939 </sketch-tool-card>`;
940 }
941}
942
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700943declare global {
944 interface HTMLElementTagNameMap {
945 "sketch-tool-card": SketchToolCard;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100946 "sketch-tool-card-generic": SketchToolCardGeneric;
947 "sketch-tool-card-bash": SketchToolCardBash;
948 "sketch-tool-card-codereview": SketchToolCardCodeReview;
949 "sketch-tool-card-done": SketchToolCardDone;
950 "sketch-tool-card-patch": SketchToolCardPatch;
951 "sketch-tool-card-think": SketchToolCardThink;
952 "sketch-tool-card-title": SketchToolCardTitle;
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000953 "sketch-tool-card-precommit": SketchToolCardPrecommit;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100954 "sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700955 "sketch-tool-card-todo-write": SketchToolCardTodoWrite;
956 "sketch-tool-card-todo-read": SketchToolCardTodoRead;
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000957 "sketch-tool-card-keyword-search": SketchToolCardKeywordSearch;
Philip Zeyliger84a8ae62025-05-13 16:36:01 -0700958 // TODO: We haven't implemented this for browser tools.
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700959 }
960}