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.ts b/webui/src/web-components/sketch-tool-card.ts
index 33fa050..72d4a48 100644
--- a/webui/src/web-components/sketch-tool-card.ts
+++ b/webui/src/web-components/sketch-tool-card.ts
@@ -1,6 +1,6 @@
-import { css, html, LitElement } from "lit";
+import { html } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
-import { customElement, property, state } from "lit/decorators.js";
+import { customElement, property } from "lit/decorators.js";
import {
ToolCall,
MultipleChoiceOption,
@@ -9,6 +9,8 @@
} from "../types";
import { marked } from "marked";
import DOMPurify from "dompurify";
+import { SketchTailwindElement } from "./sketch-tailwind-element";
+import "./sketch-tool-card-base";
// Shared utility function for markdown rendering with DOMPurify sanitization
function renderMarkdown(markdownContent: string): string {
@@ -63,325 +65,20 @@
}
}
-// Common styles shared across all tool cards
-const commonStyles = css`
- :host {
- display: block;
- max-width: 100%;
- width: 100%;
- box-sizing: border-box;
- overflow: hidden;
- }
- pre {
- background: rgb(236, 236, 236);
- color: black;
- padding: 0.5em;
- border-radius: 4px;
- white-space: pre-wrap;
- word-break: break-word;
- max-width: 100%;
- width: 100%;
- box-sizing: border-box;
- overflow-wrap: break-word;
- }
- .summary-text {
- overflow: hidden !important;
- text-overflow: ellipsis !important;
- white-space: nowrap !important;
- max-width: 100% !important;
- width: 100% !important;
- font-family: monospace;
- display: block;
- }
-`;
-
-@customElement("sketch-tool-card")
-export class SketchToolCard extends LitElement {
- @property() toolCall: ToolCall;
- @property() open: boolean;
- @state() detailsVisible: boolean = false;
-
- static styles = css`
- .tool-call {
- display: flex;
- flex-direction: column;
- width: 100%;
- }
- .tool-row {
- display: flex;
- width: 100%;
- box-sizing: border-box;
- padding: 6px 8px 6px 12px;
- align-items: center;
- gap: 8px;
- cursor: pointer;
- border-radius: 4px;
- position: relative;
- overflow: hidden;
- flex-wrap: wrap;
- }
- .tool-row:hover {
- background-color: rgba(0, 0, 0, 0.02);
- }
- .tool-name {
- font-family: monospace;
- font-weight: 500;
- color: #444;
- background-color: rgba(0, 0, 0, 0.05);
- border-radius: 3px;
- padding: 2px 6px;
- flex-shrink: 0;
- min-width: 45px;
- font-size: 12px;
- text-align: center;
- white-space: nowrap;
- }
- .tool-success {
- color: #5cb85c;
- font-size: 14px;
- }
- .tool-error {
- color: #6c757d;
- font-size: 14px;
- }
- .tool-pending {
- color: #f0ad4e;
- font-size: 14px;
- }
- .summary-text {
- white-space: normal;
- overflow-wrap: break-word;
- word-break: break-word;
- flex-grow: 1;
- flex-shrink: 1;
- color: #444;
- font-family: monospace;
- font-size: 12px;
- padding: 0 4px;
- min-width: 50px;
- max-width: calc(100% - 150px);
- display: inline-block;
- }
- .tool-status {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-left: auto;
- flex-shrink: 0;
- min-width: 120px;
- justify-content: flex-end;
- padding-right: 8px;
- }
- .tool-call-status {
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .tool-call-status.spinner {
- animation: spin 1s infinite linear;
- }
- @keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
- }
- .elapsed {
- font-size: 11px;
- color: #777;
- white-space: nowrap;
- min-width: 40px;
- text-align: right;
- }
- .tool-details {
- padding: 8px;
- background-color: rgba(0, 0, 0, 0.02);
- margin-top: 1px;
- border-top: 1px solid rgba(0, 0, 0, 0.05);
- display: none;
- font-family: monospace;
- font-size: 12px;
- color: #333;
- border-radius: 0 0 4px 4px;
- max-width: 100%;
- width: 100%;
- box-sizing: border-box;
- overflow: hidden;
- }
- .tool-details.visible {
- display: block;
- }
- .cancel-button {
- cursor: pointer;
- color: white;
- background-color: #d9534f;
- border: none;
- border-radius: 3px;
- font-size: 11px;
- padding: 2px 6px;
- white-space: nowrap;
- min-width: 50px;
- }
- .cancel-button:hover {
- background-color: #c9302c;
- }
- .cancel-button[disabled] {
- background-color: #999;
- cursor: not-allowed;
- }
- `;
-
- _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="tool-call-status spinner tool-pending"
- >⏳</span
- >`;
- if (this.toolCall?.result_message) {
- statusIcon = this.toolCall?.result_message.tool_error
- ? html`<span class="tool-call-status tool-error">〰️</span>`
- : html`<span class="tool-call-status tool-success">✓</span>`;
- }
-
- // Cancel button for pending operations
- const cancelButton = this.toolCall?.result_message
- ? ""
- : html`<button
- class="cancel-button"
- 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="elapsed"
- >${(this.toolCall?.result_message?.elapsed / 1e9).toFixed(1)}s</span
- >`
- : html`<span class="elapsed"></span>`;
-
- // Initialize details visibility based on open property
- if (this.open && !this.detailsVisible) {
- this.detailsVisible = true;
- }
-
- return html`<div class="tool-call">
- <div class="tool-row" @click=${this._toggleDetails}>
- <span class="tool-name">${this.toolCall?.name}</span>
- <span class="summary-text"><slot name="summary"></slot></span>
- <div class="tool-status">${statusIcon} ${elapsed} ${cancelButton}</div>
- </div>
- <div class="tool-details ${this.detailsVisible ? "visible" : ""}">
- <slot name="input"></slot>
- <slot name="result"></slot>
- </div>
- </div>`;
- }
+// Shared utility function for creating Tailwind pre elements
+function createPreElement(content: string, additionalClasses: string = "") {
+ return html`<pre
+ 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}"
+ >
+${content}</pre
+ >`;
}
@customElement("sketch-tool-card-bash")
-export class SketchToolCardBash extends LitElement {
+export class SketchToolCardBash extends SketchTailwindElement {
@property() toolCall: ToolCall;
@property() open: boolean;
- static styles = [
- commonStyles,
- css`
- :host {
- max-width: 100%;
- display: block;
- }
- .input {
- display: flex;
- width: 100%;
- max-width: 100%;
- flex-direction: column;
- overflow-wrap: break-word;
- word-break: break-word;
- }
- .command-wrapper {
- max-width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .input pre {
- width: 100%;
- margin-bottom: 0;
- border-radius: 4px 4px 0 0;
- box-sizing: border-box;
- }
- .result pre {
- margin-top: 0;
- color: #555;
- border-radius: 0 0 4px 4px;
- width: 100%;
- box-sizing: border-box;
- }
- .result pre.scrollable-on-hover {
- max-height: 300px;
- overflow-y: auto;
- }
- .tool-call-result-container {
- width: 100%;
- position: relative;
- }
- .background-badge {
- display: inline-block;
- background-color: #6200ea;
- color: white;
- font-size: 10px;
- font-weight: bold;
- padding: 2px 6px;
- border-radius: 10px;
- margin-left: 8px;
- vertical-align: middle;
- }
- .command-wrapper {
- display: inline-block;
- max-width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- `,
- ];
-
render() {
const inputData = JSON.parse(this.toolCall?.input || "{}");
const isBackground = inputData?.background === true;
@@ -394,42 +91,44 @@
const displayCommand =
command.length > 80 ? command.substring(0, 80) + "..." : command;
- return html` <sketch-tool-card
+ const summaryContent = html`<div
+ class="max-w-full overflow-hidden text-ellipsis whitespace-nowrap"
+ >
+ ${backgroundIcon}${slowIcon}${displayCommand}
+ </div>`;
+
+ const inputContent = html`<div
+ class="flex w-full max-w-full flex-col overflow-wrap-break-word break-words"
+ >
+ <div class="w-full relative">
+ ${createPreElement(
+ `${backgroundIcon}${slowIcon}${inputData?.command}`,
+ "w-full mb-0 rounded-t rounded-b-none box-border",
+ )}
+ </div>
+ </div>`;
+
+ const resultContent = this.toolCall?.result_message?.tool_result
+ ? html`<div class="w-full relative">
+ ${createPreElement(
+ this.toolCall.result_message.tool_result,
+ "mt-0 text-gray-600 rounded-t-none rounded-b w-full box-border max-h-[300px] overflow-y-auto",
+ )}
+ </div>`
+ : "";
+
+ return html`<sketch-tool-card-base
.open=${this.open}
.toolCall=${this.toolCall}
- >
- <span
- slot="summary"
- class="summary-text"
- style="display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
- >
- <div
- class="command-wrapper"
- style="max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
- >
- ${backgroundIcon}${slowIcon}${displayCommand}
- </div>
- </span>
- <div slot="input" class="input">
- <div class="tool-call-result-container">
- <pre>${backgroundIcon}${slowIcon}${inputData?.command}</pre>
- </div>
- </div>
- ${this.toolCall?.result_message?.tool_result
- ? html`<div slot="result" class="result">
- <div class="tool-call-result-container">
- <pre class="tool-call-result">
-${this.toolCall?.result_message.tool_result}</pre
- >
- </div>
- </div>`
- : ""}
- </sketch-tool-card>`;
+ .summaryContent=${summaryContent}
+ .inputContent=${inputContent}
+ .resultContent=${resultContent}
+ ></sketch-tool-card-base>`;
}
}
@customElement("sketch-tool-card-codereview")
-export class SketchToolCardCodeReview extends LitElement {
+export class SketchToolCardCodeReview extends SketchTailwindElement {
@property() toolCall: ToolCall;
@property() open: boolean;
@@ -449,158 +148,146 @@
const resultText = this.toolCall?.result_message?.tool_result || "";
const statusIcon = this.getStatusIcon(resultText);
- return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text">${statusIcon}</span>
- <div slot="result"><pre>${resultText}</pre></div>
- </sketch-tool-card>`;
+ const summaryContent = html`<span>${statusIcon}</span>`;
+ const resultContent = resultText ? createPreElement(resultText) : "";
+
+ return html`<sketch-tool-card-base
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ .summaryContent=${summaryContent}
+ .resultContent=${resultContent}
+ ></sketch-tool-card-base>`;
}
}
@customElement("sketch-tool-card-done")
-export class SketchToolCardDone extends LitElement {
+export class SketchToolCardDone extends SketchTailwindElement {
@property() toolCall: ToolCall;
@property() open: boolean;
render() {
const doneInput = JSON.parse(this.toolCall.input);
- return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text"></span>
- <div slot="result">
- ${Object.keys(doneInput.checklist_items).map((key) => {
- const item = doneInput.checklist_items[key];
- let statusIcon = "〰️";
- if (item.status == "yes") {
- statusIcon = "✅";
- } else if (item.status == "not applicable") {
- statusIcon = "🤷";
- }
- return html`<div>
- <span>${statusIcon}</span> ${key}:${item.status}
- </div>`;
- })}
- </div>
- </sketch-tool-card>`;
+
+ const summaryContent = html`<span></span>`;
+
+ const resultContent = html`<div>
+ ${Object.keys(doneInput.checklist_items).map((key) => {
+ const item = doneInput.checklist_items[key];
+ let statusIcon = "〰️";
+ if (item.status == "yes") {
+ statusIcon = "✅";
+ } else if (item.status == "not applicable") {
+ statusIcon = "🤷";
+ }
+ return html`<div class="mb-1">
+ <span>${statusIcon}</span> ${key}:${item.status}
+ </div>`;
+ })}
+ </div>`;
+
+ return html`<sketch-tool-card-base
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ .summaryContent=${summaryContent}
+ .resultContent=${resultContent}
+ ></sketch-tool-card-base>`;
}
}
@customElement("sketch-tool-card-patch")
-export class SketchToolCardPatch extends LitElement {
+export class SketchToolCardPatch extends SketchTailwindElement {
@property() toolCall: ToolCall;
@property() open: boolean;
- static styles = css`
- .summary-text {
- color: #555;
- font-family: monospace;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- border-radius: 3px;
- }
- `;
-
render() {
const patchInput = JSON.parse(this.toolCall?.input);
- return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text">
- ${patchInput?.path}: ${patchInput.patches.length}
- edit${patchInput.patches.length > 1 ? "s" : ""}
- </span>
- <div slot="input">
- ${patchInput.patches.map((patch) => {
- return html`Patch operation: <b>${patch.operation}</b>
- <pre>${patch.newText}</pre>`;
- })}
- </div>
- <div slot="result">
- <pre>${this.toolCall?.result_message?.tool_result}</pre>
- </div>
- </sketch-tool-card>`;
+
+ const summaryContent = html`<span
+ class="text-gray-600 font-mono overflow-hidden text-ellipsis whitespace-nowrap rounded"
+ >
+ ${patchInput?.path}: ${patchInput.patches.length}
+ edit${patchInput.patches.length > 1 ? "s" : ""}
+ </span>`;
+
+ const inputContent = html`<div>
+ ${patchInput.patches.map((patch) => {
+ return html`<div class="mb-2">
+ Patch operation: <b>${patch.operation}</b>
+ ${createPreElement(patch.newText)}
+ </div>`;
+ })}
+ </div>`;
+
+ const resultContent = this.toolCall?.result_message?.tool_result
+ ? createPreElement(this.toolCall.result_message.tool_result)
+ : "";
+
+ return html`<sketch-tool-card-base
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ .summaryContent=${summaryContent}
+ .inputContent=${inputContent}
+ .resultContent=${resultContent}
+ ></sketch-tool-card-base>`;
}
}
@customElement("sketch-tool-card-think")
-export class SketchToolCardThink extends LitElement {
+export class SketchToolCardThink extends SketchTailwindElement {
@property() toolCall: ToolCall;
@property() open: boolean;
- static styles = css`
- .thought-bubble {
- overflow-x: auto;
- margin-bottom: 3px;
- font-family: monospace;
- padding: 3px 5px;
- background: rgb(236, 236, 236);
- border-radius: 6px;
- user-select: text;
- cursor: text;
- -webkit-user-select: text;
- -moz-user-select: text;
- -ms-user-select: text;
- font-size: 13px;
- line-height: 1.3;
- }
- .summary-text {
- overflow: hidden;
- text-overflow: ellipsis;
- font-family: monospace;
- }
- `;
-
render() {
- return html`
- <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text">
- ${JSON.parse(this.toolCall?.input)?.thoughts?.split("\n")[0]}
- </span>
- <div slot="input" class="thought-bubble">
- <div class="markdown-content">
- ${unsafeHTML(
- renderMarkdown(JSON.parse(this.toolCall?.input)?.thoughts),
- )}
- </div>
- </div>
- </sketch-tool-card>
- `;
+ const thoughts = JSON.parse(this.toolCall?.input)?.thoughts || "";
+
+ const summaryContent = html`<span
+ class="overflow-hidden text-ellipsis font-mono"
+ >
+ ${thoughts.split("\n")[0]}
+ </span>`;
+
+ const inputContent = html`<div
+ class="overflow-x-auto mb-1 font-mono px-2 py-1 bg-gray-200 rounded select-text cursor-text text-sm leading-relaxed"
+ >
+ <div class="markdown-content">
+ ${unsafeHTML(renderMarkdown(thoughts))}
+ </div>
+ </div>`;
+
+ return html`<sketch-tool-card-base
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ .summaryContent=${summaryContent}
+ .inputContent=${inputContent}
+ ></sketch-tool-card-base>`;
}
}
@customElement("sketch-tool-card-set-slug")
-export class SketchToolCardSetSlug extends LitElement {
+export class SketchToolCardSetSlug extends SketchTailwindElement {
@property() toolCall: ToolCall;
@property() open: boolean;
- static styles = css`
- .summary-text {
- font-style: italic;
- }
- pre {
- display: inline;
- font-family: monospace;
- background: rgb(236, 236, 236);
- padding: 2px 4px;
- border-radius: 2px;
- margin: 0;
- }
- `;
-
render() {
const inputData = JSON.parse(this.toolCall?.input || "{}");
- return html`
- <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text">
- Slug: "${inputData.slug}"
- </span>
- <div slot="input">
- <div>Set slug to: <b>${inputData.slug}</b></div>
- </div>
- </sketch-tool-card>
- `;
+
+ const summaryContent = html`<span class="italic">
+ Slug: "${inputData.slug}"
+ </span>`;
+
+ const inputContent = html`<div>Set slug to: <b>${inputData.slug}</b></div>`;
+
+ return html`<sketch-tool-card-base
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ .summaryContent=${summaryContent}
+ .inputContent=${inputContent}
+ ></sketch-tool-card-base>`;
}
}
@customElement("sketch-tool-card-commit-message-style")
-export class SketchToolCardCommitMessageStyle extends LitElement {
+export class SketchToolCardCommitMessageStyle extends SketchTailwindElement {
@property()
toolCall: ToolCall;
@@ -610,19 +297,6 @@
@property()
state: State;
- static styles = css`
- .summary-text {
- font-style: italic;
- }
- pre {
- display: inline;
- font-family: monospace;
- background: rgb(236, 236, 236);
- padding: 2px 4px;
- border-radius: 2px;
- margin: 0;
- }
- `;
constructor() {
super();
}
@@ -636,70 +310,19 @@
}
render() {
- return html`
- <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- </sketch-tool-card>
- `;
+ return html`<sketch-tool-card-base
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ ></sketch-tool-card-base>`;
}
}
@customElement("sketch-tool-card-multiple-choice")
-export class SketchToolCardMultipleChoice extends LitElement {
+export class SketchToolCardMultipleChoice extends SketchTailwindElement {
@property() toolCall: ToolCall;
@property() open: boolean;
@property() selectedOption: MultipleChoiceOption = null;
- static styles = css`
- .options-container {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- gap: 8px;
- margin: 10px 0;
- }
- .option {
- display: inline-flex;
- align-items: center;
- padding: 8px 12px;
- border-radius: 4px;
- background-color: #f5f5f5;
- cursor: pointer;
- transition: all 0.2s;
- border: 1px solid transparent;
- user-select: none;
- }
- .option:hover {
- background-color: #e0e0e0;
- border-color: #ccc;
- transform: translateY(-1px);
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
- .option:active {
- transform: translateY(0);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
- background-color: #d5d5d5;
- }
- .option.selected {
- background-color: #e3f2fd;
- border-color: #2196f3;
- border-width: 1px;
- border-style: solid;
- }
- .option-checkmark {
- margin-left: 6px;
- color: #2196f3;
- }
- .summary-text {
- font-style: italic;
- padding: 0.5em;
- }
- .summary-text strong {
- font-style: normal;
- color: #2196f3;
- font-weight: 600;
- }
- `;
-
connectedCallback() {
super.connectedCallback();
this.updateSelectedOption();
@@ -754,49 +377,46 @@
const summaryContent =
this.selectedOption !== null
- ? html`<span class="summary-text">
- ${question}: <strong>${this.selectedOption.caption}</strong>
+ ? html`<span class="italic p-2">
+ ${question}:
+ <strong class="not-italic text-blue-600 font-semibold"
+ >${this.selectedOption.caption}</strong
+ >
</span>`
- : html`<span class="summary-text">${question}</span>`;
+ : html`<span class="italic p-2">${question}</span>`;
- return html`
- <div class="multiple-choice-card">
- ${summaryContent}
- <div class="options-container">
- ${choices.map((choice) => {
- const isSelected =
- this.selectedOption !== null && this.selectedOption === choice;
- return html`
- <div
- class="option ${isSelected ? "selected" : ""}"
- @click=${() => this.handleOptionClick(choice)}
- title="${choice.responseText}"
- >
- <span class="option-label">${choice.caption}</span>
- ${isSelected
- ? html`<span class="option-checkmark">✓</span>`
- : ""}
- </div>
- `;
- })}
- </div>
- </div>
- `;
+ const inputContent = html`<div class="flex flex-row flex-wrap gap-2 my-2">
+ ${choices.map((choice) => {
+ const isSelected =
+ this.selectedOption !== null && this.selectedOption === choice;
+ return html`
+ <div
+ class="inline-flex items-center px-3 py-2 rounded cursor-pointer transition-all duration-200 border select-none ${isSelected
+ ? "bg-blue-50 border-blue-500"
+ : "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"}"
+ @click=${() => this.handleOptionClick(choice)}
+ title="${choice.responseText}"
+ >
+ <span class="option-label">${choice.caption}</span>
+ ${isSelected
+ ? html`<span class="ml-1.5 text-blue-600">✓</span>`
+ : ""}
+ </div>
+ `;
+ })}
+ </div>`;
+
+ return html`<div class="multiple-choice-card">
+ ${summaryContent} ${inputContent}
+ </div>`;
}
}
@customElement("sketch-tool-card-todo-write")
-export class SketchToolCardTodoWrite extends LitElement {
+export class SketchToolCardTodoWrite extends SketchTailwindElement {
@property() toolCall: ToolCall;
@property() open: boolean;
- static styles = css`
- .summary-text {
- font-style: italic;
- color: #666;
- }
- `;
-
render() {
const inputData = JSON.parse(this.toolCall?.input || "{}");
const tasks = inputData.tasks || [];
@@ -816,136 +436,126 @@
})
.join(" ");
- return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text"> ${circles} </span>
- <div slot="result">
- <pre>${this.toolCall?.result_message?.tool_result}</pre>
- </div>
- </sketch-tool-card>`;
+ const summaryContent = html`<span class="italic text-gray-600">
+ ${circles}
+ </span>`;
+ const resultContent = this.toolCall?.result_message?.tool_result
+ ? createPreElement(this.toolCall.result_message.tool_result)
+ : "";
+
+ return html`<sketch-tool-card-base
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ .summaryContent=${summaryContent}
+ .resultContent=${resultContent}
+ ></sketch-tool-card-base>`;
}
}
@customElement("sketch-tool-card-keyword-search")
-export class SketchToolCardKeywordSearch extends LitElement {
+export class SketchToolCardKeywordSearch extends SketchTailwindElement {
@property() toolCall: ToolCall;
@property() open: boolean;
- static styles = css`
- .summary-container {
- display: flex;
- flex-direction: column;
- gap: 2px;
- width: 100%;
- max-width: 100%;
- overflow: hidden;
- }
- .query-line {
- color: #333;
- font-family: inherit;
- font-size: 12px;
- font-weight: normal;
- white-space: normal;
- word-wrap: break-word;
- word-break: break-word;
- overflow-wrap: break-word;
- line-height: 1.2;
- }
- .keywords-line {
- color: #666;
- font-family: inherit;
- font-size: 11px;
- font-weight: normal;
- white-space: normal;
- word-wrap: break-word;
- word-break: break-word;
- overflow-wrap: break-word;
- line-height: 1.2;
- margin-top: 1px;
- }
- `;
-
render() {
const inputData = JSON.parse(this.toolCall?.input || "{}");
const query = inputData.query || "";
const searchTerms = inputData.search_terms || [];
- return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <div slot="summary" class="summary-container">
- <div class="query-line">🔍 ${query}</div>
- <div class="keywords-line">🗝️ ${searchTerms.join(", ")}</div>
+ const summaryContent = html`<div
+ class="flex flex-col gap-0.5 w-full max-w-full overflow-hidden"
+ >
+ <div
+ class="text-gray-800 text-xs normal-case whitespace-normal break-words leading-tight"
+ >
+ 🔍 ${query}
</div>
- <div slot="input">
- <div><strong>Query:</strong> ${query}</div>
- <div><strong>Search terms:</strong> ${searchTerms.join(", ")}</div>
+ <div
+ class="text-gray-600 text-xs normal-case whitespace-normal break-words leading-tight mt-px"
+ >
+ 🗝️ ${searchTerms.join(", ")}
</div>
- <div slot="result">
- <pre>${this.toolCall?.result_message?.tool_result}</pre>
- </div>
- </sketch-tool-card>`;
+ </div>`;
+
+ const inputContent = html`<div>
+ <div><strong>Query:</strong> ${query}</div>
+ <div><strong>Search terms:</strong> ${searchTerms.join(", ")}</div>
+ </div>`;
+
+ const resultContent = this.toolCall?.result_message?.tool_result
+ ? createPreElement(this.toolCall.result_message.tool_result)
+ : "";
+
+ return html`<sketch-tool-card-base
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ .summaryContent=${summaryContent}
+ .inputContent=${inputContent}
+ .resultContent=${resultContent}
+ ></sketch-tool-card-base>`;
}
}
@customElement("sketch-tool-card-todo-read")
-export class SketchToolCardTodoRead extends LitElement {
+export class SketchToolCardTodoRead extends SketchTailwindElement {
@property() toolCall: ToolCall;
@property() open: boolean;
- static styles = css`
- .summary-text {
- font-style: italic;
- color: #666;
- }
- `;
-
render() {
- return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text"> Read todo list </span>
- <div slot="result">
- <pre>${this.toolCall?.result_message?.tool_result}</pre>
- </div>
- </sketch-tool-card>`;
+ const summaryContent = html`<span class="italic text-gray-600">
+ Read todo list
+ </span>`;
+ const resultContent = this.toolCall?.result_message?.tool_result
+ ? createPreElement(this.toolCall.result_message.tool_result)
+ : "";
+
+ return html`<sketch-tool-card-base
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ .summaryContent=${summaryContent}
+ .resultContent=${resultContent}
+ ></sketch-tool-card-base>`;
}
}
@customElement("sketch-tool-card-generic")
-export class SketchToolCardGeneric extends LitElement {
+export class SketchToolCardGeneric extends SketchTailwindElement {
@property() toolCall: ToolCall;
@property() open: boolean;
render() {
- return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span
- slot="summary"
- style="display: block; white-space: normal; word-break: break-word; overflow-wrap: break-word; max-width: 100%; width: 100%;"
- >${this.toolCall?.input}</span
- >
- <div
- slot="input"
- style="max-width: 100%; overflow-wrap: break-word; word-break: break-word;"
- >
- Input:
- <pre
- style="max-width: 100%; white-space: pre-wrap; overflow-wrap: break-word; word-break: break-word;"
- >
-${this.toolCall?.input}</pre
- >
- </div>
- <div
- slot="result"
- style="max-width: 100%; overflow-wrap: break-word; word-break: break-word;"
- >
- Result:
- ${this.toolCall?.result_message?.tool_result
- ? html`<pre>${this.toolCall?.result_message.tool_result}</pre>`
- : ""}
- </div>
- </sketch-tool-card>`;
+ const summaryContent = html`<span
+ class="block whitespace-normal break-words max-w-full w-full"
+ >
+ ${this.toolCall?.input}
+ </span>`;
+
+ const inputContent = html`<div class="max-w-full break-words">
+ Input:
+ ${createPreElement(
+ this.toolCall?.input || "",
+ "max-w-full whitespace-pre-wrap break-words",
+ )}
+ </div>`;
+
+ const resultContent = this.toolCall?.result_message?.tool_result
+ ? html`<div class="max-w-full break-words">
+ Result: ${createPreElement(this.toolCall.result_message.tool_result)}
+ </div>`
+ : "";
+
+ return html`<sketch-tool-card-base
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ .summaryContent=${summaryContent}
+ .inputContent=${inputContent}
+ .resultContent=${resultContent}
+ ></sketch-tool-card-base>`;
}
}
declare global {
interface HTMLElementTagNameMap {
- "sketch-tool-card": SketchToolCard;
"sketch-tool-card-generic": SketchToolCardGeneric;
"sketch-tool-card-bash": SketchToolCardBash;
"sketch-tool-card-codereview": SketchToolCardCodeReview;
@@ -958,6 +568,5 @@
"sketch-tool-card-todo-write": SketchToolCardTodoWrite;
"sketch-tool-card-todo-read": SketchToolCardTodoRead;
"sketch-tool-card-keyword-search": SketchToolCardKeywordSearch;
- // TODO: We haven't implemented this for browser tools.
}
}