blob: 72d4a48b93344eaf533512689be0bd52e633ae21 [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;
86 const backgroundIcon = isBackground ? "🥷 " : "";
87 const slowIcon = isSlowOk ? "🐢 " : "";
Pokey Rule7ac5ed02025-05-07 15:26:10 +010088
Philip Zeyligere31d2a92025-05-11 15:22:35 -070089 // Truncate the command if it's too long to display nicely
90 const command = inputData?.command || "";
91 const displayCommand =
92 command.length > 80 ? command.substring(0, 80) + "..." : command;
93
banksean333aa672025-07-13 19:49:21 +000094 const summaryContent = html`<div
95 class="max-w-full overflow-hidden text-ellipsis whitespace-nowrap"
96 >
97 ${backgroundIcon}${slowIcon}${displayCommand}
98 </div>`;
99
100 const inputContent = html`<div
101 class="flex w-full max-w-full flex-col overflow-wrap-break-word break-words"
102 >
103 <div class="w-full relative">
104 ${createPreElement(
105 `${backgroundIcon}${slowIcon}${inputData?.command}`,
106 "w-full mb-0 rounded-t rounded-b-none box-border",
107 )}
108 </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
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100202 render() {
203 const patchInput = JSON.parse(this.toolCall?.input);
banksean333aa672025-07-13 19:49:21 +0000204
205 const summaryContent = html`<span
206 class="text-gray-600 font-mono overflow-hidden text-ellipsis whitespace-nowrap rounded"
207 >
208 ${patchInput?.path}: ${patchInput.patches.length}
209 edit${patchInput.patches.length > 1 ? "s" : ""}
210 </span>`;
211
212 const inputContent = html`<div>
213 ${patchInput.patches.map((patch) => {
214 return html`<div class="mb-2">
215 Patch operation: <b>${patch.operation}</b>
216 ${createPreElement(patch.newText)}
217 </div>`;
218 })}
219 </div>`;
220
221 const resultContent = this.toolCall?.result_message?.tool_result
222 ? createPreElement(this.toolCall.result_message.tool_result)
223 : "";
224
225 return html`<sketch-tool-card-base
226 .open=${this.open}
227 .toolCall=${this.toolCall}
228 .summaryContent=${summaryContent}
229 .inputContent=${inputContent}
230 .resultContent=${resultContent}
231 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100232 }
233}
234
235@customElement("sketch-tool-card-think")
banksean333aa672025-07-13 19:49:21 +0000236export class SketchToolCardThink extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100237 @property() toolCall: ToolCall;
238 @property() open: boolean;
239
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100240 render() {
banksean333aa672025-07-13 19:49:21 +0000241 const thoughts = JSON.parse(this.toolCall?.input)?.thoughts || "";
242
243 const summaryContent = html`<span
244 class="overflow-hidden text-ellipsis font-mono"
245 >
246 ${thoughts.split("\n")[0]}
247 </span>`;
248
249 const inputContent = html`<div
250 class="overflow-x-auto mb-1 font-mono px-2 py-1 bg-gray-200 rounded select-text cursor-text text-sm leading-relaxed"
251 >
252 <div class="markdown-content">
253 ${unsafeHTML(renderMarkdown(thoughts))}
254 </div>
255 </div>`;
256
257 return html`<sketch-tool-card-base
258 .open=${this.open}
259 .toolCall=${this.toolCall}
260 .summaryContent=${summaryContent}
261 .inputContent=${inputContent}
262 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100263 }
264}
265
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700266@customElement("sketch-tool-card-set-slug")
banksean333aa672025-07-13 19:49:21 +0000267export class SketchToolCardSetSlug extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100268 @property() toolCall: ToolCall;
269 @property() open: boolean;
270
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100271 render() {
272 const inputData = JSON.parse(this.toolCall?.input || "{}");
banksean333aa672025-07-13 19:49:21 +0000273
274 const summaryContent = html`<span class="italic">
275 Slug: "${inputData.slug}"
276 </span>`;
277
278 const inputContent = html`<div>Set slug to: <b>${inputData.slug}</b></div>`;
279
280 return html`<sketch-tool-card-base
281 .open=${this.open}
282 .toolCall=${this.toolCall}
283 .summaryContent=${summaryContent}
284 .inputContent=${inputContent}
285 ></sketch-tool-card-base>`;
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000286 }
287}
288
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700289@customElement("sketch-tool-card-commit-message-style")
banksean333aa672025-07-13 19:49:21 +0000290export class SketchToolCardCommitMessageStyle extends SketchTailwindElement {
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000291 @property()
292 toolCall: ToolCall;
293
294 @property()
295 open: boolean;
296
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000297 @property()
298 state: State;
299
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000300 constructor() {
301 super();
302 }
303
304 connectedCallback() {
305 super.connectedCallback();
306 }
307
308 disconnectedCallback() {
309 super.disconnectedCallback();
310 }
311
312 render() {
banksean333aa672025-07-13 19:49:21 +0000313 return html`<sketch-tool-card-base
314 .open=${this.open}
315 .toolCall=${this.toolCall}
316 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100317 }
318}
319
320@customElement("sketch-tool-card-multiple-choice")
banksean333aa672025-07-13 19:49:21 +0000321export class SketchToolCardMultipleChoice extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100322 @property() toolCall: ToolCall;
323 @property() open: boolean;
324 @property() selectedOption: MultipleChoiceOption = null;
325
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100326 connectedCallback() {
327 super.connectedCallback();
328 this.updateSelectedOption();
329 }
330
331 updated(changedProps) {
332 if (changedProps.has("toolCall")) {
333 this.updateSelectedOption();
334 }
335 }
336
337 updateSelectedOption() {
338 if (this.toolCall?.result_message?.tool_result) {
339 try {
340 this.selectedOption = JSON.parse(
341 this.toolCall.result_message.tool_result,
342 ).selected;
343 } catch (e) {
344 console.error("Error parsing result:", e);
345 }
346 } else {
347 this.selectedOption = null;
348 }
349 }
350
351 async handleOptionClick(choice) {
352 this.selectedOption = this.selectedOption === choice ? null : choice;
353
354 const event = new CustomEvent("multiple-choice-selected", {
355 detail: {
356 responseText: this.selectedOption.responseText,
357 toolCall: this.toolCall,
358 },
359 bubbles: true,
360 composed: true,
361 });
362 this.dispatchEvent(event);
363 }
364
365 render() {
366 let choices = [];
367 let question = "";
368 try {
369 const inputData = JSON.parse(
370 this.toolCall?.input || "{}",
371 ) as MultipleChoiceParams;
372 choices = inputData.responseOptions || [];
373 question = inputData.question || "Please select an option:";
374 } catch (e) {
375 console.error("Error parsing multiple-choice input:", e);
376 }
377
378 const summaryContent =
379 this.selectedOption !== null
banksean333aa672025-07-13 19:49:21 +0000380 ? html`<span class="italic p-2">
381 ${question}:
382 <strong class="not-italic text-blue-600 font-semibold"
383 >${this.selectedOption.caption}</strong
384 >
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100385 </span>`
banksean333aa672025-07-13 19:49:21 +0000386 : html`<span class="italic p-2">${question}</span>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100387
banksean333aa672025-07-13 19:49:21 +0000388 const inputContent = html`<div class="flex flex-row flex-wrap gap-2 my-2">
389 ${choices.map((choice) => {
390 const isSelected =
391 this.selectedOption !== null && this.selectedOption === choice;
392 return html`
393 <div
394 class="inline-flex items-center px-3 py-2 rounded cursor-pointer transition-all duration-200 border select-none ${isSelected
395 ? "bg-blue-50 border-blue-500"
396 : "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"}"
397 @click=${() => this.handleOptionClick(choice)}
398 title="${choice.responseText}"
399 >
400 <span class="option-label">${choice.caption}</span>
401 ${isSelected
402 ? html`<span class="ml-1.5 text-blue-600">✓</span>`
403 : ""}
404 </div>
405 `;
406 })}
407 </div>`;
408
409 return html`<div class="multiple-choice-card">
410 ${summaryContent} ${inputContent}
411 </div>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100412 }
413}
414
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700415@customElement("sketch-tool-card-todo-write")
banksean333aa672025-07-13 19:49:21 +0000416export class SketchToolCardTodoWrite extends SketchTailwindElement {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700417 @property() toolCall: ToolCall;
418 @property() open: boolean;
419
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700420 render() {
421 const inputData = JSON.parse(this.toolCall?.input || "{}");
422 const tasks = inputData.tasks || [];
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000423
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700424 // Generate circles based on task status
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000425 const circles = tasks
426 .map((task) => {
427 switch (task.status) {
428 case "completed":
429 return "●"; // full circle
430 case "in-progress":
431 return "◐"; // half circle
432 case "queued":
433 default:
434 return "○"; // empty circle
435 }
436 })
437 .join(" ");
438
banksean333aa672025-07-13 19:49:21 +0000439 const summaryContent = html`<span class="italic text-gray-600">
440 ${circles}
441 </span>`;
442 const resultContent = this.toolCall?.result_message?.tool_result
443 ? createPreElement(this.toolCall.result_message.tool_result)
444 : "";
445
446 return html`<sketch-tool-card-base
447 .open=${this.open}
448 .toolCall=${this.toolCall}
449 .summaryContent=${summaryContent}
450 .resultContent=${resultContent}
451 ></sketch-tool-card-base>`;
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000452 }
453}
454
455@customElement("sketch-tool-card-keyword-search")
banksean333aa672025-07-13 19:49:21 +0000456export class SketchToolCardKeywordSearch extends SketchTailwindElement {
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000457 @property() toolCall: ToolCall;
458 @property() open: boolean;
459
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000460 render() {
461 const inputData = JSON.parse(this.toolCall?.input || "{}");
462 const query = inputData.query || "";
463 const searchTerms = inputData.search_terms || [];
464
banksean333aa672025-07-13 19:49:21 +0000465 const summaryContent = html`<div
466 class="flex flex-col gap-0.5 w-full max-w-full overflow-hidden"
467 >
468 <div
469 class="text-gray-800 text-xs normal-case whitespace-normal break-words leading-tight"
470 >
471 🔍 ${query}
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000472 </div>
banksean333aa672025-07-13 19:49:21 +0000473 <div
474 class="text-gray-600 text-xs normal-case whitespace-normal break-words leading-tight mt-px"
475 >
476 🗝️ ${searchTerms.join(", ")}
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000477 </div>
banksean333aa672025-07-13 19:49:21 +0000478 </div>`;
479
480 const inputContent = html`<div>
481 <div><strong>Query:</strong> ${query}</div>
482 <div><strong>Search terms:</strong> ${searchTerms.join(", ")}</div>
483 </div>`;
484
485 const resultContent = this.toolCall?.result_message?.tool_result
486 ? createPreElement(this.toolCall.result_message.tool_result)
487 : "";
488
489 return html`<sketch-tool-card-base
490 .open=${this.open}
491 .toolCall=${this.toolCall}
492 .summaryContent=${summaryContent}
493 .inputContent=${inputContent}
494 .resultContent=${resultContent}
495 ></sketch-tool-card-base>`;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700496 }
497}
498
499@customElement("sketch-tool-card-todo-read")
banksean333aa672025-07-13 19:49:21 +0000500export class SketchToolCardTodoRead extends SketchTailwindElement {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700501 @property() toolCall: ToolCall;
502 @property() open: boolean;
503
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700504 render() {
banksean333aa672025-07-13 19:49:21 +0000505 const summaryContent = html`<span class="italic text-gray-600">
506 Read todo list
507 </span>`;
508 const resultContent = this.toolCall?.result_message?.tool_result
509 ? createPreElement(this.toolCall.result_message.tool_result)
510 : "";
511
512 return html`<sketch-tool-card-base
513 .open=${this.open}
514 .toolCall=${this.toolCall}
515 .summaryContent=${summaryContent}
516 .resultContent=${resultContent}
517 ></sketch-tool-card-base>`;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700518 }
519}
520
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100521@customElement("sketch-tool-card-generic")
banksean333aa672025-07-13 19:49:21 +0000522export class SketchToolCardGeneric extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100523 @property() toolCall: ToolCall;
524 @property() open: boolean;
525
526 render() {
banksean333aa672025-07-13 19:49:21 +0000527 const summaryContent = html`<span
528 class="block whitespace-normal break-words max-w-full w-full"
529 >
530 ${this.toolCall?.input}
531 </span>`;
532
533 const inputContent = html`<div class="max-w-full break-words">
534 Input:
535 ${createPreElement(
536 this.toolCall?.input || "",
537 "max-w-full whitespace-pre-wrap break-words",
538 )}
539 </div>`;
540
541 const resultContent = this.toolCall?.result_message?.tool_result
542 ? html`<div class="max-w-full break-words">
543 Result: ${createPreElement(this.toolCall.result_message.tool_result)}
544 </div>`
545 : "";
546
547 return html`<sketch-tool-card-base
548 .open=${this.open}
549 .toolCall=${this.toolCall}
550 .summaryContent=${summaryContent}
551 .inputContent=${inputContent}
552 .resultContent=${resultContent}
553 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100554 }
555}
556
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700557declare global {
558 interface HTMLElementTagNameMap {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100559 "sketch-tool-card-generic": SketchToolCardGeneric;
560 "sketch-tool-card-bash": SketchToolCardBash;
561 "sketch-tool-card-codereview": SketchToolCardCodeReview;
562 "sketch-tool-card-done": SketchToolCardDone;
563 "sketch-tool-card-patch": SketchToolCardPatch;
564 "sketch-tool-card-think": SketchToolCardThink;
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700565 "sketch-tool-card-set-slug": SketchToolCardSetSlug;
566 "sketch-tool-card-commit-message-style": SketchToolCardCommitMessageStyle;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100567 "sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700568 "sketch-tool-card-todo-write": SketchToolCardTodoWrite;
569 "sketch-tool-card-todo-read": SketchToolCardTodoRead;
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000570 "sketch-tool-card-keyword-search": SketchToolCardKeywordSearch;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700571 }
572}