sketch: remove shadowDOM dependency from tool card components
Replace shadowDOM-based slot system with property-based composition in all
sketch-tool-card-[TOOL_NAME] components to support shadowDOM-free architecture.
Problem Analysis:
- sketch-tool-card component relied on HTML5 template slots which require shadowDOM
- 13 tool card components used sketch-tool-card as composition base via slots
- shadowDOM dependency blocked broader effort to reduce shadowDOM usage
- Need to preserve all existing functionality while removing slot dependency
Solution Implementation:
- Created sketch-tool-card-base component with property-based content injection
- Replaced slot system with summaryContent, inputContent, resultContent properties
- Maintained all existing styling, behavior, and expand/collapse functionality
- Migrated all 13 existing tool card components to use new base component
Components Migrated:
- sketch-tool-card-about-sketch
- sketch-tool-card-browser-clear-console-logs
- sketch-tool-card-browser-click
- sketch-tool-card-browser-eval
- sketch-tool-card-browser-get-text
- sketch-tool-card-browser-navigate
- sketch-tool-card-browser-recent-console-logs
- sketch-tool-card-browser-resize
- sketch-tool-card-browser-scroll-into-view
- sketch-tool-card-browser-type
- sketch-tool-card-browser-wait-for
- sketch-tool-card-read-image
- sketch-tool-card-take-screenshot
Migration Pattern:
- Changed from: <slot name="summary">content</slot>
- Changed to: .summaryContent=html content
- Preserved all component-specific styling and logic
- Maintained existing API surface for parent components
Architecture Benefits:
- Removes shadowDOM requirement from 13+ components
- Enables future shadowDOM-free component development
- Maintains backward compatibility during migration
- Preserves all existing tool card functionality
Files Added:
- sketch/webui/src/web-components/sketch-tool-card-base.ts (new shadowDOM-free base)
Files Modified:
- All 13 sketch-tool-card-[TOOL_NAME].ts components migrated to use new base
Verification:
- TypeScript compilation passes without errors
- Demo pages render correctly with consistent styling
- Expand/collapse behavior preserved across all tool types
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: sa3288c1d986356e5k
diff --git a/webui/src/web-components/sketch-tool-card-base.ts b/webui/src/web-components/sketch-tool-card-base.ts
new file mode 100644
index 0000000..1bfdbe1
--- /dev/null
+++ b/webui/src/web-components/sketch-tool-card-base.ts
@@ -0,0 +1,133 @@
+import { html, TemplateResult } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+import { ToolCall } from "../types";
+import { SketchTailwindElement } from "./sketch-tailwind-element";
+
+@customElement("sketch-tool-card-base")
+export class SketchToolCardBase extends SketchTailwindElement {
+ @property() toolCall: ToolCall;
+ @property() open: boolean;
+ @property() summaryContent: TemplateResult | string = "";
+ @property() inputContent: TemplateResult | string = "";
+ @property() resultContent: TemplateResult | string = "";
+ @state() detailsVisible: boolean = false;
+
+ _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
+ button.innerText = "Cancelling";
+ button.disabled = true;
+ try {
+ const response = await fetch("cancel", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ tool_call_id: tool_call_id,
+ reason: "user requested cancellation",
+ }),
+ });
+ if (response.ok) {
+ button.parentElement.removeChild(button);
+ } else {
+ button.innerText = "Cancel";
+ }
+ } catch (e) {
+ console.error("cancel", tool_call_id, e);
+ }
+ };
+
+ _toggleDetails(e: Event) {
+ e.stopPropagation();
+ this.detailsVisible = !this.detailsVisible;
+ }
+
+ render() {
+ // Status indicator based on result
+ let statusIcon = html`<span
+ class="flex items-center justify-center text-sm text-yellow-500 animate-spin"
+ >⏳</span
+ >`;
+ if (this.toolCall?.result_message) {
+ statusIcon = this.toolCall?.result_message.tool_error
+ ? html`<span
+ class="flex items-center justify-center text-sm text-gray-500"
+ >〰️</span
+ >`
+ : html`<span
+ class="flex items-center justify-center text-sm text-green-600"
+ >✓</span
+ >`;
+ }
+
+ // Cancel button for pending operations
+ const cancelButton = this.toolCall?.result_message
+ ? ""
+ : html`<button
+ class="cursor-pointer text-white bg-red-600 hover:bg-red-700 disabled:bg-gray-400 disabled:cursor-not-allowed border-none rounded text-xs px-1.5 py-0.5 whitespace-nowrap min-w-[50px]"
+ title="Cancel this operation"
+ @click=${(e: Event) => {
+ e.stopPropagation();
+ this._cancelToolCall(
+ this.toolCall?.tool_call_id,
+ e.target as HTMLButtonElement,
+ );
+ }}
+ >
+ Cancel
+ </button>`;
+
+ // Elapsed time display
+ const elapsed = this.toolCall?.result_message?.elapsed
+ ? html`<span
+ class="text-xs text-gray-600 whitespace-nowrap min-w-[40px] text-right"
+ >${(this.toolCall?.result_message?.elapsed / 1e9).toFixed(1)}s</span
+ >`
+ : html`<span
+ class="text-xs text-gray-600 whitespace-nowrap min-w-[40px] text-right"
+ ></span>`;
+
+ // Initialize details visibility based on open property
+ if (this.open && !this.detailsVisible) {
+ this.detailsVisible = true;
+ }
+
+ return html`<div class="block max-w-full w-full box-border overflow-hidden">
+ <div class="flex flex-col w-full">
+ <div
+ class="flex w-full box-border py-1.5 px-2 pl-3 items-center gap-2 cursor-pointer rounded relative overflow-hidden flex-wrap hover:bg-black/[0.02]"
+ @click=${this._toggleDetails}
+ >
+ <span
+ class="font-mono font-medium text-gray-700 bg-black/[0.05] rounded px-1.5 py-0.5 flex-shrink-0 min-w-[45px] text-xs text-center whitespace-nowrap"
+ >${this.toolCall?.name}</span
+ >
+ <span
+ class="whitespace-normal break-words flex-grow flex-shrink text-gray-700 font-mono text-xs px-1 min-w-[50px] max-w-[calc(100%-150px)] inline-block"
+ >${this.summaryContent}</span
+ >
+ <div
+ class="flex items-center gap-3 ml-auto flex-shrink-0 min-w-[120px] justify-end pr-2"
+ >
+ ${statusIcon} ${elapsed} ${cancelButton}
+ </div>
+ </div>
+ <div
+ class="${this.detailsVisible
+ ? "block"
+ : "hidden"} p-2 bg-black/[0.02] mt-px border-t border-black/[0.05] font-mono text-xs text-gray-800 rounded-b max-w-full w-full box-border overflow-hidden"
+ >
+ ${this.inputContent
+ ? html`<div class="mb-2">${this.inputContent}</div>`
+ : ""}
+ ${this.resultContent
+ ? html`<div class="mt-2">${this.resultContent}</div>`
+ : ""}
+ </div>
+ </div>
+ </div>`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-tool-card-base": SketchToolCardBase;
+ }
+}