blob: e7a5c749199d479b7c7c221e677c3da699c9f7c7 [file] [log] [blame]
Sean McCullough86b56862025-04-18 13:04:03 -07001import { css, html, LitElement } from "lit";
2import { unsafeHTML } from "lit/directives/unsafe-html.js";
3import { repeat } from "lit/directives/repeat.js";
4import { customElement, property } from "lit/decorators.js";
5import { State, ToolCall } from "../types";
6import { marked, MarkedOptions } from "marked";
7
8@customElement("sketch-tool-calls")
9export class SketchToolCalls extends LitElement {
10 @property()
11 toolCalls: ToolCall[] = [];
12
13 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
14 // Note that these styles only apply to the scope of this web component's
15 // shadow DOM node, so they won't leak out or collide with CSS declared in
16 // other components or the containing web page (...unless you want it to do that).
17 static styles = css`
18 /* Tool calls container styles */
19 .tool-calls-container {
20 /* Removed dotted border */
21 }
22
23 .tool-calls-toggle {
24 cursor: pointer;
25 background-color: #f0f0f0;
26 padding: 5px 10px;
27 border: none;
28 border-radius: 4px;
29 text-align: left;
30 font-size: 12px;
31 margin-top: 5px;
32 color: #555;
33 font-weight: 500;
34 }
35
36 .tool-calls-toggle:hover {
37 background-color: #e0e0e0;
38 }
39
40 .tool-calls-details {
41 margin-top: 10px;
42 transition: max-height 0.3s ease;
43 }
44
45 .tool-calls-details.collapsed {
46 max-height: 0;
47 overflow: hidden;
48 margin-top: 0;
49 }
50
51 .tool-call {
52 background: #f9f9f9;
53 border-radius: 4px;
54 padding: 10px;
55 margin-bottom: 10px;
56 border-left: 3px solid #4caf50;
57 }
58
59 .tool-call-header {
60 margin-bottom: 8px;
61 font-size: 14px;
62 padding: 2px 0;
63 }
64
65 /* Compact tool display styles */
66 .tool-compact-line {
67 font-family: monospace;
68 font-size: 12px;
69 line-height: 1.4;
70 padding: 4px 6px;
71 background: #f8f8f8;
72 border-radius: 3px;
73 position: relative;
74 white-space: nowrap;
75 overflow: hidden;
76 text-overflow: ellipsis;
77 max-width: 100%;
78 display: flex;
79 align-items: center;
80 }
81
82 .tool-result-inline {
83 font-family: monospace;
84 color: #0066bb;
85 white-space: nowrap;
86 overflow: hidden;
87 text-overflow: ellipsis;
88 max-width: 400px;
89 display: inline-block;
90 vertical-align: middle;
91 }
92
93 .copy-inline-button {
94 font-size: 10px;
95 padding: 2px 4px;
96 margin-left: 8px;
97 background: #eee;
98 border: none;
99 border-radius: 3px;
100 cursor: pointer;
101 opacity: 0.7;
102 }
103
104 .copy-inline-button:hover {
105 opacity: 1;
106 background: #ddd;
107 }
108
109 .tool-input.compact,
110 .tool-result.compact {
111 margin: 2px 0;
112 padding: 4px;
113 font-size: 12px;
114 }
115
116 /* Removed old compact container CSS */
117
118 /* Ultra-compact tool call box styles */
119 .tool-calls-header {
120 /* Empty header - just small spacing */
121 }
122
123 .tool-call-boxes-row {
124 display: flex;
125 flex-wrap: wrap;
126 gap: 8px;
127 margin-bottom: 8px;
128 }
129
130 .tool-call-wrapper {
131 display: flex;
132 flex-direction: column;
133 margin-bottom: 4px;
134 }
135
136 .tool-call-box {
137 display: inline-flex;
138 align-items: center;
139 background: #f0f0f0;
140 border-radius: 4px;
141 padding: 3px 8px;
142 font-size: 12px;
143 cursor: pointer;
144 max-width: 320px;
145 position: relative;
146 border: 1px solid #ddd;
147 transition: background-color 0.2s;
148 }
149
150 .tool-call-box:hover {
151 background-color: #e8e8e8;
152 }
153
154 .tool-call-box.expanded {
155 background-color: #e0e0e0;
156 border-bottom-left-radius: 0;
157 border-bottom-right-radius: 0;
158 border-bottom: 1px solid #ccc;
159 }
160
161 .tool-call-input {
162 color: #666;
163 white-space: nowrap;
164 overflow: hidden;
165 text-overflow: ellipsis;
166 font-family: monospace;
167 font-size: 11px;
168 }
169
170 .tool-call-card {
171 display: flex;
172 flex-direction: column;
173 background-color: white;
174 overflow: hidden;
175 cursor: pointer;
176 }
177
178 /* Compact view (default) */
179 .tool-call-compact-view {
180 display: flex;
181 align-items: center;
182 gap: 8px;
183 font-size: 0.9em;
184 white-space: nowrap;
185 overflow: visible; /* Don't hide overflow, we'll handle text truncation per element */
186 position: relative; /* For positioning the expand icon */
187 }
188
189 /* Expanded view (hidden by default) */
190 .tool-call-card.collapsed .tool-call-expanded-view {
191 display: none;
192 }
193
194 .tool-call-expanded-view {
195 display: flex;
196 flex-direction: column;
197 border-top: 1px solid #eee;
198 }
199
200 .tool-call-header {
201 display: flex;
202 align-items: center;
203 justify-content: space-between;
204 padding: 6px 10px;
205 background-color: #f0f0f0;
206 border-bottom: 1px solid #ddd;
207 font-weight: bold;
208 }
209
210 .tool-call-name {
211 color: gray;
212 }
213
214 .tool-call-status {
215 margin-right: 4px;
216 text-align: center;
217 }
218
219 .tool-call-status.spinner {
220 animation: spin 1s infinite linear;
221 display: inline-block;
222 width: 1em;
223 }
224
225 .tool-call-time {
226 margin-left: 8px;
227 font-size: 0.85em;
228 color: #666;
229 font-weight: normal;
230 }
231
232 .tool-call-input-preview {
233 color: #555;
234 font-family: var(--monospace-font);
235 overflow: hidden;
236 text-overflow: ellipsis;
237 white-space: nowrap;
238 max-width: 30%;
239 background-color: rgba(240, 240, 240, 0.5);
240 padding: 2px 5px;
241 border-radius: 3px;
242 font-size: 0.9em;
243 }
244
245 .tool-call-result-preview {
246 color: #28a745;
247 font-family: var(--monospace-font);
248 overflow: hidden;
249 text-overflow: ellipsis;
250 white-space: nowrap;
251 max-width: 40%;
252 background-color: rgba(240, 248, 240, 0.5);
253 padding: 2px 5px;
254 border-radius: 3px;
255 font-size: 0.9em;
256 }
257
258 .tool-call-expand-icon {
259 position: absolute;
260 right: 10px;
261 font-size: 0.8em;
262 color: #888;
263 }
264
265 .tool-call-input {
266 padding: 6px 10px;
267 border-bottom: 1px solid #eee;
268 font-family: var(--monospace-font);
269 font-size: 0.9em;
270 white-space: pre-wrap;
271 word-break: break-all;
272 background-color: #f5f5f5;
273 }
274
275 .tool-call-result {
276 padding: 6px 10px;
277 font-family: var(--monospace-font);
278 font-size: 0.9em;
279 white-space: pre-wrap;
280 max-height: 300px;
281 overflow-y: auto;
282 }
283
284 .tool-call-result pre {
285 margin: 0;
286 white-space: pre-wrap;
287 }
288
289 @keyframes spin {
290 0% {
291 transform: rotate(0deg);
292 }
293 100% {
294 transform: rotate(360deg);
295 }
296 }
297
298 /* Standalone tool messages (legacy/disconnected) */
299 .tool-details.standalone .tool-header {
300 border-radius: 4px;
301 background-color: #fff3cd;
302 border-color: #ffeeba;
303 }
304
305 .tool-details.standalone .tool-warning {
306 margin-left: 10px;
307 font-size: 0.85em;
308 color: #856404;
309 font-style: italic;
310 }
311
312 /* Tool call expanded view with sections */
313 .tool-call-section {
314 border-bottom: 1px solid #eee;
315 }
316
317 .tool-call-section:last-child {
318 border-bottom: none;
319 }
320
321 .tool-call-section-label {
322 display: flex;
323 justify-content: space-between;
324 align-items: center;
325 padding: 8px 10px;
326 background-color: #f5f5f5;
327 font-weight: bold;
328 font-size: 0.9em;
329 }
330
331 .tool-call-section-content {
332 padding: 0;
333 }
334
335 .tool-call-copy-btn {
336 background-color: #f0f0f0;
337 border: 1px solid #ddd;
338 border-radius: 4px;
339 padding: 2px 8px;
340 font-size: 0.8em;
341 cursor: pointer;
342 transition: background-color 0.2s;
343 }
344
345 .tool-call-copy-btn:hover {
346 background-color: #e0e0e0;
347 }
348
349 /* Override for tool call input in expanded view */
350 .tool-call-section-content .tool-call-input {
351 margin: 0;
352 padding: 8px 10px;
353 border: none;
354 background-color: #fff;
355 max-height: 300px;
356 overflow-y: auto;
357 }
358
359 .tool-call-card .tool-call-input-preview,
360 .tool-call-card .tool-call-result-preview {
361 font-family: monospace;
362 background: black;
363 padding: 1em;
364 }
365 .tool-call-input-preview {
366 color: white;
367 }
368 .tool-call-result-preview {
369 color: gray;
370 }
371
372 .tool-call-card.title {
373 font-style: italic;
374 }
375
376 .cancel-button {
377 background: rgb(76, 175, 80);
378 color: white;
379 border: none;
380 padding: 4px 10px;
381 border-radius: 4px;
382 cursor: pointer;
383 font-size: 12px;
384 margin: 5px;
385 }
386
387 .cancel-button:hover {
388 background: rgb(200, 35, 51) !important;
389 }
390
391 .thought-bubble {
392 position: relative;
393 background-color: #eee;
394 border-radius: 8px;
395 padding: 0.5em;
396 box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
397 margin-left: 24px;
398 margin-top: 24px;
399 margin-bottom: 12px;
400 max-width: 30%;
401 white-space: pre;
402 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700403
Sean McCullough86b56862025-04-18 13:04:03 -0700404 .thought-bubble .preview {
405 white-space: nowrap;
406 text-overflow: ellipsis;
407 overflow: hidden;
408 }
409
410 .thought-bubble:before {
Sean McCullough71941bd2025-04-18 13:31:48 -0700411 content: "";
Sean McCullough86b56862025-04-18 13:04:03 -0700412 position: absolute;
413 top: -8px;
414 left: -8px;
415 width: 15px;
416 height: 15px;
417 background-color: #eee;
418 border-radius: 50%;
419 box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
420 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700421
Sean McCullough86b56862025-04-18 13:04:03 -0700422 .thought-bubble:after {
Sean McCullough71941bd2025-04-18 13:31:48 -0700423 content: "";
Sean McCullough86b56862025-04-18 13:04:03 -0700424 position: absolute;
425 top: -16px;
426 left: -16px;
427 width: 8px;
428 height: 8px;
429 background-color: #eee;
430 border-radius: 50%;
431 box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
432 }
Sean McCullough86b56862025-04-18 13:04:03 -0700433
434 .patch-input-preview {
435 color: #555;
436 font-family: monospace;
437 overflow: hidden;
438 text-overflow: ellipsis;
439 white-space: nowrap;
440 max-width: 30%;
441 background-color: rgba(240, 240, 240, 0.5);
442 padding: 2px 5px;
443 border-radius: 3px;
444 font-size: 0.9em;
445 }
446
447 .codereview-OK {
448 color: green;
449 }
450 `;
451
452 constructor() {
453 super();
454 }
455
456 // See https://lit.dev/docs/components/lifecycle/
457 connectedCallback() {
458 super.connectedCallback();
459 }
460
461 // See https://lit.dev/docs/components/lifecycle/
462 disconnectedCallback() {
463 super.disconnectedCallback();
464 }
465
466 renderMarkdown(markdownContent: string): string {
467 try {
468 // Set markdown options for proper code block highlighting and safety
469 const markedOptions: MarkedOptions = {
470 gfm: true, // GitHub Flavored Markdown
471 breaks: true, // Convert newlines to <br>
472 async: false,
473 // DOMPurify is recommended for production, but not included in this implementation
474 };
475 return marked.parse(markdownContent, markedOptions) as string;
476 } catch (error) {
477 console.error("Error rendering markdown:", error);
478 // Fallback to plain text if markdown parsing fails
479 return markdownContent;
480 }
481 }
482
483 _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
484 console.log("cancelToolCall", tool_call_id, button);
485 button.innerText = "Cancelling";
486 button.disabled = true;
487 try {
488 const response = await fetch("cancel", {
489 method: "POST",
490 headers: {
491 "Content-Type": "application/json",
492 },
493 body: JSON.stringify({
494 tool_call_id: tool_call_id,
495 reason: "user requested cancellation",
496 }),
497 });
498 if (response.ok) {
499 console.log("cancel", tool_call_id, response);
500 button.parentElement.removeChild(button);
501 } else {
502 button.innerText = "Cancel";
503 console.log(`error trying to cancel ${tool_call_id}: `, response);
504 }
505 } catch (e) {
506 console.error("cancel", tool_call_id, e);
507 }
508 };
509
510 toolCard(toolCall: ToolCall) {
511 const toolCallStatus = toolCall.result_message
512 ? toolCall.result_message.tool_error
513 ? "❌"
514 : ""
515 : "⏳";
516
517 const cancelButton = toolCall.result_message
518 ? ""
519 : html`<button
520 class="cancel-button"
521 title="Cancel this operation"
522 @click=${(e: Event) => {
523 e.stopPropagation();
524 const button = e.target as HTMLButtonElement;
525 this._cancelToolCall(toolCall.tool_call_id, button);
526 }}
527 >
528 Cancel
529 </button>`;
530
531 const status = html`<span
532 class="tool-call-status ${toolCall.result_message ? "" : "spinner"}"
533 >${toolCallStatus}</span
534 >`;
535
536 switch (toolCall.name) {
537 case "title":
538 const titleInput = JSON.parse(toolCall.input);
Sean McCullough71941bd2025-04-18 13:31:48 -0700539 return html` <div class="tool-call-compact-view">
Sean McCullough86b56862025-04-18 13:04:03 -0700540 I've set the title of this sketch to <b>"${titleInput.title}"</b>
541 </div>`;
542 case "bash":
543 const bashInput = JSON.parse(toolCall.input);
Sean McCullough71941bd2025-04-18 13:31:48 -0700544 return html` <div class="tool-call-compact-view">
Sean McCullough86b56862025-04-18 13:04:03 -0700545 ${status}
546 <span class="tool-call-name">${toolCall.name}</span>
547 <pre class="tool-call-input-preview">${bashInput.command}</pre>
548 ${toolCall.result_message
Sean McCullough71941bd2025-04-18 13:31:48 -0700549 ? html` ${toolCall.result_message.tool_result
550 ? html` <pre class="tool-call-result-preview">
551${toolCall.result_message.tool_result}</pre
552 >`
553 : ""}`
Sean McCullough86b56862025-04-18 13:04:03 -0700554 : cancelButton}
555 </div>`;
556 case "codereview":
Sean McCullough71941bd2025-04-18 13:31:48 -0700557 return html` <div class="tool-call-compact-view">
Sean McCullough86b56862025-04-18 13:04:03 -0700558 ${status}
559 <span class="tool-call-name">${toolCall.name}</span>
560 ${cancelButton}
Sean McCullough71941bd2025-04-18 13:31:48 -0700561 <code
562 class="codereview-preview codereview-${toolCall.result_message
563 ?.tool_result}"
564 >${toolCall.result_message?.tool_result == "OK"
565 ? "✔️"
566 : "⛔ " + toolCall.result_message?.tool_result}</code
567 >
Sean McCullough86b56862025-04-18 13:04:03 -0700568 </div>`;
569 case "think":
570 const thinkInput = JSON.parse(toolCall.input);
Sean McCullough71941bd2025-04-18 13:31:48 -0700571 return html` <div class="tool-call-compact-view">
Sean McCullough86b56862025-04-18 13:04:03 -0700572 ${status}
573 <span class="tool-call-name">${toolCall.name}</span>
Sean McCullough71941bd2025-04-18 13:31:48 -0700574 <div class="thought-bubble">
575 <div class="preview">${thinkInput.thoughts}</div>
576 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700577 ${cancelButton}
578 </div>`;
579 case "patch":
580 const patchInput = JSON.parse(toolCall.input);
Sean McCullough71941bd2025-04-18 13:31:48 -0700581 return html` <div class="tool-call-compact-view">
Sean McCullough86b56862025-04-18 13:04:03 -0700582 ${status}
583 <span class="tool-call-name">${toolCall.name}</span>
Sean McCullough71941bd2025-04-18 13:31:48 -0700584 <div class="patch-input-preview">
585 <span class="patch-path">${patchInput.path}</span>:
586 ${patchInput.patches.length}
587 edit${patchInput.patches.length > 1 ? "s" : ""}
588 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700589 ${cancelButton}
590 </div>`;
591 case "done":
592 const doneInput = JSON.parse(toolCall.input);
Sean McCullough71941bd2025-04-18 13:31:48 -0700593 return html` <div class="tool-call-compact-view">
Sean McCullough86b56862025-04-18 13:04:03 -0700594 ${status}
595 <span class="tool-call-name">${toolCall.name}</span>
596 <div class="done-input-preview">
597 ${Object.keys(doneInput.checklist_items).map((key) => {
598 const item = doneInput.checklist_items[key];
Sean McCullough71941bd2025-04-18 13:31:48 -0700599 let statusIcon = "⛔";
600 if (item.status == "yes") {
601 statusIcon = "👍";
602 } else if (item.status == "not applicable") {
603 statusIcon = "🤷‍♂️";
Sean McCullough86b56862025-04-18 13:04:03 -0700604 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700605 return html`<div>
606 <span>${statusIcon}</span> ${key}:${item.status}
607 </div>`;
Sean McCullough86b56862025-04-18 13:04:03 -0700608 })}
609 </div>
610 ${cancelButton}
611 </div>`;
612
613 default: // Generic tool card:
614 return html`
Sean McCullough71941bd2025-04-18 13:31:48 -0700615 <div class="tool-call-compact-view">
616 ${status}
617 <span class="tool-call-name">${toolCall.name}</span>
618 <code class="tool-call-input-preview">${toolCall.input}</code>
619 ${cancelButton}
620 <code class="tool-call-result-preview"
621 >${toolCall.result_message?.tool_result}</code
622 >
623 </div>
624 ${toolCall.result_message?.tool_result}
625 `;
Sean McCullough86b56862025-04-18 13:04:03 -0700626 }
627 }
628 render() {
Sean McCullough71941bd2025-04-18 13:31:48 -0700629 return html` <div class="tool-calls-container">
Sean McCullough86b56862025-04-18 13:04:03 -0700630 <div class="tool-calls-header"></div>
631 <div class="tool-call-cards-container">
632 ${this.toolCalls?.map((toolCall) => {
633 return html`<div class="tool-call-card ${toolCall.name}">
634 ${this.toolCard(toolCall)}
635 </div>`;
636 })}
637 </div>
638 </div>`;
639 }
640}
641
642declare global {
643 interface HTMLElementTagNameMap {
644 "sketch-tool-calls": SketchToolCalls;
645 }
646}