blob: f61cdb88ac73410938853bd505cf836fc60629e6 [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";
Josh Bleecher Snyderbeaa86a2025-07-23 03:37:21 +00004import { ToolCall } from "../types";
philip.zeyliger26bc6592025-06-30 20:15:30 -07005import { marked } from "marked";
Philip Zeyliger53ab2452025-06-04 17:49:33 +00006import DOMPurify from "dompurify";
banksean333aa672025-07-13 19:49:21 +00007import { SketchTailwindElement } from "./sketch-tailwind-element";
8import "./sketch-tool-card-base";
Pokey Rule7ac5ed02025-05-07 15:26:10 +01009
Philip Zeyliger53ab2452025-06-04 17:49:33 +000010// Shared utility function for markdown rendering with DOMPurify sanitization
Pokey Rule7ac5ed02025-05-07 15:26:10 +010011function renderMarkdown(markdownContent: string): string {
12 try {
Philip Zeyliger53ab2452025-06-04 17:49:33 +000013 // Parse markdown with default settings
14 const htmlOutput = marked.parse(markdownContent, {
Pokey Rule7ac5ed02025-05-07 15:26:10 +010015 gfm: true,
16 breaks: true,
17 async: false,
18 }) as string;
Philip Zeyliger53ab2452025-06-04 17:49:33 +000019
20 // Sanitize the output HTML with DOMPurify
21 return DOMPurify.sanitize(htmlOutput, {
22 // Allow common safe HTML elements
23 ALLOWED_TAGS: [
24 "p",
25 "br",
26 "strong",
27 "em",
28 "b",
29 "i",
30 "u",
31 "s",
32 "code",
33 "pre",
34 "h1",
35 "h2",
36 "h3",
37 "h4",
38 "h5",
39 "h6",
40 "ul",
41 "ol",
42 "li",
43 "blockquote",
44 "a",
45 ],
46 ALLOWED_ATTR: [
47 "href",
48 "title",
49 "target",
50 "rel", // For links
51 "class", // For basic styling
52 ],
53 // Keep content formatting
54 KEEP_CONTENT: true,
55 });
Pokey Rule7ac5ed02025-05-07 15:26:10 +010056 } catch (error) {
57 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +000058 // Fallback to sanitized plain text if markdown parsing fails
59 return DOMPurify.sanitize(markdownContent);
Pokey Rule7ac5ed02025-05-07 15:26:10 +010060 }
61}
62
banksean333aa672025-07-13 19:49:21 +000063// Shared utility function for creating Tailwind pre elements
64function createPreElement(content: string, additionalClasses: string = "") {
65 return html`<pre
banksean3d1308e2025-07-29 17:20:10 +000066 class="bg-gray-200 dark:bg-gray-700 text-black dark:text-gray-100 p-2 rounded whitespace-pre-wrap break-words max-w-full w-full box-border overflow-wrap-break-word ${additionalClasses}"
banksean333aa672025-07-13 19:49:21 +000067 >
68${content}</pre
69 >`;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070070}
71
Pokey Rule7ac5ed02025-05-07 15:26:10 +010072@customElement("sketch-tool-card-bash")
banksean333aa672025-07-13 19:49:21 +000073export class SketchToolCardBash extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +010074 @property() toolCall: ToolCall;
75 @property() open: boolean;
76
Pokey Rule7ac5ed02025-05-07 15:26:10 +010077 render() {
78 const inputData = JSON.parse(this.toolCall?.input || "{}");
79 const isBackground = inputData?.background === true;
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000080 const isSlowOk = inputData?.slow_ok === true;
Autoformatter9d9c8122025-07-18 03:37:23 +000081 const backgroundIcon = isBackground
82 ? html`<span title="Running in background">🥷</span> `
83 : "";
84 const slowIcon = isSlowOk
85 ? html`<span title="Extended timeouts">🐢</span> `
86 : "";
Pokey Rule7ac5ed02025-05-07 15:26:10 +010087
Philip Zeyligere31d2a92025-05-11 15:22:35 -070088 // Truncate the command if it's too long to display nicely
89 const command = inputData?.command || "";
90 const displayCommand =
91 command.length > 80 ? command.substring(0, 80) + "..." : command;
92
banksean333aa672025-07-13 19:49:21 +000093 const summaryContent = html`<div
94 class="max-w-full overflow-hidden text-ellipsis whitespace-nowrap"
95 >
96 ${backgroundIcon}${slowIcon}${displayCommand}
97 </div>`;
98
99 const inputContent = html`<div
100 class="flex w-full max-w-full flex-col overflow-wrap-break-word break-words"
101 >
102 <div class="w-full relative">
Josh Bleecher Snyder45943882025-07-18 02:13:31 +0000103 <pre
banksean3d1308e2025-07-29 17:20:10 +0000104 class="bg-gray-200 dark:bg-gray-700 text-black dark:text-gray-100 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"
Josh Bleecher Snyder45943882025-07-18 02:13:31 +0000105 >
Autoformatter9d9c8122025-07-18 03:37:23 +0000106${backgroundIcon}${slowIcon}${inputData?.command}</pre
107 >
banksean333aa672025-07-13 19:49:21 +0000108 </div>
109 </div>`;
110
111 const resultContent = this.toolCall?.result_message?.tool_result
112 ? html`<div class="w-full relative">
113 ${createPreElement(
114 this.toolCall.result_message.tool_result,
115 "mt-0 text-gray-600 rounded-t-none rounded-b w-full box-border max-h-[300px] overflow-y-auto",
116 )}
117 </div>`
118 : "";
119
120 return html`<sketch-tool-card-base
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100121 .open=${this.open}
122 .toolCall=${this.toolCall}
banksean333aa672025-07-13 19:49:21 +0000123 .summaryContent=${summaryContent}
124 .inputContent=${inputContent}
125 .resultContent=${resultContent}
126 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100127 }
128}
129
130@customElement("sketch-tool-card-codereview")
banksean333aa672025-07-13 19:49:21 +0000131export class SketchToolCardCodeReview extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100132 @property() toolCall: ToolCall;
133 @property() open: boolean;
134
135 // Determine the status icon based on the content of the result message
136 getStatusIcon(resultText: string): string {
137 if (!resultText) return "";
138 if (resultText === "OK") return "✔️";
139 if (resultText.includes("# Errors")) return "⚠️";
140 if (resultText.includes("# Info")) return "ℹ️";
141 if (resultText.includes("uncommitted changes in repo")) return "🧹";
142 if (resultText.includes("no new commits have been added")) return "🐣";
143 if (resultText.includes("git repo is not clean")) return "🧼";
144 return "❓";
145 }
146
147 render() {
148 const resultText = this.toolCall?.result_message?.tool_result || "";
149 const statusIcon = this.getStatusIcon(resultText);
150
banksean333aa672025-07-13 19:49:21 +0000151 const summaryContent = html`<span>${statusIcon}</span>`;
152 const resultContent = resultText ? createPreElement(resultText) : "";
153
154 return html`<sketch-tool-card-base
155 .open=${this.open}
156 .toolCall=${this.toolCall}
157 .summaryContent=${summaryContent}
158 .resultContent=${resultContent}
159 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100160 }
161}
162
163@customElement("sketch-tool-card-done")
banksean333aa672025-07-13 19:49:21 +0000164export class SketchToolCardDone extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100165 @property() toolCall: ToolCall;
166 @property() open: boolean;
167
168 render() {
169 const doneInput = JSON.parse(this.toolCall.input);
banksean333aa672025-07-13 19:49:21 +0000170
171 const summaryContent = html`<span></span>`;
172
173 const resultContent = html`<div>
174 ${Object.keys(doneInput.checklist_items).map((key) => {
175 const item = doneInput.checklist_items[key];
176 let statusIcon = "〰️";
177 if (item.status == "yes") {
178 statusIcon = "✅";
179 } else if (item.status == "not applicable") {
180 statusIcon = "🤷";
181 }
182 return html`<div class="mb-1">
183 <span>${statusIcon}</span> ${key}:${item.status}
184 </div>`;
185 })}
186 </div>`;
187
188 return html`<sketch-tool-card-base
189 .open=${this.open}
190 .toolCall=${this.toolCall}
191 .summaryContent=${summaryContent}
192 .resultContent=${resultContent}
193 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100194 }
195}
196
197@customElement("sketch-tool-card-patch")
banksean333aa672025-07-13 19:49:21 +0000198export class SketchToolCardPatch extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100199 @property() toolCall: ToolCall;
200 @property() open: boolean;
201
Josh Bleecher Snyder3dd3e412025-07-22 20:32:03 -0700202 // Render a diff with syntax highlighting
203 renderDiff(diff: string) {
204 // Remove ---/+++ header lines and trim leading/trailing blank lines
205 const lines = diff
206 .split("\n")
207 .filter((line) => !line.startsWith("---") && !line.startsWith("+++"))
208 .join("\n")
209 .trim()
210 .split("\n");
211
212 const coloredLines = lines.map((line) => {
213 if (line.startsWith("+")) {
Josh Bleecher Snyder6b1ceb12025-07-30 12:09:43 -0700214 // prettier-ignore
215 return html`<div class="text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/20">${line}</div>`;
Josh Bleecher Snyder3dd3e412025-07-22 20:32:03 -0700216 } else if (line.startsWith("-")) {
Josh Bleecher Snyder6b1ceb12025-07-30 12:09:43 -0700217 // prettier-ignore
218 return html`<div class="text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20">${line}</div>`;
Josh Bleecher Snyder3dd3e412025-07-22 20:32:03 -0700219 } else if (line.startsWith("@@")) {
220 // prettier-ignore
banksean3d1308e2025-07-29 17:20:10 +0000221 return html`<div class="text-cyan-600 dark:text-cyan-400 bg-cyan-50 dark:bg-cyan-900/20 font-semibold">${line}</div>`;
Josh Bleecher Snyder3dd3e412025-07-22 20:32:03 -0700222 } else {
Josh Bleecher Snyder6b1ceb12025-07-30 12:09:43 -0700223 // prettier-ignore
224 return html`<div class="text-gray-800 dark:text-gray-200">${line}</div>`;
Josh Bleecher Snyder3dd3e412025-07-22 20:32:03 -0700225 }
226 });
227
Josh Bleecher Snyder6b1ceb12025-07-30 12:09:43 -0700228 // prettier-ignore
229 return html`<pre class="bg-gray-100 dark:bg-gray-800 text-xs p-2 rounded whitespace-pre-wrap break-words max-w-full w-full box-border overflow-x-auto font-mono text-gray-900 dark:text-gray-100">${coloredLines}</pre>`;
Josh Bleecher Snyder3dd3e412025-07-22 20:32:03 -0700230 }
231
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100232 render() {
233 const patchInput = JSON.parse(this.toolCall?.input);
banksean333aa672025-07-13 19:49:21 +0000234
Autoformatter2c3e02c2025-07-31 00:41:49 +0000235 const toolFailed = this.toolCall?.result_message?.tool_error;
banksean333aa672025-07-13 19:49:21 +0000236 const summaryContent = html`<span
237 class="text-gray-600 font-mono overflow-hidden text-ellipsis whitespace-nowrap rounded"
238 >
Autoformatter2c3e02c2025-07-31 00:41:49 +0000239 ${toolFailed
Josh Bleecher Snyder10bd3a62025-07-30 23:55:50 +0000240 ? `${patchInput?.path}: failed`
Autoformatter2c3e02c2025-07-31 00:41:49 +0000241 : `${patchInput?.path}: ${patchInput.patches.length} edit${patchInput.patches.length > 1 ? "s" : ""}`}
banksean333aa672025-07-13 19:49:21 +0000242 </span>`;
243
Josh Bleecher Snyder3dd3e412025-07-22 20:32:03 -0700244 const inputContent = html``;
banksean333aa672025-07-13 19:49:21 +0000245
Josh Bleecher Snyder3dd3e412025-07-22 20:32:03 -0700246 // Show diff if available, otherwise show the regular result
247 let resultContent;
248 if (
249 this.toolCall?.result_message?.display &&
250 typeof this.toolCall.result_message.display === "string"
251 ) {
252 // Render the diff with syntax highlighting
253 resultContent = html`<div class="w-full relative">
254 ${this.renderDiff(this.toolCall.result_message.display)}
255 </div>`;
256 } else if (this.toolCall?.result_message?.tool_result) {
257 resultContent = createPreElement(
258 this.toolCall.result_message.tool_result,
259 );
260 } else {
261 resultContent = "";
262 }
banksean333aa672025-07-13 19:49:21 +0000263
264 return html`<sketch-tool-card-base
265 .open=${this.open}
266 .toolCall=${this.toolCall}
267 .summaryContent=${summaryContent}
268 .inputContent=${inputContent}
269 .resultContent=${resultContent}
270 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100271 }
272}
273
274@customElement("sketch-tool-card-think")
banksean333aa672025-07-13 19:49:21 +0000275export class SketchToolCardThink extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100276 @property() toolCall: ToolCall;
277 @property() open: boolean;
278
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100279 render() {
banksean333aa672025-07-13 19:49:21 +0000280 const thoughts = JSON.parse(this.toolCall?.input)?.thoughts || "";
281
282 const summaryContent = html`<span
283 class="overflow-hidden text-ellipsis font-mono"
284 >
285 ${thoughts.split("\n")[0]}
286 </span>`;
287
288 const inputContent = html`<div
banksean3d1308e2025-07-29 17:20:10 +0000289 class="overflow-x-auto mb-1 font-mono px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded select-text cursor-text text-sm leading-relaxed text-gray-900 dark:text-gray-100"
banksean333aa672025-07-13 19:49:21 +0000290 >
291 <div class="markdown-content">
292 ${unsafeHTML(renderMarkdown(thoughts))}
293 </div>
294 </div>`;
295
296 return html`<sketch-tool-card-base
297 .open=${this.open}
298 .toolCall=${this.toolCall}
299 .summaryContent=${summaryContent}
300 .inputContent=${inputContent}
301 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100302 }
303}
304
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700305@customElement("sketch-tool-card-todo-write")
banksean333aa672025-07-13 19:49:21 +0000306export class SketchToolCardTodoWrite extends SketchTailwindElement {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700307 @property() toolCall: ToolCall;
308 @property() open: boolean;
309
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700310 render() {
311 const inputData = JSON.parse(this.toolCall?.input || "{}");
312 const tasks = inputData.tasks || [];
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000313
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700314 // Generate circles based on task status
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000315 const circles = tasks
316 .map((task) => {
317 switch (task.status) {
318 case "completed":
319 return "●"; // full circle
320 case "in-progress":
321 return "◐"; // half circle
322 case "queued":
323 default:
324 return "○"; // empty circle
325 }
326 })
327 .join(" ");
328
banksean333aa672025-07-13 19:49:21 +0000329 const summaryContent = html`<span class="italic text-gray-600">
330 ${circles}
331 </span>`;
332 const resultContent = this.toolCall?.result_message?.tool_result
333 ? createPreElement(this.toolCall.result_message.tool_result)
334 : "";
335
336 return html`<sketch-tool-card-base
337 .open=${this.open}
338 .toolCall=${this.toolCall}
339 .summaryContent=${summaryContent}
340 .resultContent=${resultContent}
341 ></sketch-tool-card-base>`;
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000342 }
343}
344
345@customElement("sketch-tool-card-keyword-search")
banksean333aa672025-07-13 19:49:21 +0000346export class SketchToolCardKeywordSearch extends SketchTailwindElement {
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000347 @property() toolCall: ToolCall;
348 @property() open: boolean;
349
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000350 render() {
351 const inputData = JSON.parse(this.toolCall?.input || "{}");
352 const query = inputData.query || "";
353 const searchTerms = inputData.search_terms || [];
354
banksean333aa672025-07-13 19:49:21 +0000355 const summaryContent = html`<div
356 class="flex flex-col gap-0.5 w-full max-w-full overflow-hidden"
357 >
358 <div
359 class="text-gray-800 text-xs normal-case whitespace-normal break-words leading-tight"
360 >
361 🔍 ${query}
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000362 </div>
banksean333aa672025-07-13 19:49:21 +0000363 <div
364 class="text-gray-600 text-xs normal-case whitespace-normal break-words leading-tight mt-px"
365 >
366 🗝️ ${searchTerms.join(", ")}
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000367 </div>
banksean333aa672025-07-13 19:49:21 +0000368 </div>`;
369
370 const inputContent = html`<div>
371 <div><strong>Query:</strong> ${query}</div>
372 <div><strong>Search terms:</strong> ${searchTerms.join(", ")}</div>
373 </div>`;
374
375 const resultContent = this.toolCall?.result_message?.tool_result
376 ? createPreElement(this.toolCall.result_message.tool_result)
377 : "";
378
379 return html`<sketch-tool-card-base
380 .open=${this.open}
381 .toolCall=${this.toolCall}
382 .summaryContent=${summaryContent}
383 .inputContent=${inputContent}
384 .resultContent=${resultContent}
385 ></sketch-tool-card-base>`;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700386 }
387}
388
389@customElement("sketch-tool-card-todo-read")
banksean333aa672025-07-13 19:49:21 +0000390export class SketchToolCardTodoRead extends SketchTailwindElement {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700391 @property() toolCall: ToolCall;
392 @property() open: boolean;
393
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700394 render() {
banksean333aa672025-07-13 19:49:21 +0000395 const summaryContent = html`<span class="italic text-gray-600">
396 Read todo list
397 </span>`;
398 const resultContent = this.toolCall?.result_message?.tool_result
399 ? createPreElement(this.toolCall.result_message.tool_result)
400 : "";
401
402 return html`<sketch-tool-card-base
403 .open=${this.open}
404 .toolCall=${this.toolCall}
405 .summaryContent=${summaryContent}
406 .resultContent=${resultContent}
407 ></sketch-tool-card-base>`;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700408 }
409}
410
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100411@customElement("sketch-tool-card-generic")
banksean333aa672025-07-13 19:49:21 +0000412export class SketchToolCardGeneric extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100413 @property() toolCall: ToolCall;
414 @property() open: boolean;
415
416 render() {
banksean333aa672025-07-13 19:49:21 +0000417 const summaryContent = html`<span
418 class="block whitespace-normal break-words max-w-full w-full"
419 >
420 ${this.toolCall?.input}
421 </span>`;
422
423 const inputContent = html`<div class="max-w-full break-words">
424 Input:
425 ${createPreElement(
426 this.toolCall?.input || "",
427 "max-w-full whitespace-pre-wrap break-words",
428 )}
429 </div>`;
430
431 const resultContent = this.toolCall?.result_message?.tool_result
432 ? html`<div class="max-w-full break-words">
433 Result: ${createPreElement(this.toolCall.result_message.tool_result)}
434 </div>`
435 : "";
436
437 return html`<sketch-tool-card-base
438 .open=${this.open}
439 .toolCall=${this.toolCall}
440 .summaryContent=${summaryContent}
441 .inputContent=${inputContent}
442 .resultContent=${resultContent}
443 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100444 }
445}
446
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700447declare global {
448 interface HTMLElementTagNameMap {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100449 "sketch-tool-card-generic": SketchToolCardGeneric;
450 "sketch-tool-card-bash": SketchToolCardBash;
451 "sketch-tool-card-codereview": SketchToolCardCodeReview;
452 "sketch-tool-card-done": SketchToolCardDone;
453 "sketch-tool-card-patch": SketchToolCardPatch;
454 "sketch-tool-card-think": SketchToolCardThink;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700455 "sketch-tool-card-todo-write": SketchToolCardTodoWrite;
456 "sketch-tool-card-todo-read": SketchToolCardTodoRead;
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000457 "sketch-tool-card-keyword-search": SketchToolCardKeywordSearch;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700458 }
459}