blob: b63e0210facb285bedd36f71f88b99d30beee5a1 [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`
23 pre {
24 background: rgb(236, 236, 236);
25 color: black;
26 padding: 0.5em;
27 border-radius: 4px;
28 white-space: pre-wrap;
29 word-break: break-word;
30 max-width: 100%;
31 width: 100%;
32 box-sizing: border-box;
33 overflow-wrap: break-word;
34 }
35 .summary-text {
36 overflow: hidden;
37 text-overflow: ellipsis;
38 white-space: nowrap;
39 max-width: 100%;
40 font-family: monospace;
41 }
42`;
Pokey Rule5e8aead2025-05-06 16:21:57 +010043
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070044@customElement("sketch-tool-card")
45export class SketchToolCard extends LitElement {
Pokey Rule5e8aead2025-05-06 16:21:57 +010046 @property() toolCall: ToolCall;
47 @property() open: boolean;
48 @state() detailsVisible: boolean = false;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000049
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070050 static styles = css`
51 .tool-call {
52 display: flex;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000053 flex-direction: column;
54 width: 100%;
55 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000056 .tool-row {
57 display: flex;
58 width: 100%;
59 box-sizing: border-box;
60 padding: 6px 8px 6px 12px;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070061 align-items: center;
Pokey Rule5e8aead2025-05-06 16:21:57 +010062 gap: 8px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000063 cursor: pointer;
64 border-radius: 4px;
65 position: relative;
Pokey Rule5e8aead2025-05-06 16:21:57 +010066 overflow: hidden;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000067 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000068 .tool-row:hover {
69 background-color: rgba(0, 0, 0, 0.02);
70 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000071 .tool-name {
72 font-family: monospace;
73 font-weight: 500;
74 color: #444;
75 background-color: rgba(0, 0, 0, 0.05);
76 border-radius: 3px;
77 padding: 2px 6px;
78 flex-shrink: 0;
79 min-width: 45px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000080 font-size: 12px;
81 text-align: center;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070082 white-space: nowrap;
83 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000084 .tool-success {
85 color: #5cb85c;
86 font-size: 14px;
87 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000088 .tool-error {
Josh Bleecher Snydere750ec92025-05-05 23:01:57 +000089 color: #6c757d;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000090 font-size: 14px;
91 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000092 .tool-pending {
93 color: #f0ad4e;
94 font-size: 14px;
95 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000096 .summary-text {
97 white-space: nowrap;
98 text-overflow: ellipsis;
99 overflow: hidden;
100 flex-grow: 1;
101 flex-shrink: 1;
102 color: #444;
103 font-family: monospace;
104 font-size: 12px;
105 padding: 0 4px;
106 min-width: 50px;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100107 max-width: calc(100% - 250px);
108 display: inline-block;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000109 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000110 .tool-status {
111 display: flex;
112 align-items: center;
113 gap: 12px;
114 margin-left: auto;
115 flex-shrink: 0;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100116 min-width: 120px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000117 justify-content: flex-end;
118 padding-right: 8px;
119 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700120 .tool-call-status {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000121 display: flex;
122 align-items: center;
123 justify-content: center;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700124 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700125 .tool-call-status.spinner {
126 animation: spin 1s infinite linear;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700127 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700128 @keyframes spin {
129 0% {
130 transform: rotate(0deg);
131 }
132 100% {
133 transform: rotate(360deg);
134 }
135 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000136 .elapsed {
137 font-size: 11px;
138 color: #777;
139 white-space: nowrap;
140 min-width: 40px;
141 text-align: right;
142 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000143 .tool-details {
144 padding: 8px;
145 background-color: rgba(0, 0, 0, 0.02);
146 margin-top: 1px;
147 border-top: 1px solid rgba(0, 0, 0, 0.05);
148 display: none;
149 font-family: monospace;
150 font-size: 12px;
151 color: #333;
152 border-radius: 0 0 4px 4px;
153 max-width: 100%;
154 width: 100%;
155 box-sizing: border-box;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100156 overflow: hidden;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000157 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000158 .tool-details.visible {
159 display: block;
160 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700161 .cancel-button {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700162 cursor: pointer;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000163 color: white;
164 background-color: #d9534f;
165 border: none;
166 border-radius: 3px;
167 font-size: 11px;
168 padding: 2px 6px;
169 white-space: nowrap;
170 min-width: 50px;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700171 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700172 .cancel-button:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000173 background-color: #c9302c;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700174 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000175 .cancel-button[disabled] {
176 background-color: #999;
177 cursor: not-allowed;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700178 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700179 `;
180
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700181 _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700182 button.innerText = "Cancelling";
183 button.disabled = true;
184 try {
185 const response = await fetch("cancel", {
186 method: "POST",
Pokey Rule5e8aead2025-05-06 16:21:57 +0100187 headers: { "Content-Type": "application/json" },
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700188 body: JSON.stringify({
189 tool_call_id: tool_call_id,
190 reason: "user requested cancellation",
191 }),
192 });
193 if (response.ok) {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700194 button.parentElement.removeChild(button);
195 } else {
196 button.innerText = "Cancel";
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700197 }
198 } catch (e) {
199 console.error("cancel", tool_call_id, e);
200 }
201 };
202
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000203 _toggleDetails(e: Event) {
204 e.stopPropagation();
205 this.detailsVisible = !this.detailsVisible;
206 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700207
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000208 render() {
Pokey Rule5e8aead2025-05-06 16:21:57 +0100209 // Status indicator based on result
210 let statusIcon = html`<span class="tool-call-status spinner tool-pending"
211 >⏳</span
212 >`;
213 if (this.toolCall?.result_message) {
214 statusIcon = this.toolCall?.result_message.tool_error
Josh Bleecher Snyderc3c20232025-05-07 05:46:04 -0700215 ? html`<span class="tool-call-status tool-error">〰️</span>`
Pokey Rule5e8aead2025-05-06 16:21:57 +0100216 : html`<span class="tool-call-status tool-success">✓</span>`;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000217 }
218
219 // Cancel button for pending operations
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700220 const cancelButton = this.toolCall?.result_message
221 ? ""
222 : html`<button
223 class="cancel-button"
224 title="Cancel this operation"
225 @click=${(e: Event) => {
226 e.stopPropagation();
Pokey Rule5e8aead2025-05-06 16:21:57 +0100227 this._cancelToolCall(
228 this.toolCall?.tool_call_id,
229 e.target as HTMLButtonElement,
230 );
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700231 }}
232 >
233 Cancel
234 </button>`;
235
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000236 // Elapsed time display
237 const elapsed = this.toolCall?.result_message?.elapsed
Sean McCullough2deac842025-04-21 18:17:57 -0700238 ? html`<span class="elapsed"
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000239 >${(this.toolCall?.result_message?.elapsed / 1e9).toFixed(1)}s</span
Sean McCullough2deac842025-04-21 18:17:57 -0700240 >`
Pokey Rule5e8aead2025-05-06 16:21:57 +0100241 : html`<span class="elapsed"></span>`;
Sean McCullough2deac842025-04-21 18:17:57 -0700242
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000243 // Initialize details visibility based on open property
244 if (this.open && !this.detailsVisible) {
245 this.detailsVisible = true;
246 }
247
248 return html`<div class="tool-call">
249 <div class="tool-row" @click=${this._toggleDetails}>
250 <span class="tool-name">${this.toolCall?.name}</span>
251 <span class="summary-text"><slot name="summary"></slot></span>
252 <div class="tool-status">${statusIcon} ${elapsed} ${cancelButton}</div>
253 </div>
254 <div class="tool-details ${this.detailsVisible ? "visible" : ""}">
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700255 <slot name="input"></slot>
256 <slot name="result"></slot>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000257 </div>
258 </div>`;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700259 }
260}
261
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100262@customElement("sketch-tool-card-bash")
263export class SketchToolCardBash extends LitElement {
264 @property() toolCall: ToolCall;
265 @property() open: boolean;
266
267 static styles = [
268 commonStyles,
269 css`
270 .input {
271 display: flex;
272 width: 100%;
273 flex-direction: column;
274 }
275 .input pre {
276 width: 100%;
277 margin-bottom: 0;
278 border-radius: 4px 4px 0 0;
279 box-sizing: border-box;
280 }
281 .result pre {
282 margin-top: 0;
283 color: #555;
284 border-radius: 0 0 4px 4px;
285 width: 100%;
286 box-sizing: border-box;
287 }
288 .result pre.scrollable-on-hover {
289 max-height: 300px;
290 overflow-y: auto;
291 }
292 .tool-call-result-container {
293 width: 100%;
294 position: relative;
295 }
296 .background-badge {
297 display: inline-block;
298 background-color: #6200ea;
299 color: white;
300 font-size: 10px;
301 font-weight: bold;
302 padding: 2px 6px;
303 border-radius: 10px;
304 margin-left: 8px;
305 vertical-align: middle;
306 }
307 .command-wrapper {
308 display: inline-block;
309 max-width: 100%;
310 overflow: hidden;
311 text-overflow: ellipsis;
312 white-space: nowrap;
313 }
314 `,
315 ];
316
317 render() {
318 const inputData = JSON.parse(this.toolCall?.input || "{}");
319 const isBackground = inputData?.background === true;
320 const backgroundIcon = isBackground ? "🔄 " : "";
321
322 return html` <sketch-tool-card
323 .open=${this.open}
324 .toolCall=${this.toolCall}
325 >
326 <span slot="summary" class="summary-text">
327 <div class="command-wrapper">
328 ${backgroundIcon}${inputData?.command}
329 </div>
330 </span>
331 <div slot="input" class="input">
332 <div class="tool-call-result-container">
333 <pre>${backgroundIcon}${inputData?.command}</pre>
334 </div>
335 </div>
336 ${this.toolCall?.result_message?.tool_result
337 ? html`<div slot="result" class="result">
338 <div class="tool-call-result-container">
339 <pre class="tool-call-result">
340${this.toolCall?.result_message.tool_result}</pre
341 >
342 </div>
343 </div>`
344 : ""}
345 </sketch-tool-card>`;
346 }
347}
348
349@customElement("sketch-tool-card-codereview")
350export class SketchToolCardCodeReview extends LitElement {
351 @property() toolCall: ToolCall;
352 @property() open: boolean;
353
354 // Determine the status icon based on the content of the result message
355 getStatusIcon(resultText: string): string {
356 if (!resultText) return "";
357 if (resultText === "OK") return "✔️";
358 if (resultText.includes("# Errors")) return "⚠️";
359 if (resultText.includes("# Info")) return "ℹ️";
360 if (resultText.includes("uncommitted changes in repo")) return "🧹";
361 if (resultText.includes("no new commits have been added")) return "🐣";
362 if (resultText.includes("git repo is not clean")) return "🧼";
363 return "❓";
364 }
365
366 render() {
367 const resultText = this.toolCall?.result_message?.tool_result || "";
368 const statusIcon = this.getStatusIcon(resultText);
369
370 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
371 <span slot="summary" class="summary-text">${statusIcon}</span>
372 <div slot="result"><pre>${resultText}</pre></div>
373 </sketch-tool-card>`;
374 }
375}
376
377@customElement("sketch-tool-card-done")
378export class SketchToolCardDone extends LitElement {
379 @property() toolCall: ToolCall;
380 @property() open: boolean;
381
382 render() {
383 const doneInput = JSON.parse(this.toolCall.input);
384 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
385 <span slot="summary" class="summary-text"></span>
386 <div slot="result">
387 ${Object.keys(doneInput.checklist_items).map((key) => {
388 const item = doneInput.checklist_items[key];
389 let statusIcon = "⛔";
390 if (item.status == "yes") {
391 statusIcon = "👍";
392 } else if (item.status == "not applicable") {
393 statusIcon = "🤷‍♂️";
394 }
395 return html`<div>
396 <span>${statusIcon}</span> ${key}:${item.status}
397 </div>`;
398 })}
399 </div>
400 </sketch-tool-card>`;
401 }
402}
403
404@customElement("sketch-tool-card-patch")
405export class SketchToolCardPatch extends LitElement {
406 @property() toolCall: ToolCall;
407 @property() open: boolean;
408
409 static styles = css`
410 .summary-text {
411 color: #555;
412 font-family: monospace;
413 overflow: hidden;
414 text-overflow: ellipsis;
415 white-space: nowrap;
416 border-radius: 3px;
417 }
418 `;
419
420 render() {
421 const patchInput = JSON.parse(this.toolCall?.input);
422 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
423 <span slot="summary" class="summary-text">
424 ${patchInput?.path}: ${patchInput.patches.length}
425 edit${patchInput.patches.length > 1 ? "s" : ""}
426 </span>
427 <div slot="input">
428 ${patchInput.patches.map((patch) => {
429 return html`Patch operation: <b>${patch.operation}</b>
430 <pre>${patch.newText}</pre>`;
431 })}
432 </div>
433 <div slot="result">
434 <pre>${this.toolCall?.result_message?.tool_result}</pre>
435 </div>
436 </sketch-tool-card>`;
437 }
438}
439
440@customElement("sketch-tool-card-think")
441export class SketchToolCardThink extends LitElement {
442 @property() toolCall: ToolCall;
443 @property() open: boolean;
444
445 static styles = css`
446 .thought-bubble {
447 overflow-x: auto;
448 margin-bottom: 3px;
449 font-family: monospace;
450 padding: 3px 5px;
451 background: rgb(236, 236, 236);
452 border-radius: 6px;
453 user-select: text;
454 cursor: text;
455 -webkit-user-select: text;
456 -moz-user-select: text;
457 -ms-user-select: text;
458 font-size: 13px;
459 line-height: 1.3;
460 }
461 .summary-text {
462 overflow: hidden;
463 text-overflow: ellipsis;
464 font-family: monospace;
465 }
466 `;
467
468 render() {
469 return html`
470 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
471 <span slot="summary" class="summary-text">
472 ${JSON.parse(this.toolCall?.input)?.thoughts?.split("\n")[0]}
473 </span>
474 <div slot="input" class="thought-bubble">
475 <div class="markdown-content">
476 ${unsafeHTML(
477 renderMarkdown(JSON.parse(this.toolCall?.input)?.thoughts),
478 )}
479 </div>
480 </div>
481 </sketch-tool-card>
482 `;
483 }
484}
485
486@customElement("sketch-tool-card-title")
487export class SketchToolCardTitle extends LitElement {
488 @property() toolCall: ToolCall;
489 @property() open: boolean;
490
491 static styles = css`
492 .summary-text {
493 font-style: italic;
494 }
495 pre {
496 display: inline;
497 font-family: monospace;
498 background: rgb(236, 236, 236);
499 padding: 2px 4px;
500 border-radius: 2px;
501 margin: 0;
502 }
503 `;
504
505 render() {
506 const inputData = JSON.parse(this.toolCall?.input || "{}");
507 return html`
508 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
509 <span slot="summary" class="summary-text">
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000510 Title: "${inputData.title}"
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100511 </span>
512 <div slot="input">
513 <div>Set title to: <b>${inputData.title}</b></div>
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000514 </div>
515 </sketch-tool-card>
516 `;
517 }
518}
519
520@customElement("sketch-tool-card-precommit")
521export class SketchToolCardPrecommit extends LitElement {
522 @property()
523 toolCall: ToolCall;
524
525 @property()
526 open: boolean;
527
528 static styles = css`
529 .summary-text {
530 font-style: italic;
531 }
532 pre {
533 display: inline;
534 font-family: monospace;
535 background: rgb(236, 236, 236);
536 padding: 2px 4px;
537 border-radius: 2px;
538 margin: 0;
539 }
540 `;
541 constructor() {
542 super();
543 }
544
545 connectedCallback() {
546 super.connectedCallback();
547 }
548
549 disconnectedCallback() {
550 super.disconnectedCallback();
551 }
552
553 render() {
554 const inputData = JSON.parse(this.toolCall?.input || "{}");
555 return html`
556 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
557 <span slot="summary" class="summary-text">
558 Branch: sketch/${inputData.branch_name}
559 </span>
560 <div slot="input">
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100561 <div>Set branch to: <code>sketch/${inputData.branch_name}</code></div>
562 </div>
563 </sketch-tool-card>
564 `;
565 }
566}
567
568@customElement("sketch-tool-card-multiple-choice")
569export class SketchToolCardMultipleChoice extends LitElement {
570 @property() toolCall: ToolCall;
571 @property() open: boolean;
572 @property() selectedOption: MultipleChoiceOption = null;
573
574 static styles = css`
575 .options-container {
576 display: flex;
577 flex-direction: row;
578 flex-wrap: wrap;
579 gap: 8px;
580 margin: 10px 0;
581 }
582 .option {
583 display: inline-flex;
584 align-items: center;
585 padding: 8px 12px;
586 border-radius: 4px;
587 background-color: #f5f5f5;
588 cursor: pointer;
589 transition: all 0.2s;
590 border: 1px solid transparent;
591 user-select: none;
592 }
593 .option:hover {
594 background-color: #e0e0e0;
595 border-color: #ccc;
596 transform: translateY(-1px);
597 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
598 }
599 .option:active {
600 transform: translateY(0);
601 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
602 background-color: #d5d5d5;
603 }
604 .option.selected {
605 background-color: #e3f2fd;
606 border-color: #2196f3;
607 border-width: 1px;
608 border-style: solid;
609 }
610 .option-checkmark {
611 margin-left: 6px;
612 color: #2196f3;
613 }
614 .summary-text {
615 font-style: italic;
616 padding: 0.5em;
617 }
618 .summary-text strong {
619 font-style: normal;
620 color: #2196f3;
621 font-weight: 600;
622 }
623 `;
624
625 connectedCallback() {
626 super.connectedCallback();
627 this.updateSelectedOption();
628 }
629
630 updated(changedProps) {
631 if (changedProps.has("toolCall")) {
632 this.updateSelectedOption();
633 }
634 }
635
636 updateSelectedOption() {
637 if (this.toolCall?.result_message?.tool_result) {
638 try {
639 this.selectedOption = JSON.parse(
640 this.toolCall.result_message.tool_result,
641 ).selected;
642 } catch (e) {
643 console.error("Error parsing result:", e);
644 }
645 } else {
646 this.selectedOption = null;
647 }
648 }
649
650 async handleOptionClick(choice) {
651 this.selectedOption = this.selectedOption === choice ? null : choice;
652
653 const event = new CustomEvent("multiple-choice-selected", {
654 detail: {
655 responseText: this.selectedOption.responseText,
656 toolCall: this.toolCall,
657 },
658 bubbles: true,
659 composed: true,
660 });
661 this.dispatchEvent(event);
662 }
663
664 render() {
665 let choices = [];
666 let question = "";
667 try {
668 const inputData = JSON.parse(
669 this.toolCall?.input || "{}",
670 ) as MultipleChoiceParams;
671 choices = inputData.responseOptions || [];
672 question = inputData.question || "Please select an option:";
673 } catch (e) {
674 console.error("Error parsing multiple-choice input:", e);
675 }
676
677 const summaryContent =
678 this.selectedOption !== null
679 ? html`<span class="summary-text">
680 ${question}: <strong>${this.selectedOption.caption}</strong>
681 </span>`
682 : html`<span class="summary-text">${question}</span>`;
683
684 return html`
685 <div class="multiple-choice-card">
686 ${summaryContent}
687 <div class="options-container">
688 ${choices.map((choice) => {
689 const isSelected =
690 this.selectedOption !== null && this.selectedOption === choice;
691 return html`
692 <div
693 class="option ${isSelected ? "selected" : ""}"
694 @click=${() => this.handleOptionClick(choice)}
695 title="${choice.responseText}"
696 >
697 <span class="option-label">${choice.caption}</span>
698 ${isSelected
699 ? html`<span class="option-checkmark">✓</span>`
700 : ""}
701 </div>
702 `;
703 })}
704 </div>
705 </div>
706 `;
707 }
708}
709
710@customElement("sketch-tool-card-generic")
711export class SketchToolCardGeneric extends LitElement {
712 @property() toolCall: ToolCall;
713 @property() open: boolean;
714
715 render() {
716 return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
717 <span slot="summary" class="summary-text">${this.toolCall?.input}</span>
718 <div slot="input">
719 Input:
720 <pre>${this.toolCall?.input}</pre>
721 </div>
722 <div slot="result">
723 Result:
724 ${this.toolCall?.result_message?.tool_result
725 ? html`<pre>${this.toolCall?.result_message.tool_result}</pre>`
726 : ""}
727 </div>
728 </sketch-tool-card>`;
729 }
730}
731
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700732declare global {
733 interface HTMLElementTagNameMap {
734 "sketch-tool-card": SketchToolCard;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100735 "sketch-tool-card-generic": SketchToolCardGeneric;
736 "sketch-tool-card-bash": SketchToolCardBash;
737 "sketch-tool-card-codereview": SketchToolCardCodeReview;
738 "sketch-tool-card-done": SketchToolCardDone;
739 "sketch-tool-card-patch": SketchToolCardPatch;
740 "sketch-tool-card-think": SketchToolCardThink;
741 "sketch-tool-card-title": SketchToolCardTitle;
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000742 "sketch-tool-card-precommit": SketchToolCardPrecommit;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100743 "sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700744 }
745}