blob: fc5d0b849e8ef07ed7a03e7c0f16e9240c94ed17 [file] [log] [blame]
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001import { css, html, LitElement } from "lit";
2import { unsafeHTML } from "lit/directives/unsafe-html.js";
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07003import { customElement, property } from "lit/decorators.js";
Sean McCulloughd9f13372025-04-21 15:08:49 -07004import { ToolCall } from "../types";
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07005import { marked, MarkedOptions } from "marked";
6
7function renderMarkdown(markdownContent: string): string {
8 try {
9 // Set markdown options for proper code block highlighting and safety
10 const markedOptions: MarkedOptions = {
11 gfm: true, // GitHub Flavored Markdown
12 breaks: true, // Convert newlines to <br>
13 async: false,
14 // DOMPurify is recommended for production, but not included in this implementation
15 };
16 return marked.parse(markdownContent, markedOptions) as string;
17 } catch (error) {
18 console.error("Error rendering markdown:", error);
19 // Fallback to plain text if markdown parsing fails
20 return markdownContent;
21 }
22}
23
24@customElement("sketch-tool-card")
25export class SketchToolCard extends LitElement {
26 @property()
27 toolCall: ToolCall;
28
29 @property()
30 open: boolean;
31
32 static styles = css`
33 .tool-call {
34 display: flex;
35 align-items: center;
36 gap: 8px;
37 white-space: nowrap;
38 }
39
40 .tool-call-status {
41 margin-right: 4px;
42 text-align: center;
43 }
44
45 .tool-call-status.spinner {
46 animation: spin 1s infinite linear;
47 display: inline-block;
48 width: 1em;
49 }
50
51 @keyframes spin {
52 0% {
53 transform: rotate(0deg);
54 }
55 100% {
56 transform: rotate(360deg);
57 }
58 }
59
60 .title {
61 font-style: italic;
62 }
63
64 .cancel-button {
65 background: rgb(76, 175, 80);
66 color: white;
67 border: none;
68 padding: 4px 10px;
69 border-radius: 4px;
70 cursor: pointer;
71 font-size: 12px;
72 margin: 5px;
73 }
74
75 .cancel-button:hover {
76 background: rgb(200, 35, 51) !important;
77 }
78
79 .codereview-OK {
80 color: green;
81 }
82
83 details {
84 border-radius: 4px;
85 padding: 0.25em;
86 margin: 0.25em;
87 display: flex;
88 flex-direction: column;
89 align-items: start;
90 }
91
92 details summary {
93 list-style: none;
94 &::before {
95 cursor: hand;
96 font-family: monospace;
97 content: "+";
98 color: white;
99 background-color: darkgray;
100 border-radius: 1em;
101 padding-left: 0.5em;
102 margin: 0.25em;
103 min-width: 1em;
104 }
105 [open] &::before {
106 content: "-";
107 }
108 }
109
110 details summary:hover {
111 list-style: none;
112 &::before {
113 background-color: gray;
114 }
115 }
116 summary {
117 display: flex;
118 flex-direction: row;
119 flex-wrap: nowrap;
120 justify-content: flex-start;
121 align-items: baseline;
122 }
123
124 summary .tool-name {
125 font-family: monospace;
126 color: white;
127 background: rgb(124 145 160);
128 border-radius: 4px;
129 padding: 0.25em;
130 margin: 0.25em;
131 white-space: pre;
132 }
133
134 .summary-text {
135 padding: 0.25em;
136 display: flex;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700137 overflow: hidden;
138 text-overflow: ellipsis;
139 }
140
141 details[open] .summary-text {
142 /*display: none;*/
143 }
144
145 .tool-error-message {
146 font-style: italic;
147 color: #aa0909;
148 }
Sean McCullough2deac842025-04-21 18:17:57 -0700149
150 .elapsed {
151 font-size: 10px;
152 color: #888;
153 font-style: italic;
154 margin-left: 3px;
155 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700156 `;
157
158 constructor() {
159 super();
160 }
161
162 connectedCallback() {
163 super.connectedCallback();
164 }
165
166 disconnectedCallback() {
167 super.disconnectedCallback();
168 }
169
170 _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
171 console.log("cancelToolCall", tool_call_id, button);
172 button.innerText = "Cancelling";
173 button.disabled = true;
174 try {
175 const response = await fetch("cancel", {
176 method: "POST",
177 headers: {
178 "Content-Type": "application/json",
179 },
180 body: JSON.stringify({
181 tool_call_id: tool_call_id,
182 reason: "user requested cancellation",
183 }),
184 });
185 if (response.ok) {
186 console.log("cancel", tool_call_id, response);
187 button.parentElement.removeChild(button);
188 } else {
189 button.innerText = "Cancel";
190 console.log(`error trying to cancel ${tool_call_id}: `, response);
191 }
192 } catch (e) {
193 console.error("cancel", tool_call_id, e);
194 }
195 };
196
197 render() {
198 const toolCallStatus = this.toolCall?.result_message
199 ? this.toolCall?.result_message.tool_error
Josh Bleecher Snyder5fabc742025-04-29 14:09:50 +0000200 ? html`🙈
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700201 <span class="tool-error-message"
Sean McCullough2deac842025-04-21 18:17:57 -0700202 >${this.toolCall?.result_message.tool_result}</span
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700203 >`
204 : ""
205 : "⏳";
206
207 const cancelButton = this.toolCall?.result_message
208 ? ""
209 : html`<button
210 class="cancel-button"
211 title="Cancel this operation"
212 @click=${(e: Event) => {
213 e.stopPropagation();
214 const button = e.target as HTMLButtonElement;
215 this._cancelToolCall(this.toolCall?.tool_call_id, button);
216 }}
217 >
218 Cancel
219 </button>`;
220
221 const status = html`<span
222 class="tool-call-status ${this.toolCall?.result_message ? "" : "spinner"}"
223 >${toolCallStatus}</span
224 >`;
225
Sean McCullough2deac842025-04-21 18:17:57 -0700226 const elapsed = html`${this.toolCall?.result_message?.elapsed
227 ? html`<span class="elapsed"
228 >${(this.toolCall?.result_message?.elapsed / 1e9).toFixed(2)}s
229 elapsed</span
230 >`
231 : ""}`;
232
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700233 const ret = html`<div class="tool-call">
234 <details ?open=${this.open}>
235 <summary>
236 <span class="tool-name">${this.toolCall?.name}</span>
237 <span class="summary-text"><slot name="summary"></slot></span>
Sean McCullough2deac842025-04-21 18:17:57 -0700238 ${status} ${cancelButton} ${elapsed}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700239 </summary>
240 <slot name="input"></slot>
241 <slot name="result"></slot>
242 </details>
243 </div> `;
244 if (true) {
245 return ret;
246 }
247 }
248}
249
250@customElement("sketch-tool-card-bash")
251export class SketchToolCardBash extends LitElement {
252 @property()
253 toolCall: ToolCall;
254
255 @property()
256 open: boolean;
257
258 static styles = css`
259 pre {
Philip Zeyligera54c6a32025-04-23 02:13:36 +0000260 background: rgb(236, 236, 236);
261 color: black;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700262 padding: 0.5em;
263 border-radius: 4px;
264 }
265 .summary-text {
266 overflow: hidden;
267 text-overflow: ellipsis;
268 font-family: monospace;
269 }
270 .input {
271 display: flex;
272 }
273 .input pre {
274 width: 100%;
275 margin-bottom: 0;
276 border-radius: 4px 4px 0 0;
277 }
278 .result pre {
279 margin-top: 0;
Philip Zeyligera54c6a32025-04-23 02:13:36 +0000280 color: #555;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700281 border-radius: 0 0 4px 4px;
282 }
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000283 .background-badge {
284 display: inline-block;
285 background-color: #6200ea;
286 color: white;
287 font-size: 10px;
288 font-weight: bold;
289 padding: 2px 6px;
290 border-radius: 10px;
291 margin-left: 8px;
292 vertical-align: middle;
293 }
294 .command-wrapper {
295 display: flex;
296 align-items: center;
297 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700298 `;
299
300 constructor() {
301 super();
302 }
303
304 connectedCallback() {
305 super.connectedCallback();
306 }
307
308 disconnectedCallback() {
309 super.disconnectedCallback();
310 }
311
312 render() {
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000313 const inputData = JSON.parse(this.toolCall?.input || "{}");
314 const isBackground = inputData?.background === true;
315 const backgroundIcon = isBackground ? "🔄 " : "";
316
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700317 return html`
318 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000319 <span slot="summary" class="summary-text">
320 <div class="command-wrapper">
321 🖥️ ${backgroundIcon}${inputData?.command}
322 </div>
323 </span>
324 <div slot="input" class="input">
325 <pre>🖥️ ${backgroundIcon}${inputData?.command}</pre>
326 </div>
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700327 ${
328 this.toolCall?.result_message
329 ? html` ${this.toolCall?.result_message.tool_result
330 ? html`<div slot="result" class="result">
331 <pre class="tool-call-result">
332${this.toolCall?.result_message.tool_result}</pre
333 >
334 </div>`
335 : ""}`
336 : ""
337 }</div>
338 </sketch-tool-card>`;
339 }
340}
341
342@customElement("sketch-tool-card-codereview")
343export class SketchToolCardCodeReview extends LitElement {
344 @property()
345 toolCall: ToolCall;
346
347 @property()
348 open: boolean;
349
350 static styles = css``;
351
352 constructor() {
353 super();
354 }
355
356 connectedCallback() {
357 super.connectedCallback();
358 }
359
360 disconnectedCallback() {
361 super.disconnectedCallback();
362 }
Josh Bleecher Snyder2dc86b92025-04-29 14:11:58 +0000363 // Determine the status icon based on the content of the result message
364 // This corresponds to the output format in claudetool/differential.go:Run
365 getStatusIcon(resultText: string): string {
366 if (!resultText) return "";
367 if (resultText === "OK") return "✔️";
368 if (resultText.includes("# Errors")) return "⛔";
369 if (resultText.includes("# Info")) return "ℹ️";
370 if (resultText.includes("uncommitted changes in repo")) return "🧹";
371 if (resultText.includes("no new commits have been added")) return "🐣";
372 if (resultText.includes("git repo is not clean")) return "🧼";
373 return "❓";
374 }
375
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700376 render() {
Josh Bleecher Snyder2dc86b92025-04-29 14:11:58 +0000377 const resultText = this.toolCall?.result_message?.tool_result || "";
378 const statusIcon = this.getStatusIcon(resultText);
379
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700380 return html` <sketch-tool-card
381 .open=${this.open}
382 .toolCall=${this.toolCall}
383 >
Josh Bleecher Snyder2dc86b92025-04-29 14:11:58 +0000384 <span slot="summary" class="summary-text"> ${statusIcon} </span>
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700385 <div slot="result">
Josh Bleecher Snyder2dc86b92025-04-29 14:11:58 +0000386 <pre>${resultText}</pre>
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700387 </div>
388 </sketch-tool-card>`;
389 }
390}
391
392@customElement("sketch-tool-card-done")
393export class SketchToolCardDone extends LitElement {
394 @property()
395 toolCall: ToolCall;
396
397 @property()
398 open: boolean;
399
400 static styles = css``;
401
402 constructor() {
403 super();
404 }
405
406 connectedCallback() {
407 super.connectedCallback();
408 }
409
410 disconnectedCallback() {
411 super.disconnectedCallback();
412 }
413
414 render() {
415 const doneInput = JSON.parse(this.toolCall.input);
416 return html` <sketch-tool-card
417 .open=${this.open}
418 .toolCall=${this.toolCall}
419 >
420 <span slot="summary" class="summary-text"> </span>
421 <div slot="result">
422 ${Object.keys(doneInput.checklist_items).map((key) => {
423 const item = doneInput.checklist_items[key];
424 let statusIcon = "⛔";
425 if (item.status == "yes") {
426 statusIcon = "👍";
427 } else if (item.status == "not applicable") {
428 statusIcon = "🤷‍♂️";
429 }
430 return html`<div>
431 <span>${statusIcon}</span> ${key}:${item.status}
432 </div>`;
433 })}
434 </div>
435 </sketch-tool-card>`;
436 }
437}
438
439@customElement("sketch-tool-card-patch")
440export class SketchToolCardPatch extends LitElement {
441 @property()
442 toolCall: ToolCall;
443
444 @property()
445 open: boolean;
446
447 static styles = css`
448 .summary-text {
449 color: #555;
450 font-family: monospace;
451 overflow: hidden;
452 text-overflow: ellipsis;
453 white-space: nowrap;
454 border-radius: 3px;
455 }
456 `;
457
458 constructor() {
459 super();
460 }
461
462 connectedCallback() {
463 super.connectedCallback();
464 }
465
466 disconnectedCallback() {
467 super.disconnectedCallback();
468 }
469
470 render() {
471 const patchInput = JSON.parse(this.toolCall?.input);
472 return html` <sketch-tool-card
473 .open=${this.open}
474 .toolCall=${this.toolCall}
475 >
476 <span slot="summary" class="summary-text">
477 ${patchInput?.path}: ${patchInput.patches.length}
478 edit${patchInput.patches.length > 1 ? "s" : ""}
479 </span>
480 <div slot="input">
481 ${patchInput.patches.map((patch) => {
482 return html` Patch operation: <b>${patch.operation}</b>
483 <pre>${patch.newText}</pre>`;
484 })}
485 </div>
486 <div slot="result">
487 <pre>${this.toolCall?.result_message?.tool_result}</pre>
488 </div>
489 </sketch-tool-card>`;
490 }
491}
492
493@customElement("sketch-tool-card-think")
494export class SketchToolCardThink extends LitElement {
495 @property()
496 toolCall: ToolCall;
497
498 @property()
499 open: boolean;
500
501 static styles = css`
502 .thought-bubble {
503 overflow-x: auto;
504 margin-bottom: 3px;
505 font-family: monospace;
506 padding: 3px 5px;
507 background: rgb(236, 236, 236);
508 border-radius: 6px;
509 user-select: text;
510 cursor: text;
511 -webkit-user-select: text;
512 -moz-user-select: text;
513 -ms-user-select: text;
514 font-size: 13px;
515 line-height: 1.3;
516 }
517 .summary-text {
518 overflow: hidden;
519 text-overflow: ellipsis;
520 font-family: monospace;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700521 }
522 `;
523
524 constructor() {
525 super();
526 }
527
528 connectedCallback() {
529 super.connectedCallback();
530 }
531
532 disconnectedCallback() {
533 super.disconnectedCallback();
534 }
535
536 render() {
537 return html`
538 <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
539 <span slot="summary" class="summary-text"
Sean McCulloughffb58a32025-04-28 13:50:56 -0700540 >${JSON.parse(this.toolCall?.input)?.thoughts?.split("\n")[0]}</span
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700541 >
542 <div slot="input" class="thought-bubble">
543 <div class="markdown-content">
544 ${unsafeHTML(
545 renderMarkdown(JSON.parse(this.toolCall?.input)?.thoughts),
546 )}
547 </div>
548 </div>
549 </sketch-tool-card>
550 `;
551 }
552}
553
554@customElement("sketch-tool-card-title")
555export class SketchToolCardTitle extends LitElement {
556 @property()
557 toolCall: ToolCall;
558
559 @property()
560 open: boolean;
561
562 static styles = css`
563 .summary-text {
564 font-style: italic;
565 }
Josh Bleecher Snyder47b19362025-04-30 01:34:14 +0000566 pre {
567 display: inline;
568 font-family: monospace;
569 background: rgb(236, 236, 236);
570 padding: 2px 4px;
571 border-radius: 2px;
572 margin: 0;
573 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700574 `;
575 constructor() {
576 super();
577 }
578
579 connectedCallback() {
580 super.connectedCallback();
581 }
582
583 disconnectedCallback() {
584 super.disconnectedCallback();
585 }
586
587 render() {
Josh Bleecher Snyder47b19362025-04-30 01:34:14 +0000588 const inputData = JSON.parse(this.toolCall?.input || "{}");
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700589 return html`
Josh Bleecher Snyder47b19362025-04-30 01:34:14 +0000590 <span class="summary-text">
591 Setting title to
592 <b>${inputData.title}</b>
593 and branch to
594 <pre>sketch/${inputData.branch_name}</pre>
595 </span>
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700596 `;
597 }
598}
599
Sean McCulloughfa40c412025-04-28 20:10:04 +0000600@customElement("sketch-tool-card-multiple-choice")
601export class SketchToolCardMultipleChoice extends LitElement {
602 @property()
603 toolCall: ToolCall;
604
605 @property()
606 open: boolean;
607
608 @property()
609 selectedOption: string | number | null = null;
610
611 static styles = css`
612 .options-container {
613 display: flex;
614 flex-direction: row;
615 flex-wrap: wrap;
616 gap: 8px;
617 margin: 10px 0;
618 }
619
620 .option {
621 display: inline-flex;
622 align-items: center;
623 padding: 8px 12px;
624 border-radius: 4px;
625 background-color: #f5f5f5;
626 cursor: pointer;
627 transition: all 0.2s;
628 border: 1px solid transparent;
629 user-select: none;
630 }
631
632 .option:hover {
633 background-color: #e0e0e0;
634 border-color: #ccc;
635 transform: translateY(-1px);
636 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
637 }
638
639 .option:active {
640 transform: translateY(0);
641 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
642 background-color: #d5d5d5;
643 }
644
645 .option.selected {
646 background-color: #e3f2fd;
647 border-color: #2196f3;
648 border-width: 1px;
649 border-style: solid;
650 }
651
652 .option-index {
653 font-size: 0.8em;
654 opacity: 0.7;
655 margin-right: 6px;
656 }
657
658 .option-label {
659 font-family: sans-serif;
660 }
661
662 .option-checkmark {
663 margin-left: 6px;
664 color: #2196f3;
665 }
666
667 .summary-text {
668 font-style: italic;
669 }
670
671 .summary-text strong {
672 font-style: normal;
673 color: #2196f3;
674 font-weight: 600;
675 }
676
677 p {
678 display: flex;
679 align-items: center;
680 flex-wrap: wrap;
681 margin-bottom: 10px;
682 }
683 `;
684
685 constructor() {
686 super();
687 }
688
689 connectedCallback() {
690 super.connectedCallback();
691 this.updateSelectedOption();
692 }
693
694 disconnectedCallback() {
695 super.disconnectedCallback();
696 }
697
698 updated(changedProps) {
699 if (changedProps.has("toolCall")) {
700 this.updateSelectedOption();
701 }
702 }
703
704 updateSelectedOption() {
705 // Get selected option from result if available
706 if (this.toolCall?.result_message?.tool_result) {
707 try {
708 this.selectedOption = JSON.parse(
709 this.toolCall.result_message.tool_result,
710 ).selected;
711 } catch (e) {
712 console.error("Error parsing result:", e);
713 this.selectedOption = this.toolCall.result_message.tool_result;
714 }
715 } else {
716 this.selectedOption = null;
717 }
718 }
719
720 handleOptionClick(choice) {
721 // If this option is already selected, unselect it (toggle behavior)
722 if (this.selectedOption === choice) {
723 this.selectedOption = null;
724 } else {
725 // Otherwise, select the clicked option
726 this.selectedOption = choice;
727 }
728
729 // Dispatch a custom event that can be listened to by parent components
730 const event = new CustomEvent("option-selected", {
731 detail: { selected: this.selectedOption },
732 bubbles: true,
733 composed: true,
734 });
735 this.dispatchEvent(event);
736 }
737
738 render() {
739 // Parse the input to get choices if available
740 let choices = [];
741 let question = "";
742 try {
743 const inputData = JSON.parse(this.toolCall?.input || "{}");
744 choices = inputData.choices || [];
745 question = inputData.question || "Please select an option:";
746 } catch (e) {
747 console.error("Error parsing multiple-choice input:", e);
748 }
749
750 // Determine what to show in the summary slot
751 const summaryContent =
752 this.selectedOption !== null
753 ? html`<span class="summary-text"
754 >${question}: <strong>${this.selectedOption}</strong></span
755 >`
756 : html`<span class="summary-text">${question}</span>`;
757
758 return html` <sketch-tool-card
759 .open=${this.open}
760 .toolCall=${this.toolCall}
761 >
762 <span slot="summary">${summaryContent}</span>
763 <div slot="input">
764 <p>${question}</p>
765 <div class="options-container">
766 ${choices.map((choice, index) => {
767 const isSelected =
768 this.selectedOption !== null &&
769 (this.selectedOption === choice || this.selectedOption === index);
770 return html`
771 <div
772 class="option ${isSelected ? "selected" : ""}"
773 @click=${() => this.handleOptionClick(choice)}
774 >
775 <span class="option-index">${index + 1}</span>
776 <span class="option-label">${choice}</span>
777 ${isSelected
778 ? html`<span class="option-checkmark">✓</span>`
779 : ""}
780 </div>
781 `;
782 })}
783 </div>
784 </div>
785 <div slot="result">
786 ${this.toolCall?.result_message && this.selectedOption
787 ? html`<p>Selected: <strong>${this.selectedOption}</strong></p>`
788 : ""}
789 </div>
790 </sketch-tool-card>`;
791 }
792}
793
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700794@customElement("sketch-tool-card-generic")
795export class SketchToolCardGeneric extends LitElement {
796 @property()
797 toolCall: ToolCall;
798
799 @property()
800 open: boolean;
801
802 constructor() {
803 super();
804 }
805
806 connectedCallback() {
807 super.connectedCallback();
808 }
809
810 disconnectedCallback() {
811 super.disconnectedCallback();
812 }
813
814 render() {
815 return html` <sketch-tool-card
816 .open=${this.open}
817 .toolCall=${this.toolCall}
818 >
819 <span slot="summary" class="summary-text">${this.toolCall?.input}</span>
820 <div slot="input">
821 Input:
822 <pre>${this.toolCall?.input}</pre>
823 </div>
824 <div slot="result">
825 Result:
826 ${this.toolCall?.result_message
827 ? html` ${this.toolCall?.result_message.tool_result
828 ? html`<pre>${this.toolCall?.result_message.tool_result}</pre>`
829 : ""}`
830 : ""}
831 </div>
832 </sketch-tool-card>`;
833 }
834}
835
836declare global {
837 interface HTMLElementTagNameMap {
838 "sketch-tool-card": SketchToolCard;
839 "sketch-tool-card-generic": SketchToolCardGeneric;
840 "sketch-tool-card-bash": SketchToolCardBash;
841 "sketch-tool-card-codereview": SketchToolCardCodeReview;
842 "sketch-tool-card-done": SketchToolCardDone;
843 "sketch-tool-card-patch": SketchToolCardPatch;
844 "sketch-tool-card-think": SketchToolCardThink;
845 "sketch-tool-card-title": SketchToolCardTitle;
Sean McCulloughfa40c412025-04-28 20:10:04 +0000846 "sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700847 }
848}