blob: a6fcdf98566d8a585fa85967ed682596ad6bed26 [file] [log] [blame]
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001import { css, html, LitElement } from "lit";
2import { unsafeHTML } from "lit/directives/unsafe-html.js";
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00003import { customElement, property, state } from "lit/decorators.js";
Sean McCullough485afc62025-04-28 14:28:39 -07004import { ToolCall, MultipleChoiceOption, MultipleChoiceParams } from "../types";
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07005import { marked, MarkedOptions } from "marked";
6
7function renderMarkdown(markdownContent: string): string {
8 try {
9 // Set markdown options for proper code block highlighting and safety
10 const markedOptions: MarkedOptions = {
11 gfm: true, // GitHub Flavored Markdown
12 breaks: true, // Convert newlines to <br>
13 async: false,
14 // DOMPurify is recommended for production, but not included in this implementation
15 };
16 return marked.parse(markdownContent, markedOptions) as string;
17 } catch (error) {
18 console.error("Error rendering markdown:", error);
19 // Fallback to plain text if markdown parsing fails
20 return markdownContent;
21 }
22}
23
24@customElement("sketch-tool-card")
25export class SketchToolCard extends LitElement {
26 @property()
27 toolCall: ToolCall;
28
29 @property()
30 open: boolean;
31
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000032 @state()
33 detailsVisible: boolean = false;
34
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070035 static styles = css`
36 .tool-call {
37 display: flex;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000038 flex-direction: column;
39 width: 100%;
40 }
41
42 .tool-row {
43 display: flex;
44 width: 100%;
45 box-sizing: border-box;
46 padding: 6px 8px 6px 12px;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070047 align-items: center;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000048 gap: 8px; /* Reduce gap slightly to accommodate longer tool names */
49 cursor: pointer;
50 border-radius: 4px;
51 position: relative;
52 overflow: hidden; /* Changed to hidden to prevent horizontal scrolling */
53 }
54
55 .tool-row:hover {
56 background-color: rgba(0, 0, 0, 0.02);
57 }
58
59 .tool-name {
60 font-family: monospace;
61 font-weight: 500;
62 color: #444;
63 background-color: rgba(0, 0, 0, 0.05);
64 border-radius: 3px;
65 padding: 2px 6px;
66 flex-shrink: 0;
67 min-width: 45px;
68 /* Remove max-width to prevent truncation */
69 font-size: 12px;
70 text-align: center;
71 /* Remove overflow/ellipsis to ensure names are fully visible */
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070072 white-space: nowrap;
73 }
74
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000075 .tool-success {
76 color: #5cb85c;
77 font-size: 14px;
78 }
79
80 .tool-error {
Josh Bleecher Snydere750ec92025-05-05 23:01:57 +000081 color: #6c757d;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000082 font-size: 14px;
83 }
84
85 .tool-pending {
86 color: #f0ad4e;
87 font-size: 14px;
88 }
89
90 .summary-text {
91 white-space: nowrap;
92 text-overflow: ellipsis;
93 overflow: hidden;
94 flex-grow: 1;
95 flex-shrink: 1;
96 color: #444;
97 font-family: monospace;
98 font-size: 12px;
99 padding: 0 4px;
100 min-width: 50px;
101 max-width: calc(
102 100% - 250px
103 ); /* More space for tool-name and tool-status */
104 display: inline-block; /* Ensure proper truncation */
105 }
106
107 .tool-status {
108 display: flex;
109 align-items: center;
110 gap: 12px;
111 margin-left: auto;
112 flex-shrink: 0;
113 min-width: 120px; /* Increased width to prevent cutoff */
114 justify-content: flex-end;
115 padding-right: 8px;
116 }
117
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700118 .tool-call-status {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000119 display: flex;
120 align-items: center;
121 justify-content: center;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700122 }
123
124 .tool-call-status.spinner {
125 animation: spin 1s infinite linear;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700126 }
127
128 @keyframes spin {
129 0% {
130 transform: rotate(0deg);
131 }
132 100% {
133 transform: rotate(360deg);
134 }
135 }
136
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000137 .elapsed {
138 font-size: 11px;
139 color: #777;
140 white-space: nowrap;
141 min-width: 40px;
142 text-align: right;
143 }
144
145 .tool-details {
146 padding: 8px;
147 background-color: rgba(0, 0, 0, 0.02);
148 margin-top: 1px;
149 border-top: 1px solid rgba(0, 0, 0, 0.05);
150 display: none;
151 font-family: monospace;
152 font-size: 12px;
153 color: #333;
154 border-radius: 0 0 4px 4px;
155 max-width: 100%;
156 width: 100%;
157 box-sizing: border-box;
158 overflow: hidden; /* Hide overflow at container level */
159 }
160
161 .tool-details.visible {
162 display: block;
163 }
164
165 .expand-indicator {
166 color: #aaa;
167 font-size: 10px;
168 width: 12px;
169 display: inline-block;
170 text-align: center;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700171 }
172
173 .cancel-button {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700174 cursor: pointer;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000175 color: white;
176 background-color: #d9534f;
177 border: none;
178 border-radius: 3px;
179 font-size: 11px;
180 padding: 2px 6px;
181 white-space: nowrap;
182 min-width: 50px;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700183 }
184
185 .cancel-button:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000186 background-color: #c9302c;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700187 }
188
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000189 .cancel-button[disabled] {
190 background-color: #999;
191 cursor: not-allowed;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700192 }
193
194 .tool-error-message {
195 font-style: italic;
Josh Bleecher Snydere750ec92025-05-05 23:01:57 +0000196 color: #6c757d;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700197 }
Sean McCullough2deac842025-04-21 18:17:57 -0700198
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000199 .codereview-OK {
200 color: green;
Sean McCullough2deac842025-04-21 18:17:57 -0700201 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700202 `;
203
204 constructor() {
205 super();
206 }
207
208 connectedCallback() {
209 super.connectedCallback();
210 }
211
212 disconnectedCallback() {
213 super.disconnectedCallback();
214 }
215
216 _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
217 console.log("cancelToolCall", tool_call_id, button);
218 button.innerText = "Cancelling";
219 button.disabled = true;
220 try {
221 const response = await fetch("cancel", {
222 method: "POST",
223 headers: {
224 "Content-Type": "application/json",
225 },
226 body: JSON.stringify({
227 tool_call_id: tool_call_id,
228 reason: "user requested cancellation",
229 }),
230 });
231 if (response.ok) {
232 console.log("cancel", tool_call_id, response);
233 button.parentElement.removeChild(button);
234 } else {
235 button.innerText = "Cancel";
236 console.log(`error trying to cancel ${tool_call_id}: `, response);
237 }
238 } catch (e) {
239 console.error("cancel", tool_call_id, e);
240 }
241 };
242
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000243 _toggleDetails(e: Event) {
244 e.stopPropagation();
245 this.detailsVisible = !this.detailsVisible;
246 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700247
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000248 render() {
249 // Determine the status indicator based on the tool call result
250 let statusIcon;
251 if (!this.toolCall?.result_message) {
252 // Pending status with spinner
253 statusIcon = html`<span class="tool-call-status spinner tool-pending"
254 >⏳</span
255 >`;
256 } else if (this.toolCall?.result_message.tool_error) {
257 // Error status
Josh Bleecher Snydere750ec92025-05-05 23:01:57 +0000258 statusIcon = html`<span class="tool-call-status tool-error">🔔</span>`;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000259 } else {
260 // Success status
261 statusIcon = html`<span class="tool-call-status tool-success">✓</span>`;
262 }
263
264 // Cancel button for pending operations
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700265 const cancelButton = this.toolCall?.result_message
266 ? ""
267 : html`<button
268 class="cancel-button"
269 title="Cancel this operation"
270 @click=${(e: Event) => {
271 e.stopPropagation();
272 const button = e.target as HTMLButtonElement;
273 this._cancelToolCall(this.toolCall?.tool_call_id, button);
274 }}
275 >
276 Cancel
277 </button>`;
278
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000279 // Elapsed time display
280 const elapsed = this.toolCall?.result_message?.elapsed
Sean McCullough2deac842025-04-21 18:17:57 -0700281 ? html`<span class="elapsed"
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000282 >${(this.toolCall?.result_message?.elapsed / 1e9).toFixed(1)}s</span
Sean McCullough2deac842025-04-21 18:17:57 -0700283 >`
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000284 : html`<span class="elapsed"></span>`; // Empty span to maintain layout
Sean McCullough2deac842025-04-21 18:17:57 -0700285
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000286 // Initialize details visibility based on open property
287 if (this.open && !this.detailsVisible) {
288 this.detailsVisible = true;
289 }
290
291 return html`<div class="tool-call">
292 <div class="tool-row" @click=${this._toggleDetails}>
293 <span class="tool-name">${this.toolCall?.name}</span>
294 <span class="summary-text"><slot name="summary"></slot></span>
295 <div class="tool-status">${statusIcon} ${elapsed} ${cancelButton}</div>
296 </div>
297 <div class="tool-details ${this.detailsVisible ? "visible" : ""}">
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700298 <slot name="input"></slot>
299 <slot name="result"></slot>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000300 </div>
301 </div>`;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700302 }
303}
304
305@customElement("sketch-tool-card-bash")
306export class SketchToolCardBash extends LitElement {
307 @property()
308 toolCall: ToolCall;
309
310 @property()
311 open: boolean;
312
313 static styles = css`
314 pre {
Philip Zeyligera54c6a32025-04-23 02:13:36 +0000315 background: rgb(236, 236, 236);
316 color: black;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700317 padding: 0.5em;
318 border-radius: 4px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000319 white-space: pre-wrap; /* Always wrap long lines */
320 word-break: break-word; /* Use break-word for a more readable break */
321 max-width: 100%;
322 width: 100%;
323 box-sizing: border-box;
324 overflow-wrap: break-word; /* Additional property for better wrapping */
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700325 }
326 .summary-text {
327 overflow: hidden;
328 text-overflow: ellipsis;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000329 white-space: nowrap;
330 max-width: 100%;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700331 font-family: monospace;
332 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000333
334 .command-wrapper {
335 overflow: hidden;
336 text-overflow: ellipsis;
337 white-space: nowrap;
338 max-width: 100%;
339 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700340 .input {
341 display: flex;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000342 width: 100%;
343 flex-direction: column; /* Change to column layout */
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700344 }
345 .input pre {
346 width: 100%;
347 margin-bottom: 0;
348 border-radius: 4px 4px 0 0;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000349 box-sizing: border-box; /* Include padding in width calculation */
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700350 }
351 .result pre {
352 margin-top: 0;
Philip Zeyligera54c6a32025-04-23 02:13:36 +0000353 color: #555;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700354 border-radius: 0 0 4px 4px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000355 width: 100%; /* Ensure it uses full width */
356 box-sizing: border-box; /* Include padding in width calculation */
357 overflow-wrap: break-word; /* Ensure long words wrap */
358 }
359
360 /* Add a special class for long output that should be scrollable on hover */
361 .result pre.scrollable-on-hover {
362 max-height: 300px;
363 overflow-y: auto;
364 }
365
366 /* Container for tool call results with proper text wrapping */
367 .tool-call-result-container {
368 width: 100%;
369 position: relative;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700370 }
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000371 .background-badge {
372 display: inline-block;
373 background-color: #6200ea;
374 color: white;
375 font-size: 10px;
376 font-weight: bold;
377 padding: 2px 6px;
378 border-radius: 10px;
379 margin-left: 8px;
380 vertical-align: middle;
381 }
382 .command-wrapper {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000383 display: inline-block;
384 max-width: 100%;
385 overflow: hidden;
386 text-overflow: ellipsis;
387 white-space: nowrap;
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000388 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700389 `;
390
391 constructor() {
392 super();
393 }
394
395 connectedCallback() {
396 super.connectedCallback();
397 }
398
399 disconnectedCallback() {
400 super.disconnectedCallback();
401 }
402
403 render() {
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000404 const inputData = JSON.parse(this.toolCall?.input || "{}");
405 const isBackground = inputData?.background === true;
406 const backgroundIcon = isBackground ? "🔄 " : "";
407
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700408 return html`
409 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000410 <span slot="summary" class="summary-text">
411 <div class="command-wrapper">
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000412 ${backgroundIcon}${inputData?.command}
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000413 </div>
414 </span>
415 <div slot="input" class="input">
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000416 <div class="tool-call-result-container">
417 <pre>${backgroundIcon}${inputData?.command}</pre>
418 </div>
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000419 </div>
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700420 ${
421 this.toolCall?.result_message
422 ? html` ${this.toolCall?.result_message.tool_result
423 ? html`<div slot="result" class="result">
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000424 <div class="tool-call-result-container">
425 <pre class="tool-call-result">
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700426${this.toolCall?.result_message.tool_result}</pre
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000427 >
428 </div>
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700429 </div>`
430 : ""}`
431 : ""
432 }</div>
433 </sketch-tool-card>`;
434 }
435}
436
437@customElement("sketch-tool-card-codereview")
438export class SketchToolCardCodeReview extends LitElement {
439 @property()
440 toolCall: ToolCall;
441
442 @property()
443 open: boolean;
444
445 static styles = css``;
446
447 constructor() {
448 super();
449 }
450
451 connectedCallback() {
452 super.connectedCallback();
453 }
454
455 disconnectedCallback() {
456 super.disconnectedCallback();
457 }
Josh Bleecher Snyder2dc86b92025-04-29 14:11:58 +0000458 // Determine the status icon based on the content of the result message
459 // This corresponds to the output format in claudetool/differential.go:Run
460 getStatusIcon(resultText: string): string {
461 if (!resultText) return "";
462 if (resultText === "OK") return "✔️";
Josh Bleecher Snydere750ec92025-05-05 23:01:57 +0000463 if (resultText.includes("# Errors")) return "⚠️";
Josh Bleecher Snyder2dc86b92025-04-29 14:11:58 +0000464 if (resultText.includes("# Info")) return "ℹ️";
465 if (resultText.includes("uncommitted changes in repo")) return "🧹";
466 if (resultText.includes("no new commits have been added")) return "🐣";
467 if (resultText.includes("git repo is not clean")) return "🧼";
468 return "❓";
469 }
470
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700471 render() {
Josh Bleecher Snyder2dc86b92025-04-29 14:11:58 +0000472 const resultText = this.toolCall?.result_message?.tool_result || "";
473 const statusIcon = this.getStatusIcon(resultText);
474
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700475 return html` <sketch-tool-card
476 .open=${this.open}
477 .toolCall=${this.toolCall}
478 >
Josh Bleecher Snyder2dc86b92025-04-29 14:11:58 +0000479 <span slot="summary" class="summary-text"> ${statusIcon} </span>
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700480 <div slot="result">
Josh Bleecher Snyder2dc86b92025-04-29 14:11:58 +0000481 <pre>${resultText}</pre>
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700482 </div>
483 </sketch-tool-card>`;
484 }
485}
486
487@customElement("sketch-tool-card-done")
488export class SketchToolCardDone extends LitElement {
489 @property()
490 toolCall: ToolCall;
491
492 @property()
493 open: boolean;
494
495 static styles = css``;
496
497 constructor() {
498 super();
499 }
500
501 connectedCallback() {
502 super.connectedCallback();
503 }
504
505 disconnectedCallback() {
506 super.disconnectedCallback();
507 }
508
509 render() {
510 const doneInput = JSON.parse(this.toolCall.input);
511 return html` <sketch-tool-card
512 .open=${this.open}
513 .toolCall=${this.toolCall}
514 >
515 <span slot="summary" class="summary-text"> </span>
516 <div slot="result">
517 ${Object.keys(doneInput.checklist_items).map((key) => {
518 const item = doneInput.checklist_items[key];
519 let statusIcon = "⛔";
520 if (item.status == "yes") {
521 statusIcon = "👍";
522 } else if (item.status == "not applicable") {
523 statusIcon = "🤷‍♂️";
524 }
525 return html`<div>
526 <span>${statusIcon}</span> ${key}:${item.status}
527 </div>`;
528 })}
529 </div>
530 </sketch-tool-card>`;
531 }
532}
533
534@customElement("sketch-tool-card-patch")
535export class SketchToolCardPatch extends LitElement {
536 @property()
537 toolCall: ToolCall;
538
539 @property()
540 open: boolean;
541
542 static styles = css`
543 .summary-text {
544 color: #555;
545 font-family: monospace;
546 overflow: hidden;
547 text-overflow: ellipsis;
548 white-space: nowrap;
549 border-radius: 3px;
550 }
551 `;
552
553 constructor() {
554 super();
555 }
556
557 connectedCallback() {
558 super.connectedCallback();
559 }
560
561 disconnectedCallback() {
562 super.disconnectedCallback();
563 }
564
565 render() {
566 const patchInput = JSON.parse(this.toolCall?.input);
567 return html` <sketch-tool-card
568 .open=${this.open}
569 .toolCall=${this.toolCall}
570 >
571 <span slot="summary" class="summary-text">
572 ${patchInput?.path}: ${patchInput.patches.length}
573 edit${patchInput.patches.length > 1 ? "s" : ""}
574 </span>
575 <div slot="input">
576 ${patchInput.patches.map((patch) => {
577 return html` Patch operation: <b>${patch.operation}</b>
578 <pre>${patch.newText}</pre>`;
579 })}
580 </div>
581 <div slot="result">
582 <pre>${this.toolCall?.result_message?.tool_result}</pre>
583 </div>
584 </sketch-tool-card>`;
585 }
586}
587
588@customElement("sketch-tool-card-think")
589export class SketchToolCardThink extends LitElement {
590 @property()
591 toolCall: ToolCall;
592
593 @property()
594 open: boolean;
595
596 static styles = css`
597 .thought-bubble {
598 overflow-x: auto;
599 margin-bottom: 3px;
600 font-family: monospace;
601 padding: 3px 5px;
602 background: rgb(236, 236, 236);
603 border-radius: 6px;
604 user-select: text;
605 cursor: text;
606 -webkit-user-select: text;
607 -moz-user-select: text;
608 -ms-user-select: text;
609 font-size: 13px;
610 line-height: 1.3;
611 }
612 .summary-text {
613 overflow: hidden;
614 text-overflow: ellipsis;
615 font-family: monospace;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700616 }
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 return html`
633 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
634 <span slot="summary" class="summary-text"
Sean McCulloughffb58a32025-04-28 13:50:56 -0700635 >${JSON.parse(this.toolCall?.input)?.thoughts?.split("\n")[0]}</span
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700636 >
637 <div slot="input" class="thought-bubble">
638 <div class="markdown-content">
639 ${unsafeHTML(
640 renderMarkdown(JSON.parse(this.toolCall?.input)?.thoughts),
641 )}
642 </div>
643 </div>
644 </sketch-tool-card>
645 `;
646 }
647}
648
649@customElement("sketch-tool-card-title")
650export class SketchToolCardTitle extends LitElement {
651 @property()
652 toolCall: ToolCall;
653
654 @property()
655 open: boolean;
656
657 static styles = css`
658 .summary-text {
659 font-style: italic;
660 }
Josh Bleecher Snyder47b19362025-04-30 01:34:14 +0000661 pre {
662 display: inline;
663 font-family: monospace;
664 background: rgb(236, 236, 236);
665 padding: 2px 4px;
666 border-radius: 2px;
667 margin: 0;
668 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700669 `;
670 constructor() {
671 super();
672 }
673
674 connectedCallback() {
675 super.connectedCallback();
676 }
677
678 disconnectedCallback() {
679 super.disconnectedCallback();
680 }
681
682 render() {
Josh Bleecher Snyder47b19362025-04-30 01:34:14 +0000683 const inputData = JSON.parse(this.toolCall?.input || "{}");
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700684 return html`
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000685 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
686 <span slot="summary" class="summary-text">
687 Title: "${inputData.title}" | Branch: sketch/${inputData.branch_name}
688 </span>
689 <div slot="input">
690 <div>Set title to: <b>${inputData.title}</b></div>
691 <div>Set branch to: <code>sketch/${inputData.branch_name}</code></div>
692 </div>
693 </sketch-tool-card>
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700694 `;
695 }
696}
697
Sean McCulloughfa40c412025-04-28 20:10:04 +0000698@customElement("sketch-tool-card-multiple-choice")
699export class SketchToolCardMultipleChoice extends LitElement {
700 @property()
701 toolCall: ToolCall;
702
703 @property()
704 open: boolean;
705
706 @property()
Sean McCullough485afc62025-04-28 14:28:39 -0700707 selectedOption: MultipleChoiceOption = null;
Sean McCulloughfa40c412025-04-28 20:10:04 +0000708
709 static styles = css`
710 .options-container {
711 display: flex;
712 flex-direction: row;
713 flex-wrap: wrap;
714 gap: 8px;
715 margin: 10px 0;
716 }
717
718 .option {
719 display: inline-flex;
720 align-items: center;
721 padding: 8px 12px;
722 border-radius: 4px;
723 background-color: #f5f5f5;
724 cursor: pointer;
725 transition: all 0.2s;
726 border: 1px solid transparent;
727 user-select: none;
728 }
729
730 .option:hover {
731 background-color: #e0e0e0;
732 border-color: #ccc;
733 transform: translateY(-1px);
734 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
735 }
736
737 .option:active {
738 transform: translateY(0);
739 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
740 background-color: #d5d5d5;
741 }
742
743 .option.selected {
744 background-color: #e3f2fd;
745 border-color: #2196f3;
746 border-width: 1px;
747 border-style: solid;
748 }
749
750 .option-index {
751 font-size: 0.8em;
752 opacity: 0.7;
753 margin-right: 6px;
754 }
755
756 .option-label {
757 font-family: sans-serif;
758 }
759
760 .option-checkmark {
761 margin-left: 6px;
762 color: #2196f3;
763 }
764
765 .summary-text {
766 font-style: italic;
Sean McCullough485afc62025-04-28 14:28:39 -0700767 padding: 0.5em;
Sean McCulloughfa40c412025-04-28 20:10:04 +0000768 }
769
770 .summary-text strong {
771 font-style: normal;
772 color: #2196f3;
773 font-weight: 600;
774 }
775
776 p {
777 display: flex;
778 align-items: center;
779 flex-wrap: wrap;
780 margin-bottom: 10px;
781 }
782 `;
783
784 constructor() {
785 super();
786 }
787
788 connectedCallback() {
789 super.connectedCallback();
790 this.updateSelectedOption();
791 }
792
793 disconnectedCallback() {
794 super.disconnectedCallback();
795 }
796
797 updated(changedProps) {
798 if (changedProps.has("toolCall")) {
799 this.updateSelectedOption();
800 }
801 }
802
803 updateSelectedOption() {
804 // Get selected option from result if available
805 if (this.toolCall?.result_message?.tool_result) {
806 try {
807 this.selectedOption = JSON.parse(
808 this.toolCall.result_message.tool_result,
809 ).selected;
810 } catch (e) {
811 console.error("Error parsing result:", e);
Sean McCulloughfa40c412025-04-28 20:10:04 +0000812 }
813 } else {
814 this.selectedOption = null;
815 }
816 }
817
Sean McCullough485afc62025-04-28 14:28:39 -0700818 async handleOptionClick(choice) {
Sean McCulloughfa40c412025-04-28 20:10:04 +0000819 // If this option is already selected, unselect it (toggle behavior)
820 if (this.selectedOption === choice) {
821 this.selectedOption = null;
822 } else {
823 // Otherwise, select the clicked option
824 this.selectedOption = choice;
825 }
826
827 // Dispatch a custom event that can be listened to by parent components
Sean McCullough485afc62025-04-28 14:28:39 -0700828 const event = new CustomEvent("multiple-choice-selected", {
829 detail: {
830 responseText: this.selectedOption.responseText,
831 toolCall: this.toolCall,
832 },
Sean McCulloughfa40c412025-04-28 20:10:04 +0000833 bubbles: true,
834 composed: true,
835 });
836 this.dispatchEvent(event);
837 }
838
839 render() {
840 // Parse the input to get choices if available
841 let choices = [];
842 let question = "";
843 try {
Sean McCullough485afc62025-04-28 14:28:39 -0700844 const inputData = JSON.parse(
845 this.toolCall?.input || "{}",
846 ) as MultipleChoiceParams;
847 choices = inputData.responseOptions || [];
Sean McCulloughfa40c412025-04-28 20:10:04 +0000848 question = inputData.question || "Please select an option:";
849 } catch (e) {
850 console.error("Error parsing multiple-choice input:", e);
851 }
852
853 // Determine what to show in the summary slot
854 const summaryContent =
855 this.selectedOption !== null
856 ? html`<span class="summary-text"
Sean McCullough485afc62025-04-28 14:28:39 -0700857 >${question}: <strong>${this.selectedOption.caption}</strong></span
Sean McCulloughfa40c412025-04-28 20:10:04 +0000858 >`
859 : html`<span class="summary-text">${question}</span>`;
860
Sean McCullough485afc62025-04-28 14:28:39 -0700861 return html`
862 <div class="multiple-choice-card">
863 ${summaryContent}
Sean McCulloughfa40c412025-04-28 20:10:04 +0000864 <div class="options-container">
Sean McCullough485afc62025-04-28 14:28:39 -0700865 ${choices.map((choice) => {
Sean McCulloughfa40c412025-04-28 20:10:04 +0000866 const isSelected =
Sean McCullough485afc62025-04-28 14:28:39 -0700867 this.selectedOption !== null && this.selectedOption === choice;
Sean McCulloughfa40c412025-04-28 20:10:04 +0000868 return html`
869 <div
870 class="option ${isSelected ? "selected" : ""}"
871 @click=${() => this.handleOptionClick(choice)}
Sean McCullough485afc62025-04-28 14:28:39 -0700872 title="${choice.responseText}"
Sean McCulloughfa40c412025-04-28 20:10:04 +0000873 >
Sean McCullough485afc62025-04-28 14:28:39 -0700874 <span class="option-label">${choice.caption}</span>
Sean McCulloughfa40c412025-04-28 20:10:04 +0000875 ${isSelected
876 ? html`<span class="option-checkmark">✓</span>`
877 : ""}
878 </div>
879 `;
880 })}
881 </div>
882 </div>
Sean McCullough485afc62025-04-28 14:28:39 -0700883 `;
Sean McCulloughfa40c412025-04-28 20:10:04 +0000884 }
885}
886
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700887@customElement("sketch-tool-card-generic")
888export class SketchToolCardGeneric extends LitElement {
889 @property()
890 toolCall: ToolCall;
891
892 @property()
893 open: boolean;
894
895 constructor() {
896 super();
897 }
898
899 connectedCallback() {
900 super.connectedCallback();
901 }
902
903 disconnectedCallback() {
904 super.disconnectedCallback();
905 }
906
907 render() {
908 return html` <sketch-tool-card
909 .open=${this.open}
910 .toolCall=${this.toolCall}
911 >
912 <span slot="summary" class="summary-text">${this.toolCall?.input}</span>
913 <div slot="input">
914 Input:
915 <pre>${this.toolCall?.input}</pre>
916 </div>
917 <div slot="result">
918 Result:
919 ${this.toolCall?.result_message
920 ? html` ${this.toolCall?.result_message.tool_result
921 ? html`<pre>${this.toolCall?.result_message.tool_result}</pre>`
922 : ""}`
923 : ""}
924 </div>
925 </sketch-tool-card>`;
926 }
927}
928
929declare global {
930 interface HTMLElementTagNameMap {
931 "sketch-tool-card": SketchToolCard;
932 "sketch-tool-card-generic": SketchToolCardGeneric;
933 "sketch-tool-card-bash": SketchToolCardBash;
934 "sketch-tool-card-codereview": SketchToolCardCodeReview;
935 "sketch-tool-card-done": SketchToolCardDone;
936 "sketch-tool-card-patch": SketchToolCardPatch;
937 "sketch-tool-card-think": SketchToolCardThink;
938 "sketch-tool-card-title": SketchToolCardTitle;
Sean McCulloughfa40c412025-04-28 20:10:04 +0000939 "sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700940 }
941}