blob: cc974055c548bf097a02ab84b0591348bd2f5264 [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";
5import { marked, MarkedOptions } from "marked";
6
7// Shared utility function for markdown rendering
8function renderMarkdown(markdownContent: string): string {
9 try {
10 return marked.parse(markdownContent, {
11 gfm: true,
12 breaks: true,
13 async: false,
14 }) as string;
15 } catch (error) {
16 console.error("Error rendering markdown:", error);
17 return markdownContent;
18 }
19}
20
21// Common styles shared across all tool cards
22const commonStyles = css`
Philip Zeyligere31d2a92025-05-11 15:22:35 -070023 :host {
24 display: block;
25 max-width: 100%;
26 width: 100%;
27 box-sizing: border-box;
28 overflow: hidden;
29 }
Pokey Rule7ac5ed02025-05-07 15:26:10 +010030 pre {
31 background: rgb(236, 236, 236);
32 color: black;
33 padding: 0.5em;
34 border-radius: 4px;
35 white-space: pre-wrap;
36 word-break: break-word;
37 max-width: 100%;
38 width: 100%;
39 box-sizing: border-box;
40 overflow-wrap: break-word;
41 }
42 .summary-text {
Philip Zeyligere31d2a92025-05-11 15:22:35 -070043 overflow: hidden !important;
44 text-overflow: ellipsis !important;
45 white-space: nowrap !important;
46 max-width: 100% !important;
47 width: 100% !important;
Pokey Rule7ac5ed02025-05-07 15:26:10 +010048 font-family: monospace;
Philip Zeyligere31d2a92025-05-11 15:22:35 -070049 display: block;
Pokey Rule7ac5ed02025-05-07 15:26:10 +010050 }
51`;
Pokey Rule5e8aead2025-05-06 16:21:57 +010052
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070053@customElement("sketch-tool-card")
54export class SketchToolCard extends LitElement {
Pokey Rule5e8aead2025-05-06 16:21:57 +010055 @property() toolCall: ToolCall;
56 @property() open: boolean;
57 @state() detailsVisible: boolean = false;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000058
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070059 static styles = css`
60 .tool-call {
61 display: flex;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000062 flex-direction: column;
63 width: 100%;
64 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000065 .tool-row {
66 display: flex;
67 width: 100%;
68 box-sizing: border-box;
69 padding: 6px 8px 6px 12px;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070070 align-items: center;
Pokey Rule5e8aead2025-05-06 16:21:57 +010071 gap: 8px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000072 cursor: pointer;
73 border-radius: 4px;
74 position: relative;
Pokey Rule5e8aead2025-05-06 16:21:57 +010075 overflow: hidden;
Philip Zeyligere31d2a92025-05-11 15:22:35 -070076 flex-wrap: wrap;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000077 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000078 .tool-row:hover {
79 background-color: rgba(0, 0, 0, 0.02);
80 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000081 .tool-name {
82 font-family: monospace;
83 font-weight: 500;
84 color: #444;
85 background-color: rgba(0, 0, 0, 0.05);
86 border-radius: 3px;
87 padding: 2px 6px;
88 flex-shrink: 0;
89 min-width: 45px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000090 font-size: 12px;
91 text-align: center;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070092 white-space: nowrap;
93 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000094 .tool-success {
95 color: #5cb85c;
96 font-size: 14px;
97 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000098 .tool-error {
Josh Bleecher Snydere750ec92025-05-05 23:01:57 +000099 color: #6c757d;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000100 font-size: 14px;
101 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000102 .tool-pending {
103 color: #f0ad4e;
104 font-size: 14px;
105 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000106 .summary-text {
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700107 white-space: normal;
108 overflow-wrap: break-word;
109 word-break: break-word;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000110 flex-grow: 1;
111 flex-shrink: 1;
112 color: #444;
113 font-family: monospace;
114 font-size: 12px;
115 padding: 0 4px;
116 min-width: 50px;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700117 max-width: calc(100% - 150px);
Pokey Rule5e8aead2025-05-06 16:21:57 +0100118 display: inline-block;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000119 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000120 .tool-status {
121 display: flex;
122 align-items: center;
123 gap: 12px;
124 margin-left: auto;
125 flex-shrink: 0;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100126 min-width: 120px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000127 justify-content: flex-end;
128 padding-right: 8px;
129 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700130 .tool-call-status {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000131 display: flex;
132 align-items: center;
133 justify-content: center;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700134 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700135 .tool-call-status.spinner {
136 animation: spin 1s infinite linear;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700137 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700138 @keyframes spin {
139 0% {
140 transform: rotate(0deg);
141 }
142 100% {
143 transform: rotate(360deg);
144 }
145 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000146 .elapsed {
147 font-size: 11px;
148 color: #777;
149 white-space: nowrap;
150 min-width: 40px;
151 text-align: right;
152 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000153 .tool-details {
154 padding: 8px;
155 background-color: rgba(0, 0, 0, 0.02);
156 margin-top: 1px;
157 border-top: 1px solid rgba(0, 0, 0, 0.05);
158 display: none;
159 font-family: monospace;
160 font-size: 12px;
161 color: #333;
162 border-radius: 0 0 4px 4px;
163 max-width: 100%;
164 width: 100%;
165 box-sizing: border-box;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100166 overflow: hidden;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000167 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000168 .tool-details.visible {
169 display: block;
170 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700171 .cancel-button {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700172 cursor: pointer;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000173 color: white;
174 background-color: #d9534f;
175 border: none;
176 border-radius: 3px;
177 font-size: 11px;
178 padding: 2px 6px;
179 white-space: nowrap;
180 min-width: 50px;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700181 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700182 .cancel-button:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000183 background-color: #c9302c;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700184 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000185 .cancel-button[disabled] {
186 background-color: #999;
187 cursor: not-allowed;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700188 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700189 `;
190
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700191 _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700192 button.innerText = "Cancelling";
193 button.disabled = true;
194 try {
195 const response = await fetch("cancel", {
196 method: "POST",
Pokey Rule5e8aead2025-05-06 16:21:57 +0100197 headers: { "Content-Type": "application/json" },
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700198 body: JSON.stringify({
199 tool_call_id: tool_call_id,
200 reason: "user requested cancellation",
201 }),
202 });
203 if (response.ok) {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700204 button.parentElement.removeChild(button);
205 } else {
206 button.innerText = "Cancel";
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700207 }
208 } catch (e) {
209 console.error("cancel", tool_call_id, e);
210 }
211 };
212
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000213 _toggleDetails(e: Event) {
214 e.stopPropagation();
215 this.detailsVisible = !this.detailsVisible;
216 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700217
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000218 render() {
Pokey Rule5e8aead2025-05-06 16:21:57 +0100219 // Status indicator based on result
220 let statusIcon = html`<span class="tool-call-status spinner tool-pending"
221 >⏳</span
222 >`;
223 if (this.toolCall?.result_message) {
224 statusIcon = this.toolCall?.result_message.tool_error
Josh Bleecher Snyderc3c20232025-05-07 05:46:04 -0700225 ? html`<span class="tool-call-status tool-error">〰️</span>`
Pokey Rule5e8aead2025-05-06 16:21:57 +0100226 : html`<span class="tool-call-status tool-success">✓</span>`;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000227 }
228
229 // Cancel button for pending operations
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700230 const cancelButton = this.toolCall?.result_message
231 ? ""
232 : html`<button
233 class="cancel-button"
234 title="Cancel this operation"
235 @click=${(e: Event) => {
236 e.stopPropagation();
Pokey Rule5e8aead2025-05-06 16:21:57 +0100237 this._cancelToolCall(
238 this.toolCall?.tool_call_id,
239 e.target as HTMLButtonElement,
240 );
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700241 }}
242 >
243 Cancel
244 </button>`;
245
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000246 // Elapsed time display
247 const elapsed = this.toolCall?.result_message?.elapsed
Sean McCullough2deac842025-04-21 18:17:57 -0700248 ? html`<span class="elapsed"
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000249 >${(this.toolCall?.result_message?.elapsed / 1e9).toFixed(1)}s</span
Sean McCullough2deac842025-04-21 18:17:57 -0700250 >`
Pokey Rule5e8aead2025-05-06 16:21:57 +0100251 : html`<span class="elapsed"></span>`;
Sean McCullough2deac842025-04-21 18:17:57 -0700252
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000253 // Initialize details visibility based on open property
254 if (this.open && !this.detailsVisible) {
255 this.detailsVisible = true;
256 }
257
258 return html`<div class="tool-call">
259 <div class="tool-row" @click=${this._toggleDetails}>
260 <span class="tool-name">${this.toolCall?.name}</span>
261 <span class="summary-text"><slot name="summary"></slot></span>
262 <div class="tool-status">${statusIcon} ${elapsed} ${cancelButton}</div>
263 </div>
264 <div class="tool-details ${this.detailsVisible ? "visible" : ""}">
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700265 <slot name="input"></slot>
266 <slot name="result"></slot>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000267 </div>
268 </div>`;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700269 }
270}
271
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100272@customElement("sketch-tool-card-bash")
273export class SketchToolCardBash extends LitElement {
274 @property() toolCall: ToolCall;
275 @property() open: boolean;
276
277 static styles = [
278 commonStyles,
279 css`
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700280 :host {
281 max-width: 100%;
282 display: block;
283 }
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100284 .input {
285 display: flex;
286 width: 100%;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700287 max-width: 100%;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100288 flex-direction: column;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700289 overflow-wrap: break-word;
290 word-break: break-word;
291 }
292 .command-wrapper {
293 max-width: 100%;
294 overflow: hidden;
295 text-overflow: ellipsis;
296 white-space: nowrap;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100297 }
298 .input pre {
299 width: 100%;
300 margin-bottom: 0;
301 border-radius: 4px 4px 0 0;
302 box-sizing: border-box;
303 }
304 .result pre {
305 margin-top: 0;
306 color: #555;
307 border-radius: 0 0 4px 4px;
308 width: 100%;
309 box-sizing: border-box;
310 }
311 .result pre.scrollable-on-hover {
312 max-height: 300px;
313 overflow-y: auto;
314 }
315 .tool-call-result-container {
316 width: 100%;
317 position: relative;
318 }
319 .background-badge {
320 display: inline-block;
321 background-color: #6200ea;
322 color: white;
323 font-size: 10px;
324 font-weight: bold;
325 padding: 2px 6px;
326 border-radius: 10px;
327 margin-left: 8px;
328 vertical-align: middle;
329 }
330 .command-wrapper {
331 display: inline-block;
332 max-width: 100%;
333 overflow: hidden;
334 text-overflow: ellipsis;
335 white-space: nowrap;
336 }
337 `,
338 ];
339
340 render() {
341 const inputData = JSON.parse(this.toolCall?.input || "{}");
342 const isBackground = inputData?.background === true;
343 const backgroundIcon = isBackground ? "🔄 " : "";
344
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700345 // Truncate the command if it's too long to display nicely
346 const command = inputData?.command || "";
347 const displayCommand =
348 command.length > 80 ? command.substring(0, 80) + "..." : command;
349
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100350 return html` <sketch-tool-card
351 .open=${this.open}
352 .toolCall=${this.toolCall}
353 >
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700354 <span
355 slot="summary"
356 class="summary-text"
357 style="display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
358 >
359 <div
360 class="command-wrapper"
361 style="max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
362 >
363 ${backgroundIcon}${displayCommand}
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100364 </div>
365 </span>
366 <div slot="input" class="input">
367 <div class="tool-call-result-container">
368 <pre>${backgroundIcon}${inputData?.command}</pre>
369 </div>
370 </div>
371 ${this.toolCall?.result_message?.tool_result
372 ? html`<div slot="result" class="result">
373 <div class="tool-call-result-container">
374 <pre class="tool-call-result">
375${this.toolCall?.result_message.tool_result}</pre
376 >
377 </div>
378 </div>`
379 : ""}
380 </sketch-tool-card>`;
381 }
382}
383
384@customElement("sketch-tool-card-codereview")
385export class SketchToolCardCodeReview extends LitElement {
386 @property() toolCall: ToolCall;
387 @property() open: boolean;
388
389 // Determine the status icon based on the content of the result message
390 getStatusIcon(resultText: string): string {
391 if (!resultText) return "";
392 if (resultText === "OK") return "✔️";
393 if (resultText.includes("# Errors")) return "⚠️";
394 if (resultText.includes("# Info")) return "ℹ️";
395 if (resultText.includes("uncommitted changes in repo")) return "🧹";
396 if (resultText.includes("no new commits have been added")) return "🐣";
397 if (resultText.includes("git repo is not clean")) return "🧼";
398 return "❓";
399 }
400
401 render() {
402 const resultText = this.toolCall?.result_message?.tool_result || "";
403 const statusIcon = this.getStatusIcon(resultText);
404
405 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
406 <span slot="summary" class="summary-text">${statusIcon}</span>
407 <div slot="result"><pre>${resultText}</pre></div>
408 </sketch-tool-card>`;
409 }
410}
411
412@customElement("sketch-tool-card-done")
413export class SketchToolCardDone extends LitElement {
414 @property() toolCall: ToolCall;
415 @property() open: boolean;
416
417 render() {
418 const doneInput = JSON.parse(this.toolCall.input);
419 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
420 <span slot="summary" class="summary-text"></span>
421 <div slot="result">
422 ${Object.keys(doneInput.checklist_items).map((key) => {
423 const item = doneInput.checklist_items[key];
424 let statusIcon = "⛔";
425 if (item.status == "yes") {
426 statusIcon = "👍";
427 } else if (item.status == "not applicable") {
428 statusIcon = "🤷‍♂️";
429 }
430 return html`<div>
431 <span>${statusIcon}</span> ${key}:${item.status}
432 </div>`;
433 })}
434 </div>
435 </sketch-tool-card>`;
436 }
437}
438
439@customElement("sketch-tool-card-patch")
440export class SketchToolCardPatch extends LitElement {
441 @property() toolCall: ToolCall;
442 @property() open: boolean;
443
444 static styles = css`
445 .summary-text {
446 color: #555;
447 font-family: monospace;
448 overflow: hidden;
449 text-overflow: ellipsis;
450 white-space: nowrap;
451 border-radius: 3px;
452 }
453 `;
454
455 render() {
456 const patchInput = JSON.parse(this.toolCall?.input);
457 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
458 <span slot="summary" class="summary-text">
459 ${patchInput?.path}: ${patchInput.patches.length}
460 edit${patchInput.patches.length > 1 ? "s" : ""}
461 </span>
462 <div slot="input">
463 ${patchInput.patches.map((patch) => {
464 return html`Patch operation: <b>${patch.operation}</b>
465 <pre>${patch.newText}</pre>`;
466 })}
467 </div>
468 <div slot="result">
469 <pre>${this.toolCall?.result_message?.tool_result}</pre>
470 </div>
471 </sketch-tool-card>`;
472 }
473}
474
475@customElement("sketch-tool-card-think")
476export class SketchToolCardThink extends LitElement {
477 @property() toolCall: ToolCall;
478 @property() open: boolean;
479
480 static styles = css`
481 .thought-bubble {
482 overflow-x: auto;
483 margin-bottom: 3px;
484 font-family: monospace;
485 padding: 3px 5px;
486 background: rgb(236, 236, 236);
487 border-radius: 6px;
488 user-select: text;
489 cursor: text;
490 -webkit-user-select: text;
491 -moz-user-select: text;
492 -ms-user-select: text;
493 font-size: 13px;
494 line-height: 1.3;
495 }
496 .summary-text {
497 overflow: hidden;
498 text-overflow: ellipsis;
499 font-family: monospace;
500 }
501 `;
502
503 render() {
504 return html`
505 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
506 <span slot="summary" class="summary-text">
507 ${JSON.parse(this.toolCall?.input)?.thoughts?.split("\n")[0]}
508 </span>
509 <div slot="input" class="thought-bubble">
510 <div class="markdown-content">
511 ${unsafeHTML(
512 renderMarkdown(JSON.parse(this.toolCall?.input)?.thoughts),
513 )}
514 </div>
515 </div>
516 </sketch-tool-card>
517 `;
518 }
519}
520
521@customElement("sketch-tool-card-title")
522export class SketchToolCardTitle extends LitElement {
523 @property() toolCall: ToolCall;
524 @property() open: boolean;
525
526 static styles = css`
527 .summary-text {
528 font-style: italic;
529 }
530 pre {
531 display: inline;
532 font-family: monospace;
533 background: rgb(236, 236, 236);
534 padding: 2px 4px;
535 border-radius: 2px;
536 margin: 0;
537 }
538 `;
539
540 render() {
541 const inputData = JSON.parse(this.toolCall?.input || "{}");
542 return html`
543 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
544 <span slot="summary" class="summary-text">
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000545 Title: "${inputData.title}"
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100546 </span>
547 <div slot="input">
548 <div>Set title to: <b>${inputData.title}</b></div>
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000549 </div>
550 </sketch-tool-card>
551 `;
552 }
553}
554
555@customElement("sketch-tool-card-precommit")
556export class SketchToolCardPrecommit extends LitElement {
557 @property()
558 toolCall: ToolCall;
559
560 @property()
561 open: boolean;
562
563 static styles = css`
564 .summary-text {
565 font-style: italic;
566 }
567 pre {
568 display: inline;
569 font-family: monospace;
570 background: rgb(236, 236, 236);
571 padding: 2px 4px;
572 border-radius: 2px;
573 margin: 0;
574 }
575 `;
576 constructor() {
577 super();
578 }
579
580 connectedCallback() {
581 super.connectedCallback();
582 }
583
584 disconnectedCallback() {
585 super.disconnectedCallback();
586 }
587
588 render() {
589 const inputData = JSON.parse(this.toolCall?.input || "{}");
590 return html`
591 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
592 <span slot="summary" class="summary-text">
593 Branch: sketch/${inputData.branch_name}
594 </span>
595 <div slot="input">
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100596 <div>Set branch to: <code>sketch/${inputData.branch_name}</code></div>
597 </div>
598 </sketch-tool-card>
599 `;
600 }
601}
602
603@customElement("sketch-tool-card-multiple-choice")
604export class SketchToolCardMultipleChoice extends LitElement {
605 @property() toolCall: ToolCall;
606 @property() open: boolean;
607 @property() selectedOption: MultipleChoiceOption = null;
608
609 static styles = css`
610 .options-container {
611 display: flex;
612 flex-direction: row;
613 flex-wrap: wrap;
614 gap: 8px;
615 margin: 10px 0;
616 }
617 .option {
618 display: inline-flex;
619 align-items: center;
620 padding: 8px 12px;
621 border-radius: 4px;
622 background-color: #f5f5f5;
623 cursor: pointer;
624 transition: all 0.2s;
625 border: 1px solid transparent;
626 user-select: none;
627 }
628 .option:hover {
629 background-color: #e0e0e0;
630 border-color: #ccc;
631 transform: translateY(-1px);
632 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
633 }
634 .option:active {
635 transform: translateY(0);
636 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
637 background-color: #d5d5d5;
638 }
639 .option.selected {
640 background-color: #e3f2fd;
641 border-color: #2196f3;
642 border-width: 1px;
643 border-style: solid;
644 }
645 .option-checkmark {
646 margin-left: 6px;
647 color: #2196f3;
648 }
649 .summary-text {
650 font-style: italic;
651 padding: 0.5em;
652 }
653 .summary-text strong {
654 font-style: normal;
655 color: #2196f3;
656 font-weight: 600;
657 }
658 `;
659
660 connectedCallback() {
661 super.connectedCallback();
662 this.updateSelectedOption();
663 }
664
665 updated(changedProps) {
666 if (changedProps.has("toolCall")) {
667 this.updateSelectedOption();
668 }
669 }
670
671 updateSelectedOption() {
672 if (this.toolCall?.result_message?.tool_result) {
673 try {
674 this.selectedOption = JSON.parse(
675 this.toolCall.result_message.tool_result,
676 ).selected;
677 } catch (e) {
678 console.error("Error parsing result:", e);
679 }
680 } else {
681 this.selectedOption = null;
682 }
683 }
684
685 async handleOptionClick(choice) {
686 this.selectedOption = this.selectedOption === choice ? null : choice;
687
688 const event = new CustomEvent("multiple-choice-selected", {
689 detail: {
690 responseText: this.selectedOption.responseText,
691 toolCall: this.toolCall,
692 },
693 bubbles: true,
694 composed: true,
695 });
696 this.dispatchEvent(event);
697 }
698
699 render() {
700 let choices = [];
701 let question = "";
702 try {
703 const inputData = JSON.parse(
704 this.toolCall?.input || "{}",
705 ) as MultipleChoiceParams;
706 choices = inputData.responseOptions || [];
707 question = inputData.question || "Please select an option:";
708 } catch (e) {
709 console.error("Error parsing multiple-choice input:", e);
710 }
711
712 const summaryContent =
713 this.selectedOption !== null
714 ? html`<span class="summary-text">
715 ${question}: <strong>${this.selectedOption.caption}</strong>
716 </span>`
717 : html`<span class="summary-text">${question}</span>`;
718
719 return html`
720 <div class="multiple-choice-card">
721 ${summaryContent}
722 <div class="options-container">
723 ${choices.map((choice) => {
724 const isSelected =
725 this.selectedOption !== null && this.selectedOption === choice;
726 return html`
727 <div
728 class="option ${isSelected ? "selected" : ""}"
729 @click=${() => this.handleOptionClick(choice)}
730 title="${choice.responseText}"
731 >
732 <span class="option-label">${choice.caption}</span>
733 ${isSelected
734 ? html`<span class="option-checkmark">✓</span>`
735 : ""}
736 </div>
737 `;
738 })}
739 </div>
740 </div>
741 `;
742 }
743}
744
745@customElement("sketch-tool-card-generic")
746export class SketchToolCardGeneric extends LitElement {
747 @property() toolCall: ToolCall;
748 @property() open: boolean;
749
750 render() {
751 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700752 <span
753 slot="summary"
754 style="display: block; white-space: normal; word-break: break-word; overflow-wrap: break-word; max-width: 100%; width: 100%;"
755 >${this.toolCall?.input}</span
756 >
757 <div
758 slot="input"
759 style="max-width: 100%; overflow-wrap: break-word; word-break: break-word;"
760 >
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100761 Input:
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700762 <pre
763 style="max-width: 100%; white-space: pre-wrap; overflow-wrap: break-word; word-break: break-word;"
764 >
765${this.toolCall?.input}</pre
766 >
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100767 </div>
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700768 <div
769 slot="result"
770 style="max-width: 100%; overflow-wrap: break-word; word-break: break-word;"
771 >
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100772 Result:
773 ${this.toolCall?.result_message?.tool_result
774 ? html`<pre>${this.toolCall?.result_message.tool_result}</pre>`
775 : ""}
776 </div>
777 </sketch-tool-card>`;
778 }
779}
780
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700781declare global {
782 interface HTMLElementTagNameMap {
783 "sketch-tool-card": SketchToolCard;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100784 "sketch-tool-card-generic": SketchToolCardGeneric;
785 "sketch-tool-card-bash": SketchToolCardBash;
786 "sketch-tool-card-codereview": SketchToolCardCodeReview;
787 "sketch-tool-card-done": SketchToolCardDone;
788 "sketch-tool-card-patch": SketchToolCardPatch;
789 "sketch-tool-card-think": SketchToolCardThink;
790 "sketch-tool-card-title": SketchToolCardTitle;
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000791 "sketch-tool-card-precommit": SketchToolCardPrecommit;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100792 "sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
Philip Zeyliger84a8ae62025-05-13 16:36:01 -0700793 // TODO: We haven't implemented this for browser tools.
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700794 }
795}