blob: 5954e24aaf51422387937244fe1afb338dda9fd9 [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-set-slug")
banksean333aa672025-07-13 19:49:21 +0000272export class SketchToolCardSetSlug extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100273 @property() toolCall: ToolCall;
274 @property() open: boolean;
275
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100276 render() {
277 const inputData = JSON.parse(this.toolCall?.input || "{}");
banksean333aa672025-07-13 19:49:21 +0000278
279 const summaryContent = html`<span class="italic">
280 Slug: "${inputData.slug}"
281 </span>`;
282
283 const inputContent = html`<div>Set slug to: <b>${inputData.slug}</b></div>`;
284
285 return html`<sketch-tool-card-base
286 .open=${this.open}
287 .toolCall=${this.toolCall}
288 .summaryContent=${summaryContent}
289 .inputContent=${inputContent}
290 ></sketch-tool-card-base>`;
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000291 }
292}
293
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700294@customElement("sketch-tool-card-commit-message-style")
banksean333aa672025-07-13 19:49:21 +0000295export class SketchToolCardCommitMessageStyle extends SketchTailwindElement {
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000296 @property()
297 toolCall: ToolCall;
298
299 @property()
300 open: boolean;
301
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000302 @property()
303 state: State;
304
Josh Bleecher Snydera2a31502025-05-07 12:37:18 +0000305 constructor() {
306 super();
307 }
308
309 connectedCallback() {
310 super.connectedCallback();
311 }
312
313 disconnectedCallback() {
314 super.disconnectedCallback();
315 }
316
317 render() {
banksean333aa672025-07-13 19:49:21 +0000318 return html`<sketch-tool-card-base
319 .open=${this.open}
320 .toolCall=${this.toolCall}
321 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100322 }
323}
324
325@customElement("sketch-tool-card-multiple-choice")
banksean333aa672025-07-13 19:49:21 +0000326export class SketchToolCardMultipleChoice extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100327 @property() toolCall: ToolCall;
328 @property() open: boolean;
329 @property() selectedOption: MultipleChoiceOption = null;
330
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100331 connectedCallback() {
332 super.connectedCallback();
333 this.updateSelectedOption();
334 }
335
336 updated(changedProps) {
337 if (changedProps.has("toolCall")) {
338 this.updateSelectedOption();
339 }
340 }
341
342 updateSelectedOption() {
343 if (this.toolCall?.result_message?.tool_result) {
344 try {
345 this.selectedOption = JSON.parse(
346 this.toolCall.result_message.tool_result,
347 ).selected;
348 } catch (e) {
349 console.error("Error parsing result:", e);
350 }
351 } else {
352 this.selectedOption = null;
353 }
354 }
355
356 async handleOptionClick(choice) {
357 this.selectedOption = this.selectedOption === choice ? null : choice;
358
359 const event = new CustomEvent("multiple-choice-selected", {
360 detail: {
361 responseText: this.selectedOption.responseText,
362 toolCall: this.toolCall,
363 },
364 bubbles: true,
365 composed: true,
366 });
367 this.dispatchEvent(event);
368 }
369
370 render() {
371 let choices = [];
372 let question = "";
373 try {
374 const inputData = JSON.parse(
375 this.toolCall?.input || "{}",
376 ) as MultipleChoiceParams;
377 choices = inputData.responseOptions || [];
378 question = inputData.question || "Please select an option:";
379 } catch (e) {
380 console.error("Error parsing multiple-choice input:", e);
381 }
382
383 const summaryContent =
384 this.selectedOption !== null
banksean333aa672025-07-13 19:49:21 +0000385 ? html`<span class="italic p-2">
386 ${question}:
387 <strong class="not-italic text-blue-600 font-semibold"
388 >${this.selectedOption.caption}</strong
389 >
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100390 </span>`
banksean333aa672025-07-13 19:49:21 +0000391 : html`<span class="italic p-2">${question}</span>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100392
banksean333aa672025-07-13 19:49:21 +0000393 const inputContent = html`<div class="flex flex-row flex-wrap gap-2 my-2">
394 ${choices.map((choice) => {
395 const isSelected =
396 this.selectedOption !== null && this.selectedOption === choice;
397 return html`
398 <div
399 class="inline-flex items-center px-3 py-2 rounded cursor-pointer transition-all duration-200 border select-none ${isSelected
400 ? "bg-blue-50 border-blue-500"
401 : "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"}"
402 @click=${() => this.handleOptionClick(choice)}
403 title="${choice.responseText}"
404 >
405 <span class="option-label">${choice.caption}</span>
406 ${isSelected
407 ? html`<span class="ml-1.5 text-blue-600">✓</span>`
408 : ""}
409 </div>
410 `;
411 })}
412 </div>`;
413
414 return html`<div class="multiple-choice-card">
415 ${summaryContent} ${inputContent}
416 </div>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100417 }
418}
419
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700420@customElement("sketch-tool-card-todo-write")
banksean333aa672025-07-13 19:49:21 +0000421export class SketchToolCardTodoWrite extends SketchTailwindElement {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700422 @property() toolCall: ToolCall;
423 @property() open: boolean;
424
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700425 render() {
426 const inputData = JSON.parse(this.toolCall?.input || "{}");
427 const tasks = inputData.tasks || [];
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000428
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700429 // Generate circles based on task status
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000430 const circles = tasks
431 .map((task) => {
432 switch (task.status) {
433 case "completed":
434 return "●"; // full circle
435 case "in-progress":
436 return "◐"; // half circle
437 case "queued":
438 default:
439 return "○"; // empty circle
440 }
441 })
442 .join(" ");
443
banksean333aa672025-07-13 19:49:21 +0000444 const summaryContent = html`<span class="italic text-gray-600">
445 ${circles}
446 </span>`;
447 const resultContent = this.toolCall?.result_message?.tool_result
448 ? createPreElement(this.toolCall.result_message.tool_result)
449 : "";
450
451 return html`<sketch-tool-card-base
452 .open=${this.open}
453 .toolCall=${this.toolCall}
454 .summaryContent=${summaryContent}
455 .resultContent=${resultContent}
456 ></sketch-tool-card-base>`;
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000457 }
458}
459
460@customElement("sketch-tool-card-keyword-search")
banksean333aa672025-07-13 19:49:21 +0000461export class SketchToolCardKeywordSearch extends SketchTailwindElement {
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000462 @property() toolCall: ToolCall;
463 @property() open: boolean;
464
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000465 render() {
466 const inputData = JSON.parse(this.toolCall?.input || "{}");
467 const query = inputData.query || "";
468 const searchTerms = inputData.search_terms || [];
469
banksean333aa672025-07-13 19:49:21 +0000470 const summaryContent = html`<div
471 class="flex flex-col gap-0.5 w-full max-w-full overflow-hidden"
472 >
473 <div
474 class="text-gray-800 text-xs normal-case whitespace-normal break-words leading-tight"
475 >
476 🔍 ${query}
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000477 </div>
banksean333aa672025-07-13 19:49:21 +0000478 <div
479 class="text-gray-600 text-xs normal-case whitespace-normal break-words leading-tight mt-px"
480 >
481 🗝️ ${searchTerms.join(", ")}
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000482 </div>
banksean333aa672025-07-13 19:49:21 +0000483 </div>`;
484
485 const inputContent = html`<div>
486 <div><strong>Query:</strong> ${query}</div>
487 <div><strong>Search terms:</strong> ${searchTerms.join(", ")}</div>
488 </div>`;
489
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 .inputContent=${inputContent}
499 .resultContent=${resultContent}
500 ></sketch-tool-card-base>`;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700501 }
502}
503
504@customElement("sketch-tool-card-todo-read")
banksean333aa672025-07-13 19:49:21 +0000505export class SketchToolCardTodoRead extends SketchTailwindElement {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700506 @property() toolCall: ToolCall;
507 @property() open: boolean;
508
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700509 render() {
banksean333aa672025-07-13 19:49:21 +0000510 const summaryContent = html`<span class="italic text-gray-600">
511 Read todo list
512 </span>`;
513 const resultContent = this.toolCall?.result_message?.tool_result
514 ? createPreElement(this.toolCall.result_message.tool_result)
515 : "";
516
517 return html`<sketch-tool-card-base
518 .open=${this.open}
519 .toolCall=${this.toolCall}
520 .summaryContent=${summaryContent}
521 .resultContent=${resultContent}
522 ></sketch-tool-card-base>`;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700523 }
524}
525
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100526@customElement("sketch-tool-card-generic")
banksean333aa672025-07-13 19:49:21 +0000527export class SketchToolCardGeneric extends SketchTailwindElement {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100528 @property() toolCall: ToolCall;
529 @property() open: boolean;
530
531 render() {
banksean333aa672025-07-13 19:49:21 +0000532 const summaryContent = html`<span
533 class="block whitespace-normal break-words max-w-full w-full"
534 >
535 ${this.toolCall?.input}
536 </span>`;
537
538 const inputContent = html`<div class="max-w-full break-words">
539 Input:
540 ${createPreElement(
541 this.toolCall?.input || "",
542 "max-w-full whitespace-pre-wrap break-words",
543 )}
544 </div>`;
545
546 const resultContent = this.toolCall?.result_message?.tool_result
547 ? html`<div class="max-w-full break-words">
548 Result: ${createPreElement(this.toolCall.result_message.tool_result)}
549 </div>`
550 : "";
551
552 return html`<sketch-tool-card-base
553 .open=${this.open}
554 .toolCall=${this.toolCall}
555 .summaryContent=${summaryContent}
556 .inputContent=${inputContent}
557 .resultContent=${resultContent}
558 ></sketch-tool-card-base>`;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100559 }
560}
561
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700562declare global {
563 interface HTMLElementTagNameMap {
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100564 "sketch-tool-card-generic": SketchToolCardGeneric;
565 "sketch-tool-card-bash": SketchToolCardBash;
566 "sketch-tool-card-codereview": SketchToolCardCodeReview;
567 "sketch-tool-card-done": SketchToolCardDone;
568 "sketch-tool-card-patch": SketchToolCardPatch;
569 "sketch-tool-card-think": SketchToolCardThink;
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700570 "sketch-tool-card-set-slug": SketchToolCardSetSlug;
571 "sketch-tool-card-commit-message-style": SketchToolCardCommitMessageStyle;
Pokey Rule7ac5ed02025-05-07 15:26:10 +0100572 "sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700573 "sketch-tool-card-todo-write": SketchToolCardTodoWrite;
574 "sketch-tool-card-todo-read": SketchToolCardTodoRead;
Josh Bleecher Snyder991164f2025-05-29 05:02:10 +0000575 "sketch-tool-card-keyword-search": SketchToolCardKeywordSearch;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700576 }
577}