blob: a8d0accf49e5d793fb720750eff32e601f37cced [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 }
403
404 .thought-bubble .preview {
405 white-space: nowrap;
406 text-overflow: ellipsis;
407 overflow: hidden;
408 }
409
410 .thought-bubble:before {
411 content: '';
412 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 }
421
422 .thought-bubble:after {
423 content: '';
424 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 }
433
434
435 .patch-input-preview {
436 color: #555;
437 font-family: monospace;
438 overflow: hidden;
439 text-overflow: ellipsis;
440 white-space: nowrap;
441 max-width: 30%;
442 background-color: rgba(240, 240, 240, 0.5);
443 padding: 2px 5px;
444 border-radius: 3px;
445 font-size: 0.9em;
446 }
447
448 .codereview-OK {
449 color: green;
450 }
451 `;
452
453 constructor() {
454 super();
455 }
456
457 // See https://lit.dev/docs/components/lifecycle/
458 connectedCallback() {
459 super.connectedCallback();
460 }
461
462 // See https://lit.dev/docs/components/lifecycle/
463 disconnectedCallback() {
464 super.disconnectedCallback();
465 }
466
467 renderMarkdown(markdownContent: string): string {
468 try {
469 // Set markdown options for proper code block highlighting and safety
470 const markedOptions: MarkedOptions = {
471 gfm: true, // GitHub Flavored Markdown
472 breaks: true, // Convert newlines to <br>
473 async: false,
474 // DOMPurify is recommended for production, but not included in this implementation
475 };
476 return marked.parse(markdownContent, markedOptions) as string;
477 } catch (error) {
478 console.error("Error rendering markdown:", error);
479 // Fallback to plain text if markdown parsing fails
480 return markdownContent;
481 }
482 }
483
484 _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
485 console.log("cancelToolCall", tool_call_id, button);
486 button.innerText = "Cancelling";
487 button.disabled = true;
488 try {
489 const response = await fetch("cancel", {
490 method: "POST",
491 headers: {
492 "Content-Type": "application/json",
493 },
494 body: JSON.stringify({
495 tool_call_id: tool_call_id,
496 reason: "user requested cancellation",
497 }),
498 });
499 if (response.ok) {
500 console.log("cancel", tool_call_id, response);
501 button.parentElement.removeChild(button);
502 } else {
503 button.innerText = "Cancel";
504 console.log(`error trying to cancel ${tool_call_id}: `, response);
505 }
506 } catch (e) {
507 console.error("cancel", tool_call_id, e);
508 }
509 };
510
511 toolCard(toolCall: ToolCall) {
512 const toolCallStatus = toolCall.result_message
513 ? toolCall.result_message.tool_error
514 ? "❌"
515 : ""
516 : "⏳";
517
518 const cancelButton = toolCall.result_message
519 ? ""
520 : html`<button
521 class="cancel-button"
522 title="Cancel this operation"
523 @click=${(e: Event) => {
524 e.stopPropagation();
525 const button = e.target as HTMLButtonElement;
526 this._cancelToolCall(toolCall.tool_call_id, button);
527 }}
528 >
529 Cancel
530 </button>`;
531
532 const status = html`<span
533 class="tool-call-status ${toolCall.result_message ? "" : "spinner"}"
534 >${toolCallStatus}</span
535 >`;
536
537 switch (toolCall.name) {
538 case "title":
539 const titleInput = JSON.parse(toolCall.input);
540 return html`
541 <div class="tool-call-compact-view">
542 I've set the title of this sketch to <b>"${titleInput.title}"</b>
543 </div>`;
544 case "bash":
545 const bashInput = JSON.parse(toolCall.input);
546 return html`
547 <div class="tool-call-compact-view">
548 ${status}
549 <span class="tool-call-name">${toolCall.name}</span>
550 <pre class="tool-call-input-preview">${bashInput.command}</pre>
551 ${toolCall.result_message
552 ? html`
553 ${toolCall.result_message.tool_result
554 ? html`
555 <pre class="tool-call-result-preview">
556${toolCall.result_message.tool_result}</pre>`
557 : ""}`
558 : cancelButton}
559 </div>`;
560 case "codereview":
561 return html`
562 <div class="tool-call-compact-view">
563 ${status}
564 <span class="tool-call-name">${toolCall.name}</span>
565 ${cancelButton}
566 <code class="codereview-preview codereview-${toolCall.result_message?.tool_result}">${toolCall.result_message?.tool_result == 'OK' ? '✔️': '⛔ ' + toolCall.result_message?.tool_result}</code>
567 </div>`;
568 case "think":
569 const thinkInput = JSON.parse(toolCall.input);
570 return html`
571 <div class="tool-call-compact-view">
572 ${status}
573 <span class="tool-call-name">${toolCall.name}</span>
574 <div class="thought-bubble"><div class="preview">${thinkInput.thoughts}</div></div>
575 ${cancelButton}
576 </div>`;
577 case "patch":
578 const patchInput = JSON.parse(toolCall.input);
579 return html`
580 <div class="tool-call-compact-view">
581 ${status}
582 <span class="tool-call-name">${toolCall.name}</span>
583 <div class="patch-input-preview"><span class="patch-path">${patchInput.path}</span>: ${patchInput.patches.length} edit${patchInput.patches.length > 1 ? 's': ''}</div>
584 ${cancelButton}
585 </div>`;
586 case "done":
587 const doneInput = JSON.parse(toolCall.input);
588 return html`
589 <div class="tool-call-compact-view">
590 ${status}
591 <span class="tool-call-name">${toolCall.name}</span>
592 <div class="done-input-preview">
593 ${Object.keys(doneInput.checklist_items).map((key) => {
594 const item = doneInput.checklist_items[key];
595 let statusIcon = '⛔';
596 if (item.status == 'yes') {
597 statusIcon = '👍';
598 } else if (item.status =='not applicable') {
599 statusIcon = '🤷‍♂️';
600 }
601 return html`<div><span>${statusIcon}</span> ${key}:${item.status}</div>`;
602 })}
603 </div>
604 ${cancelButton}
605 </div>`;
606
607 default: // Generic tool card:
608 return html`
609 <div class="tool-call-compact-view">
610 ${status}
611 <span class="tool-call-name">${toolCall.name}</span>
612 <code class="tool-call-input-preview">${toolCall.input}</code>
613 ${cancelButton}
614 <code class="tool-call-result-preview">${toolCall.result_message?.tool_result}</code>
615 </div>
616 ${toolCall.result_message?.tool_result}
617 `;
618 }
619 }
620 render() {
621 return html`
622 <div class="tool-calls-container">
623 <div class="tool-calls-header"></div>
624 <div class="tool-call-cards-container">
625 ${this.toolCalls?.map((toolCall) => {
626 return html`<div class="tool-call-card ${toolCall.name}">
627 ${this.toolCard(toolCall)}
628 </div>`;
629 })}
630 </div>
631 </div>`;
632 }
633}
634
635declare global {
636 interface HTMLElementTagNameMap {
637 "sketch-tool-calls": SketchToolCalls;
638 }
639}