blob: 5c5e4c2963ffb480e42a7d462a180ea772b92104 [file] [log] [blame]
banksean333aa672025-07-13 19:49:21 +00001import { html } from "lit";
Pokey Rule7ac5ed02025-05-07 15:26:10 +01002import { unsafeHTML } from "lit/directives/unsafe-html.js";
banksean333aa672025-07-13 19:49:21 +00003import { customElement, property } from "lit/decorators.js";
Autoformatter7e5fe3c2025-06-04 22:24:53 +00004import {
5 ToolCall,
6 MultipleChoiceOption,
7 MultipleChoiceParams,
8 State,
9} from "../types";
philip.zeyliger26bc6592025-06-30 20:15:30 -070010import { marked } from "marked";
Philip Zeyliger53ab2452025-06-04 17:49:33 +000011import DOMPurify from "dompurify";
banksean333aa672025-07-13 19:49:21 +000012import { SketchTailwindElement } from "./sketch-tailwind-element";
13import "./sketch-tool-card-base";
Pokey Rule7ac5ed02025-05-07 15:26:10 +010014
Philip Zeyliger53ab2452025-06-04 17:49:33 +000015// Shared utility function for markdown rendering with DOMPurify sanitization
Pokey Rule7ac5ed02025-05-07 15:26:10 +010016function renderMarkdown(markdownContent: string): string {
17 try {
Philip Zeyliger53ab2452025-06-04 17:49:33 +000018 // Parse markdown with default settings
19 const htmlOutput = marked.parse(markdownContent, {
Pokey Rule7ac5ed02025-05-07 15:26:10 +010020 gfm: true,
21 breaks: true,
22 async: false,
23 }) as string;
Philip Zeyliger53ab2452025-06-04 17:49:33 +000024
25 // Sanitize the output HTML with DOMPurify
26 return DOMPurify.sanitize(htmlOutput, {
27 // Allow common safe HTML elements
28 ALLOWED_TAGS: [
29 "p",
30 "br",
31 "strong",
32 "em",
33 "b",
34 "i",
35 "u",
36 "s",
37 "code",
38 "pre",
39 "h1",
40 "h2",
41 "h3",
42 "h4",
43 "h5",
44 "h6",
45 "ul",
46 "ol",
47 "li",
48 "blockquote",
49 "a",
50 ],
51 ALLOWED_ATTR: [
52 "href",
53 "title",
54 "target",
55 "rel", // For links
56 "class", // For basic styling
57 ],
58 // Keep content formatting
59 KEEP_CONTENT: true,
60 });
Pokey Rule7ac5ed02025-05-07 15:26:10 +010061 } catch (error) {
62 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +000063 // Fallback to sanitized plain text if markdown parsing fails
64 return DOMPurify.sanitize(markdownContent);
Pokey Rule7ac5ed02025-05-07 15:26:10 +010065 }
66}
67
banksean333aa672025-07-13 19:49:21 +000068// Shared utility function for creating Tailwind pre elements
69function createPreElement(content: string, additionalClasses: string = "") {
70 return html`<pre
71 class="bg-gray-200 text-black p-2 rounded whitespace-pre-wrap break-words max-w-full w-full box-border overflow-wrap-break-word ${additionalClasses}"
72 >
73${content}</pre
74 >`;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070075}
76
Pokey Rule7ac5ed02025-05-07 15:26:10 +010077@customElement("sketch-tool-card-bash")
banksean333aa672025-07-13 19:49:21 +000078export class SketchToolCardBash extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +010079 @property() toolCall: ToolCall;
80 @property() open: boolean;
81
Pokey Rule7ac5ed02025-05-07 15:26:10 +010082 render() {
83 const inputData = JSON.parse(this.toolCall?.input || "{}");
84 const isBackground = inputData?.background === true;
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000085 const isSlowOk = inputData?.slow_ok === true;
Autoformatter9d9c8122025-07-18 03:37:23 +000086 const backgroundIcon = isBackground
87 ? html`<span title="Running in background">🥷</span> `
88 : "";
89 const slowIcon = isSlowOk
90 ? html`<span title="Extended timeouts">🐢</span> `
91 : "";
Pokey Rule7ac5ed02025-05-07 15:26:10 +010092
Philip Zeyligere31d2a92025-05-11 15:22:35 -070093 // Truncate the command if it's too long to display nicely
94 const command = inputData?.command || "";
95 const displayCommand =
96 command.length > 80 ? command.substring(0, 80) + "..." : command;
97
banksean333aa672025-07-13 19:49:21 +000098 const summaryContent = html`<div
99 class="max-w-full overflow-hidden text-ellipsis whitespace-nowrap"
100 >
101 ${backgroundIcon}${slowIcon}${displayCommand}
102 </div>`;
103
104 const inputContent = html`<div
105 class="flex w-full max-w-full flex-col overflow-wrap-break-word break-words"
106 >
107 <div class="w-full relative">
Josh Bleecher Snyder45943882025-07-18 02:13:31 +0000108 <pre
109 class="bg-gray-200 text-black p-2 rounded whitespace-pre-wrap break-words max-w-full w-full box-border overflow-wrap-break-word w-full mb-0 rounded-t rounded-b-none box-border"
110 >
Autoformatter9d9c8122025-07-18 03:37:23 +0000111${backgroundIcon}${slowIcon}${inputData?.command}</pre
112 >
banksean333aa672025-07-13 19:49:21 +0000113 </div>
114 </div>`;
115
116 const resultContent = this.toolCall?.result_message?.tool_result
117 ? html`<div class="w-full relative">
118 ${createPreElement(
119 this.toolCall.result_message.tool_result,
120 "mt-0 text-gray-600 rounded-t-none rounded-b w-full box-border max-h-[300px] overflow-y-auto",
121 )}
122 </div>`
123 : "";
124
125 return html`<sketch-tool-card-base
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100126 .open=${this.open}
127 .toolCall=${this.toolCall}
banksean333aa672025-07-13 19:49:21 +0000128 .summaryContent=${summaryContent}
129 .inputContent=${inputContent}
130 .resultContent=${resultContent}
131 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100132 }
133}
134
135@customElement("sketch-tool-card-codereview")
banksean333aa672025-07-13 19:49:21 +0000136export class SketchToolCardCodeReview extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100137 @property() toolCall: ToolCall;
138 @property() open: boolean;
139
140 // Determine the status icon based on the content of the result message
141 getStatusIcon(resultText: string): string {
142 if (!resultText) return "";
143 if (resultText === "OK") return "✔️";
144 if (resultText.includes("# Errors")) return "⚠️";
145 if (resultText.includes("# Info")) return "ℹ️";
146 if (resultText.includes("uncommitted changes in repo")) return "🧹";
147 if (resultText.includes("no new commits have been added")) return "🐣";
148 if (resultText.includes("git repo is not clean")) return "🧼";
149 return "❓";
150 }
151
152 render() {
153 const resultText = this.toolCall?.result_message?.tool_result || "";
154 const statusIcon = this.getStatusIcon(resultText);
155
banksean333aa672025-07-13 19:49:21 +0000156 const summaryContent = html`<span>${statusIcon}</span>`;
157 const resultContent = resultText ? createPreElement(resultText) : "";
158
159 return html`<sketch-tool-card-base
160 .open=${this.open}
161 .toolCall=${this.toolCall}
162 .summaryContent=${summaryContent}
163 .resultContent=${resultContent}
164 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100165 }
166}
167
168@customElement("sketch-tool-card-done")
banksean333aa672025-07-13 19:49:21 +0000169export class SketchToolCardDone extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100170 @property() toolCall: ToolCall;
171 @property() open: boolean;
172
173 render() {
174 const doneInput = JSON.parse(this.toolCall.input);
banksean333aa672025-07-13 19:49:21 +0000175
176 const summaryContent = html`<span></span>`;
177
178 const resultContent = html`<div>
179 ${Object.keys(doneInput.checklist_items).map((key) => {
180 const item = doneInput.checklist_items[key];
181 let statusIcon = "〰️";
182 if (item.status == "yes") {
183 statusIcon = "✅";
184 } else if (item.status == "not applicable") {
185 statusIcon = "🤷";
186 }
187 return html`<div class="mb-1">
188 <span>${statusIcon}</span> ${key}:${item.status}
189 </div>`;
190 })}
191 </div>`;
192
193 return html`<sketch-tool-card-base
194 .open=${this.open}
195 .toolCall=${this.toolCall}
196 .summaryContent=${summaryContent}
197 .resultContent=${resultContent}
198 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100199 }
200}
201
202@customElement("sketch-tool-card-patch")
banksean333aa672025-07-13 19:49:21 +0000203export class SketchToolCardPatch extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100204 @property() toolCall: ToolCall;
205 @property() open: boolean;
206
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100207 render() {
208 const patchInput = JSON.parse(this.toolCall?.input);
banksean333aa672025-07-13 19:49:21 +0000209
210 const summaryContent = html`<span
211 class="text-gray-600 font-mono overflow-hidden text-ellipsis whitespace-nowrap rounded"
212 >
213 ${patchInput?.path}: ${patchInput.patches.length}
214 edit${patchInput.patches.length > 1 ? "s" : ""}
215 </span>`;
216
217 const inputContent = html`<div>
218 ${patchInput.patches.map((patch) => {
219 return html`<div class="mb-2">
220 Patch operation: <b>${patch.operation}</b>
221 ${createPreElement(patch.newText)}
222 </div>`;
223 })}
224 </div>`;
225
226 const resultContent = this.toolCall?.result_message?.tool_result
227 ? createPreElement(this.toolCall.result_message.tool_result)
228 : "";
229
230 return html`<sketch-tool-card-base
231 .open=${this.open}
232 .toolCall=${this.toolCall}
233 .summaryContent=${summaryContent}
234 .inputContent=${inputContent}
235 .resultContent=${resultContent}
236 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100237 }
238}
239
240@customElement("sketch-tool-card-think")
banksean333aa672025-07-13 19:49:21 +0000241export class SketchToolCardThink extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100242 @property() toolCall: ToolCall;
243 @property() open: boolean;
244
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100245 render() {
banksean333aa672025-07-13 19:49:21 +0000246 const thoughts = JSON.parse(this.toolCall?.input)?.thoughts || "";
247
248 const summaryContent = html`<span
249 class="overflow-hidden text-ellipsis font-mono"
250 >
251 ${thoughts.split("\n")[0]}
252 </span>`;
253
254 const inputContent = html`<div
255 class="overflow-x-auto mb-1 font-mono px-2 py-1 bg-gray-200 rounded select-text cursor-text text-sm leading-relaxed"
256 >
257 <div class="markdown-content">
258 ${unsafeHTML(renderMarkdown(thoughts))}
259 </div>
260 </div>`;
261
262 return html`<sketch-tool-card-base
263 .open=${this.open}
264 .toolCall=${this.toolCall}
265 .summaryContent=${summaryContent}
266 .inputContent=${inputContent}
267 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100268 }
269}
270
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700271@customElement("sketch-tool-card-commit-message-style")
banksean333aa672025-07-13 19:49:21 +0000272export class SketchToolCardCommitMessageStyle extends SketchTailwindElement {
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000273 @property()
274 toolCall: ToolCall;
275
276 @property()
277 open: boolean;
278
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000279 @property()
280 state: State;
281
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000282 constructor() {
283 super();
284 }
285
286 connectedCallback() {
287 super.connectedCallback();
288 }
289
290 disconnectedCallback() {
291 super.disconnectedCallback();
292 }
293
294 render() {
banksean333aa672025-07-13 19:49:21 +0000295 return html`<sketch-tool-card-base
296 .open=${this.open}
297 .toolCall=${this.toolCall}
298 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100299 }
300}
301
302@customElement("sketch-tool-card-multiple-choice")
banksean333aa672025-07-13 19:49:21 +0000303export class SketchToolCardMultipleChoice extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100304 @property() toolCall: ToolCall;
305 @property() open: boolean;
306 @property() selectedOption: MultipleChoiceOption = null;
307
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100308 connectedCallback() {
309 super.connectedCallback();
310 this.updateSelectedOption();
311 }
312
313 updated(changedProps) {
314 if (changedProps.has("toolCall")) {
315 this.updateSelectedOption();
316 }
317 }
318
319 updateSelectedOption() {
320 if (this.toolCall?.result_message?.tool_result) {
321 try {
322 this.selectedOption = JSON.parse(
323 this.toolCall.result_message.tool_result,
324 ).selected;
325 } catch (e) {
326 console.error("Error parsing result:", e);
327 }
328 } else {
329 this.selectedOption = null;
330 }
331 }
332
333 async handleOptionClick(choice) {
334 this.selectedOption = this.selectedOption === choice ? null : choice;
335
336 const event = new CustomEvent("multiple-choice-selected", {
337 detail: {
338 responseText: this.selectedOption.responseText,
339 toolCall: this.toolCall,
340 },
341 bubbles: true,
342 composed: true,
343 });
344 this.dispatchEvent(event);
345 }
346
347 render() {
348 let choices = [];
349 let question = "";
350 try {
351 const inputData = JSON.parse(
352 this.toolCall?.input || "{}",
353 ) as MultipleChoiceParams;
354 choices = inputData.responseOptions || [];
355 question = inputData.question || "Please select an option:";
356 } catch (e) {
357 console.error("Error parsing multiple-choice input:", e);
358 }
359
360 const summaryContent =
361 this.selectedOption !== null
banksean333aa672025-07-13 19:49:21 +0000362 ? html`<span class="italic p-2">
363 ${question}:
364 <strong class="not-italic text-blue-600 font-semibold"
365 >${this.selectedOption.caption}</strong
366 >
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100367 </span>`
banksean333aa672025-07-13 19:49:21 +0000368 : html`<span class="italic p-2">${question}</span>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100369
banksean333aa672025-07-13 19:49:21 +0000370 const inputContent = html`<div class="flex flex-row flex-wrap gap-2 my-2">
371 ${choices.map((choice) => {
372 const isSelected =
373 this.selectedOption !== null && this.selectedOption === choice;
374 return html`
375 <div
376 class="inline-flex items-center px-3 py-2 rounded cursor-pointer transition-all duration-200 border select-none ${isSelected
377 ? "bg-blue-50 border-blue-500"
378 : "bg-gray-100 border-transparent hover:bg-gray-200 hover:border-gray-400 hover:-translate-y-px hover:shadow-md active:translate-y-0 active:shadow-sm active:bg-gray-300"}"
379 @click=${() => this.handleOptionClick(choice)}
380 title="${choice.responseText}"
381 >
382 <span class="option-label">${choice.caption}</span>
383 ${isSelected
384 ? html`<span class="ml-1.5 text-blue-600">✓</span>`
385 : ""}
386 </div>
387 `;
388 })}
389 </div>`;
390
391 return html`<div class="multiple-choice-card">
392 ${summaryContent} ${inputContent}
393 </div>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100394 }
395}
396
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700397@customElement("sketch-tool-card-todo-write")
banksean333aa672025-07-13 19:49:21 +0000398export class SketchToolCardTodoWrite extends SketchTailwindElement {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700399 @property() toolCall: ToolCall;
400 @property() open: boolean;
401
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700402 render() {
403 const inputData = JSON.parse(this.toolCall?.input || "{}");
404 const tasks = inputData.tasks || [];
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000405
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700406 // Generate circles based on task status
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000407 const circles = tasks
408 .map((task) => {
409 switch (task.status) {
410 case "completed":
411 return "●"; // full circle
412 case "in-progress":
413 return "◐"; // half circle
414 case "queued":
415 default:
416 return "○"; // empty circle
417 }
418 })
419 .join(" ");
420
banksean333aa672025-07-13 19:49:21 +0000421 const summaryContent = html`<span class="italic text-gray-600">
422 ${circles}
423 </span>`;
424 const resultContent = this.toolCall?.result_message?.tool_result
425 ? createPreElement(this.toolCall.result_message.tool_result)
426 : "";
427
428 return html`<sketch-tool-card-base
429 .open=${this.open}
430 .toolCall=${this.toolCall}
431 .summaryContent=${summaryContent}
432 .resultContent=${resultContent}
433 ></sketch-tool-card-base>`;
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000434 }
435}
436
437@customElement("sketch-tool-card-keyword-search")
banksean333aa672025-07-13 19:49:21 +0000438export class SketchToolCardKeywordSearch extends SketchTailwindElement {
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000439 @property() toolCall: ToolCall;
440 @property() open: boolean;
441
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000442 render() {
443 const inputData = JSON.parse(this.toolCall?.input || "{}");
444 const query = inputData.query || "";
445 const searchTerms = inputData.search_terms || [];
446
banksean333aa672025-07-13 19:49:21 +0000447 const summaryContent = html`<div
448 class="flex flex-col gap-0.5 w-full max-w-full overflow-hidden"
449 >
450 <div
451 class="text-gray-800 text-xs normal-case whitespace-normal break-words leading-tight"
452 >
453 🔍 ${query}
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000454 </div>
banksean333aa672025-07-13 19:49:21 +0000455 <div
456 class="text-gray-600 text-xs normal-case whitespace-normal break-words leading-tight mt-px"
457 >
458 🗝️ ${searchTerms.join(", ")}
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000459 </div>
banksean333aa672025-07-13 19:49:21 +0000460 </div>`;
461
462 const inputContent = html`<div>
463 <div><strong>Query:</strong> ${query}</div>
464 <div><strong>Search terms:</strong> ${searchTerms.join(", ")}</div>
465 </div>`;
466
467 const resultContent = this.toolCall?.result_message?.tool_result
468 ? createPreElement(this.toolCall.result_message.tool_result)
469 : "";
470
471 return html`<sketch-tool-card-base
472 .open=${this.open}
473 .toolCall=${this.toolCall}
474 .summaryContent=${summaryContent}
475 .inputContent=${inputContent}
476 .resultContent=${resultContent}
477 ></sketch-tool-card-base>`;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700478 }
479}
480
481@customElement("sketch-tool-card-todo-read")
banksean333aa672025-07-13 19:49:21 +0000482export class SketchToolCardTodoRead extends SketchTailwindElement {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700483 @property() toolCall: ToolCall;
484 @property() open: boolean;
485
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700486 render() {
banksean333aa672025-07-13 19:49:21 +0000487 const summaryContent = html`<span class="italic text-gray-600">
488 Read todo list
489 </span>`;
490 const resultContent = this.toolCall?.result_message?.tool_result
491 ? createPreElement(this.toolCall.result_message.tool_result)
492 : "";
493
494 return html`<sketch-tool-card-base
495 .open=${this.open}
496 .toolCall=${this.toolCall}
497 .summaryContent=${summaryContent}
498 .resultContent=${resultContent}
499 ></sketch-tool-card-base>`;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700500 }
501}
502
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100503@customElement("sketch-tool-card-generic")
banksean333aa672025-07-13 19:49:21 +0000504export class SketchToolCardGeneric extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100505 @property() toolCall: ToolCall;
506 @property() open: boolean;
507
508 render() {
banksean333aa672025-07-13 19:49:21 +0000509 const summaryContent = html`<span
510 class="block whitespace-normal break-words max-w-full w-full"
511 >
512 ${this.toolCall?.input}
513 </span>`;
514
515 const inputContent = html`<div class="max-w-full break-words">
516 Input:
517 ${createPreElement(
518 this.toolCall?.input || "",
519 "max-w-full whitespace-pre-wrap break-words",
520 )}
521 </div>`;
522
523 const resultContent = this.toolCall?.result_message?.tool_result
524 ? html`<div class="max-w-full break-words">
525 Result: ${createPreElement(this.toolCall.result_message.tool_result)}
526 </div>`
527 : "";
528
529 return html`<sketch-tool-card-base
530 .open=${this.open}
531 .toolCall=${this.toolCall}
532 .summaryContent=${summaryContent}
533 .inputContent=${inputContent}
534 .resultContent=${resultContent}
535 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100536 }
537}
538
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700539declare global {
540 interface HTMLElementTagNameMap {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100541 "sketch-tool-card-generic": SketchToolCardGeneric;
542 "sketch-tool-card-bash": SketchToolCardBash;
543 "sketch-tool-card-codereview": SketchToolCardCodeReview;
544 "sketch-tool-card-done": SketchToolCardDone;
545 "sketch-tool-card-patch": SketchToolCardPatch;
546 "sketch-tool-card-think": SketchToolCardThink;
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700547 "sketch-tool-card-commit-message-style": SketchToolCardCommitMessageStyle;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100548 "sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700549 "sketch-tool-card-todo-write": SketchToolCardTodoWrite;
550 "sketch-tool-card-todo-read": SketchToolCardTodoRead;
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000551 "sketch-tool-card-keyword-search": SketchToolCardKeywordSearch;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700552 }
553}