blob: 0c6bb1e17467ad58ee8e627621b0ad891286a401 [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,
Autoformatter7e5fe3c2025-06-04 22:24:53 +00006 State,
7} from "../types";
philip.zeyliger26bc6592025-06-30 20:15:30 -07008import { marked } from "marked";
Philip Zeyliger53ab2452025-06-04 17:49:33 +00009import DOMPurify from "dompurify";
banksean333aa672025-07-13 19:49:21 +000010import { SketchTailwindElement } from "./sketch-tailwind-element";
11import "./sketch-tool-card-base";
Pokey Rule7ac5ed02025-05-07 15:26:10 +010012
Philip Zeyliger53ab2452025-06-04 17:49:33 +000013// Shared utility function for markdown rendering with DOMPurify sanitization
Pokey Rule7ac5ed02025-05-07 15:26:10 +010014function renderMarkdown(markdownContent: string): string {
15 try {
Philip Zeyliger53ab2452025-06-04 17:49:33 +000016 // Parse markdown with default settings
17 const htmlOutput = marked.parse(markdownContent, {
Pokey Rule7ac5ed02025-05-07 15:26:10 +010018 gfm: true,
19 breaks: true,
20 async: false,
21 }) as string;
Philip Zeyliger53ab2452025-06-04 17:49:33 +000022
23 // Sanitize the output HTML with DOMPurify
24 return DOMPurify.sanitize(htmlOutput, {
25 // Allow common safe HTML elements
26 ALLOWED_TAGS: [
27 "p",
28 "br",
29 "strong",
30 "em",
31 "b",
32 "i",
33 "u",
34 "s",
35 "code",
36 "pre",
37 "h1",
38 "h2",
39 "h3",
40 "h4",
41 "h5",
42 "h6",
43 "ul",
44 "ol",
45 "li",
46 "blockquote",
47 "a",
48 ],
49 ALLOWED_ATTR: [
50 "href",
51 "title",
52 "target",
53 "rel", // For links
54 "class", // For basic styling
55 ],
56 // Keep content formatting
57 KEEP_CONTENT: true,
58 });
Pokey Rule7ac5ed02025-05-07 15:26:10 +010059 } catch (error) {
60 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +000061 // Fallback to sanitized plain text if markdown parsing fails
62 return DOMPurify.sanitize(markdownContent);
Pokey Rule7ac5ed02025-05-07 15:26:10 +010063 }
64}
65
banksean333aa672025-07-13 19:49:21 +000066// Shared utility function for creating Tailwind pre elements
67function createPreElement(content: string, additionalClasses: string = "") {
68 return html`<pre
69 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}"
70 >
71${content}</pre
72 >`;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070073}
74
Pokey Rule7ac5ed02025-05-07 15:26:10 +010075@customElement("sketch-tool-card-bash")
banksean333aa672025-07-13 19:49:21 +000076export class SketchToolCardBash extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +010077 @property() toolCall: ToolCall;
78 @property() open: boolean;
79
Pokey Rule7ac5ed02025-05-07 15:26:10 +010080 render() {
81 const inputData = JSON.parse(this.toolCall?.input || "{}");
82 const isBackground = inputData?.background === true;
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000083 const isSlowOk = inputData?.slow_ok === true;
Autoformatter9d9c8122025-07-18 03:37:23 +000084 const backgroundIcon = isBackground
85 ? html`<span title="Running in background">🥷</span> `
86 : "";
87 const slowIcon = isSlowOk
88 ? html`<span title="Extended timeouts">🐢</span> `
89 : "";
Pokey Rule7ac5ed02025-05-07 15:26:10 +010090
Philip Zeyligere31d2a92025-05-11 15:22:35 -070091 // Truncate the command if it's too long to display nicely
92 const command = inputData?.command || "";
93 const displayCommand =
94 command.length > 80 ? command.substring(0, 80) + "..." : command;
95
banksean333aa672025-07-13 19:49:21 +000096 const summaryContent = html`<div
97 class="max-w-full overflow-hidden text-ellipsis whitespace-nowrap"
98 >
99 ${backgroundIcon}${slowIcon}${displayCommand}
100 </div>`;
101
102 const inputContent = html`<div
103 class="flex w-full max-w-full flex-col overflow-wrap-break-word break-words"
104 >
105 <div class="w-full relative">
Josh Bleecher Snyder45943882025-07-18 02:13:31 +0000106 <pre
107 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"
108 >
Autoformatter9d9c8122025-07-18 03:37:23 +0000109${backgroundIcon}${slowIcon}${inputData?.command}</pre
110 >
banksean333aa672025-07-13 19:49:21 +0000111 </div>
112 </div>`;
113
114 const resultContent = this.toolCall?.result_message?.tool_result
115 ? html`<div class="w-full relative">
116 ${createPreElement(
117 this.toolCall.result_message.tool_result,
118 "mt-0 text-gray-600 rounded-t-none rounded-b w-full box-border max-h-[300px] overflow-y-auto",
119 )}
120 </div>`
121 : "";
122
123 return html`<sketch-tool-card-base
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100124 .open=${this.open}
125 .toolCall=${this.toolCall}
banksean333aa672025-07-13 19:49:21 +0000126 .summaryContent=${summaryContent}
127 .inputContent=${inputContent}
128 .resultContent=${resultContent}
129 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100130 }
131}
132
133@customElement("sketch-tool-card-codereview")
banksean333aa672025-07-13 19:49:21 +0000134export class SketchToolCardCodeReview extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100135 @property() toolCall: ToolCall;
136 @property() open: boolean;
137
138 // Determine the status icon based on the content of the result message
139 getStatusIcon(resultText: string): string {
140 if (!resultText) return "";
141 if (resultText === "OK") return "✔️";
142 if (resultText.includes("# Errors")) return "⚠️";
143 if (resultText.includes("# Info")) return "ℹ️";
144 if (resultText.includes("uncommitted changes in repo")) return "🧹";
145 if (resultText.includes("no new commits have been added")) return "🐣";
146 if (resultText.includes("git repo is not clean")) return "🧼";
147 return "❓";
148 }
149
150 render() {
151 const resultText = this.toolCall?.result_message?.tool_result || "";
152 const statusIcon = this.getStatusIcon(resultText);
153
banksean333aa672025-07-13 19:49:21 +0000154 const summaryContent = html`<span>${statusIcon}</span>`;
155 const resultContent = resultText ? createPreElement(resultText) : "";
156
157 return html`<sketch-tool-card-base
158 .open=${this.open}
159 .toolCall=${this.toolCall}
160 .summaryContent=${summaryContent}
161 .resultContent=${resultContent}
162 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100163 }
164}
165
166@customElement("sketch-tool-card-done")
banksean333aa672025-07-13 19:49:21 +0000167export class SketchToolCardDone extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100168 @property() toolCall: ToolCall;
169 @property() open: boolean;
170
171 render() {
172 const doneInput = JSON.parse(this.toolCall.input);
banksean333aa672025-07-13 19:49:21 +0000173
174 const summaryContent = html`<span></span>`;
175
176 const resultContent = html`<div>
177 ${Object.keys(doneInput.checklist_items).map((key) => {
178 const item = doneInput.checklist_items[key];
179 let statusIcon = "〰️";
180 if (item.status == "yes") {
181 statusIcon = "✅";
182 } else if (item.status == "not applicable") {
183 statusIcon = "🤷";
184 }
185 return html`<div class="mb-1">
186 <span>${statusIcon}</span> ${key}:${item.status}
187 </div>`;
188 })}
189 </div>`;
190
191 return html`<sketch-tool-card-base
192 .open=${this.open}
193 .toolCall=${this.toolCall}
194 .summaryContent=${summaryContent}
195 .resultContent=${resultContent}
196 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100197 }
198}
199
200@customElement("sketch-tool-card-patch")
banksean333aa672025-07-13 19:49:21 +0000201export class SketchToolCardPatch extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100202 @property() toolCall: ToolCall;
203 @property() open: boolean;
204
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100205 render() {
206 const patchInput = JSON.parse(this.toolCall?.input);
banksean333aa672025-07-13 19:49:21 +0000207
208 const summaryContent = html`<span
209 class="text-gray-600 font-mono overflow-hidden text-ellipsis whitespace-nowrap rounded"
210 >
211 ${patchInput?.path}: ${patchInput.patches.length}
212 edit${patchInput.patches.length > 1 ? "s" : ""}
213 </span>`;
214
215 const inputContent = html`<div>
216 ${patchInput.patches.map((patch) => {
217 return html`<div class="mb-2">
218 Patch operation: <b>${patch.operation}</b>
219 ${createPreElement(patch.newText)}
220 </div>`;
221 })}
222 </div>`;
223
224 const resultContent = this.toolCall?.result_message?.tool_result
225 ? createPreElement(this.toolCall.result_message.tool_result)
226 : "";
227
228 return html`<sketch-tool-card-base
229 .open=${this.open}
230 .toolCall=${this.toolCall}
231 .summaryContent=${summaryContent}
232 .inputContent=${inputContent}
233 .resultContent=${resultContent}
234 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100235 }
236}
237
238@customElement("sketch-tool-card-think")
banksean333aa672025-07-13 19:49:21 +0000239export class SketchToolCardThink extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100240 @property() toolCall: ToolCall;
241 @property() open: boolean;
242
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100243 render() {
banksean333aa672025-07-13 19:49:21 +0000244 const thoughts = JSON.parse(this.toolCall?.input)?.thoughts || "";
245
246 const summaryContent = html`<span
247 class="overflow-hidden text-ellipsis font-mono"
248 >
249 ${thoughts.split("\n")[0]}
250 </span>`;
251
252 const inputContent = html`<div
253 class="overflow-x-auto mb-1 font-mono px-2 py-1 bg-gray-200 rounded select-text cursor-text text-sm leading-relaxed"
254 >
255 <div class="markdown-content">
256 ${unsafeHTML(renderMarkdown(thoughts))}
257 </div>
258 </div>`;
259
260 return html`<sketch-tool-card-base
261 .open=${this.open}
262 .toolCall=${this.toolCall}
263 .summaryContent=${summaryContent}
264 .inputContent=${inputContent}
265 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100266 }
267}
268
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700269@customElement("sketch-tool-card-commit-message-style")
banksean333aa672025-07-13 19:49:21 +0000270export class SketchToolCardCommitMessageStyle extends SketchTailwindElement {
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000271 @property()
272 toolCall: ToolCall;
273
274 @property()
275 open: boolean;
276
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000277 @property()
278 state: State;
279
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000280 constructor() {
281 super();
282 }
283
284 connectedCallback() {
285 super.connectedCallback();
286 }
287
288 disconnectedCallback() {
289 super.disconnectedCallback();
290 }
291
292 render() {
banksean333aa672025-07-13 19:49:21 +0000293 return html`<sketch-tool-card-base
294 .open=${this.open}
295 .toolCall=${this.toolCall}
296 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100297 }
298}
299
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100300
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700301@customElement("sketch-tool-card-todo-write")
banksean333aa672025-07-13 19:49:21 +0000302export class SketchToolCardTodoWrite extends SketchTailwindElement {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700303 @property() toolCall: ToolCall;
304 @property() open: boolean;
305
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700306 render() {
307 const inputData = JSON.parse(this.toolCall?.input || "{}");
308 const tasks = inputData.tasks || [];
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000309
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700310 // Generate circles based on task status
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000311 const circles = tasks
312 .map((task) => {
313 switch (task.status) {
314 case "completed":
315 return "●"; // full circle
316 case "in-progress":
317 return "◐"; // half circle
318 case "queued":
319 default:
320 return "○"; // empty circle
321 }
322 })
323 .join(" ");
324
banksean333aa672025-07-13 19:49:21 +0000325 const summaryContent = html`<span class="italic text-gray-600">
326 ${circles}
327 </span>`;
328 const resultContent = this.toolCall?.result_message?.tool_result
329 ? createPreElement(this.toolCall.result_message.tool_result)
330 : "";
331
332 return html`<sketch-tool-card-base
333 .open=${this.open}
334 .toolCall=${this.toolCall}
335 .summaryContent=${summaryContent}
336 .resultContent=${resultContent}
337 ></sketch-tool-card-base>`;
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000338 }
339}
340
341@customElement("sketch-tool-card-keyword-search")
banksean333aa672025-07-13 19:49:21 +0000342export class SketchToolCardKeywordSearch extends SketchTailwindElement {
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000343 @property() toolCall: ToolCall;
344 @property() open: boolean;
345
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000346 render() {
347 const inputData = JSON.parse(this.toolCall?.input || "{}");
348 const query = inputData.query || "";
349 const searchTerms = inputData.search_terms || [];
350
banksean333aa672025-07-13 19:49:21 +0000351 const summaryContent = html`<div
352 class="flex flex-col gap-0.5 w-full max-w-full overflow-hidden"
353 >
354 <div
355 class="text-gray-800 text-xs normal-case whitespace-normal break-words leading-tight"
356 >
357 🔍 ${query}
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000358 </div>
banksean333aa672025-07-13 19:49:21 +0000359 <div
360 class="text-gray-600 text-xs normal-case whitespace-normal break-words leading-tight mt-px"
361 >
362 🗝️ ${searchTerms.join(", ")}
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000363 </div>
banksean333aa672025-07-13 19:49:21 +0000364 </div>`;
365
366 const inputContent = html`<div>
367 <div><strong>Query:</strong> ${query}</div>
368 <div><strong>Search terms:</strong> ${searchTerms.join(", ")}</div>
369 </div>`;
370
371 const resultContent = this.toolCall?.result_message?.tool_result
372 ? createPreElement(this.toolCall.result_message.tool_result)
373 : "";
374
375 return html`<sketch-tool-card-base
376 .open=${this.open}
377 .toolCall=${this.toolCall}
378 .summaryContent=${summaryContent}
379 .inputContent=${inputContent}
380 .resultContent=${resultContent}
381 ></sketch-tool-card-base>`;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700382 }
383}
384
385@customElement("sketch-tool-card-todo-read")
banksean333aa672025-07-13 19:49:21 +0000386export class SketchToolCardTodoRead extends SketchTailwindElement {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700387 @property() toolCall: ToolCall;
388 @property() open: boolean;
389
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700390 render() {
banksean333aa672025-07-13 19:49:21 +0000391 const summaryContent = html`<span class="italic text-gray-600">
392 Read todo list
393 </span>`;
394 const resultContent = this.toolCall?.result_message?.tool_result
395 ? createPreElement(this.toolCall.result_message.tool_result)
396 : "";
397
398 return html`<sketch-tool-card-base
399 .open=${this.open}
400 .toolCall=${this.toolCall}
401 .summaryContent=${summaryContent}
402 .resultContent=${resultContent}
403 ></sketch-tool-card-base>`;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700404 }
405}
406
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100407@customElement("sketch-tool-card-generic")
banksean333aa672025-07-13 19:49:21 +0000408export class SketchToolCardGeneric extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100409 @property() toolCall: ToolCall;
410 @property() open: boolean;
411
412 render() {
banksean333aa672025-07-13 19:49:21 +0000413 const summaryContent = html`<span
414 class="block whitespace-normal break-words max-w-full w-full"
415 >
416 ${this.toolCall?.input}
417 </span>`;
418
419 const inputContent = html`<div class="max-w-full break-words">
420 Input:
421 ${createPreElement(
422 this.toolCall?.input || "",
423 "max-w-full whitespace-pre-wrap break-words",
424 )}
425 </div>`;
426
427 const resultContent = this.toolCall?.result_message?.tool_result
428 ? html`<div class="max-w-full break-words">
429 Result: ${createPreElement(this.toolCall.result_message.tool_result)}
430 </div>`
431 : "";
432
433 return html`<sketch-tool-card-base
434 .open=${this.open}
435 .toolCall=${this.toolCall}
436 .summaryContent=${summaryContent}
437 .inputContent=${inputContent}
438 .resultContent=${resultContent}
439 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100440 }
441}
442
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700443declare global {
444 interface HTMLElementTagNameMap {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100445 "sketch-tool-card-generic": SketchToolCardGeneric;
446 "sketch-tool-card-bash": SketchToolCardBash;
447 "sketch-tool-card-codereview": SketchToolCardCodeReview;
448 "sketch-tool-card-done": SketchToolCardDone;
449 "sketch-tool-card-patch": SketchToolCardPatch;
450 "sketch-tool-card-think": SketchToolCardThink;
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700451 "sketch-tool-card-commit-message-style": SketchToolCardCommitMessageStyle;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700452 "sketch-tool-card-todo-write": SketchToolCardTodoWrite;
453 "sketch-tool-card-todo-read": SketchToolCardTodoRead;
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000454 "sketch-tool-card-keyword-search": SketchToolCardKeywordSearch;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700455 }
456}