loop: add todo checklist
This should improve Sketch's executive function and user communication.
diff --git a/webui/src/web-components/sketch-app-shell.ts b/webui/src/web-components/sketch-app-shell.ts
index 7ea43c7..81d2fad 100644
--- a/webui/src/web-components/sketch-app-shell.ts
+++ b/webui/src/web-components/sketch-app-shell.ts
@@ -17,6 +17,7 @@
import "./sketch-terminal";
import "./sketch-timeline";
import "./sketch-view-mode-select";
+import "./sketch-todo-panel";
import { createRef, ref } from "lit/directives/ref.js";
import { SketchChatInput } from "./sketch-chat-input";
@@ -139,6 +140,15 @@
height: 100%; /* Ensure it takes full height of parent */
}
+ /* Adjust view container when todo panel is visible in chat mode */
+ #view-container-inner.with-todo-panel {
+ max-width: none;
+ width: 100%;
+ margin: 0;
+ padding-left: 20px;
+ padding-right: 20px;
+ }
+
#chat-input {
align-self: flex-end;
width: 100%;
@@ -195,6 +205,87 @@
display: flex;
flex-direction: column;
width: 100%;
+ height: 100%;
+ }
+
+ /* Chat timeline container - takes full width, memory panel will be positioned separately */
+ .chat-timeline-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ margin-right: 0; /* Default - no memory panel */
+ transition: margin-right 0.2s ease; /* Smooth transition */
+ }
+
+ /* Adjust chat timeline container when todo panel is visible */
+ .chat-timeline-container.with-todo-panel {
+ margin-right: 400px; /* Make space for fixed todo panel */
+ width: calc(100% - 400px); /* Explicitly set width to prevent overlap */
+ }
+
+ /* Todo panel container - fixed to right side */
+ .todo-panel-container {
+ position: fixed;
+ top: 48px; /* Below top banner */
+ right: 15px; /* Leave space for scroll bar */
+ width: 400px;
+ bottom: var(
+ --chat-input-height,
+ 90px
+ ); /* Dynamic height based on chat input size */
+ background-color: #fafafa;
+ border-left: 1px solid #e0e0e0;
+ z-index: 100;
+ display: none; /* Hidden by default */
+ transition: bottom 0.2s ease; /* Smooth transition when height changes */
+ /* Add fuzzy gradient at bottom to blend with text entry */
+ background: linear-gradient(
+ to bottom,
+ #fafafa 0%,
+ #fafafa 90%,
+ rgba(250, 250, 250, 0.5) 95%,
+ rgba(250, 250, 250, 0.2) 100%
+ );
+ }
+
+ .todo-panel-container.visible {
+ display: block;
+ }
+
+ /* Responsive adjustments for todo panel */
+ @media (max-width: 1200px) {
+ .todo-panel-container {
+ width: 350px;
+ /* bottom is still controlled by --chat-input-height CSS variable */
+ }
+ .chat-timeline-container.with-todo-panel {
+ margin-right: 350px;
+ width: calc(100% - 350px);
+ }
+ }
+
+ @media (max-width: 900px) {
+ .todo-panel-container {
+ width: 300px;
+ /* bottom is still controlled by --chat-input-height CSS variable */
+ }
+ .chat-timeline-container.with-todo-panel {
+ margin-right: 300px;
+ width: calc(100% - 300px);
+ }
+ }
+
+ /* On very small screens, hide todo panel or make it overlay */
+ @media (max-width: 768px) {
+ .todo-panel-container.visible {
+ display: none; /* Hide on mobile */
+ }
+ .chat-timeline-container.with-todo-panel {
+ margin-right: 0;
+ width: 100%;
+ }
}
/* Monaco diff2 view needs to take all available space */
@@ -345,6 +436,13 @@
@state()
private _windowFocused: boolean = document.hasFocus();
+ // Track if the todo panel should be visible
+ @state()
+ private _todoPanelVisible: boolean = false;
+
+ // ResizeObserver for tracking chat input height changes
+ private chatInputResizeObserver: ResizeObserver | null = null;
+
@property()
connectionErrorMessage: string = "";
@@ -476,6 +574,12 @@
}
}, 100);
}
+
+ // Check if todo panel should be visible on initial load
+ this.checkTodoPanelVisibility();
+
+ // Set up ResizeObserver for chat input to update todo panel height
+ this.setupChatInputObserver();
}
// See https://lit.dev/docs/components/lifecycle/
@@ -508,6 +612,12 @@
this.mutationObserver.disconnect();
this.mutationObserver = null;
}
+
+ // Disconnect chat input resize observer if it exists
+ if (this.chatInputResizeObserver) {
+ this.chatInputResizeObserver.disconnect();
+ this.chatInputResizeObserver = null;
+ }
}
updateUrlForViewMode(mode: ViewMode): void {
@@ -787,6 +897,48 @@
}
}
+ // Check if todo panel should be visible based on latest todo content from messages or state
+ private checkTodoPanelVisibility(): void {
+ // Find the latest todo content from messages first
+ let latestTodoContent = "";
+ for (let i = this.messages.length - 1; i >= 0; i--) {
+ const message = this.messages[i];
+ if (message.todo_content !== undefined) {
+ latestTodoContent = message.todo_content || "";
+ break;
+ }
+ }
+
+ // If no todo content found in messages, check the current state
+ if (latestTodoContent === "" && this.containerState?.todo_content) {
+ latestTodoContent = this.containerState.todo_content;
+ }
+
+ // Parse the todo data to check if there are any actual todos
+ let hasTodos = false;
+ if (latestTodoContent.trim()) {
+ try {
+ const todoData = JSON.parse(latestTodoContent);
+ hasTodos = todoData.items && todoData.items.length > 0;
+ } catch (error) {
+ // Invalid JSON, treat as no todos
+ hasTodos = false;
+ }
+ }
+
+ this._todoPanelVisible = hasTodos;
+
+ // Update todo panel content if visible
+ if (hasTodos) {
+ const todoPanel = this.shadowRoot?.querySelector(
+ "sketch-todo-panel",
+ ) as any;
+ if (todoPanel && todoPanel.updateTodoContent) {
+ todoPanel.updateTodoContent(latestTodoContent);
+ }
+ }
+ }
+
private handleDataChanged(eventData: {
state: State;
newMessages: AgentMessage[];
@@ -834,6 +986,14 @@
}
}
}
+
+ // Check if todo panel should be visible after agent loop iteration
+ this.checkTodoPanelVisibility();
+
+ // Ensure chat input observer is set up when new data comes in
+ if (!this.chatInputResizeObserver) {
+ this.setupChatInputObserver();
+ }
}
private handleConnectionStatusChanged(
@@ -973,6 +1133,40 @@
private scrollContainerRef = createRef<HTMLElement>();
+ /**
+ * Set up ResizeObserver to monitor chat input height changes
+ */
+ private setupChatInputObserver(): void {
+ // Wait for DOM to be ready
+ this.updateComplete.then(() => {
+ const chatInputElement = this.shadowRoot?.querySelector("#chat-input");
+ if (chatInputElement && !this.chatInputResizeObserver) {
+ this.chatInputResizeObserver = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ this.updateTodoPanelHeight(entry.contentRect.height);
+ }
+ });
+
+ this.chatInputResizeObserver.observe(chatInputElement);
+
+ // Initial height calculation
+ const rect = chatInputElement.getBoundingClientRect();
+ this.updateTodoPanelHeight(rect.height);
+ }
+ });
+ }
+
+ /**
+ * Update the CSS custom property that controls todo panel bottom position
+ */
+ private updateTodoPanelHeight(chatInputHeight: number): void {
+ // Add some padding (20px) between todo panel and chat input
+ const bottomOffset = chatInputHeight;
+
+ // Update the CSS custom property on the host element
+ this.style.setProperty("--chat-input-height", `${bottomOffset}px`);
+ }
+
render() {
return html`
<div id="top-banner">
@@ -1081,17 +1275,41 @@
</div>
<div id="view-container" ${ref(this.scrollContainerRef)}>
- <div id="view-container-inner">
+ <div
+ id="view-container-inner"
+ class="${this._todoPanelVisible && this.viewMode === "chat"
+ ? "with-todo-panel"
+ : ""}"
+ >
<div
class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}"
>
- <sketch-timeline
- .messages=${this.messages}
- .scrollContainer=${this.scrollContainerRef}
- .agentState=${this.containerState?.agent_state}
- .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
- .toolCalls=${this.containerState?.outstanding_tool_calls || []}
- ></sketch-timeline>
+ <div
+ class="chat-timeline-container ${this._todoPanelVisible &&
+ this.viewMode === "chat"
+ ? "with-todo-panel"
+ : ""}"
+ >
+ <sketch-timeline
+ .messages=${this.messages}
+ .scrollContainer=${this.scrollContainerRef}
+ .agentState=${this.containerState?.agent_state}
+ .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
+ .toolCalls=${this.containerState?.outstanding_tool_calls || []}
+ ></sketch-timeline>
+ </div>
+ </div>
+
+ <!-- Todo panel positioned outside the main flow - only visible in chat view -->
+ <div
+ class="todo-panel-container ${this._todoPanelVisible &&
+ this.viewMode === "chat"
+ ? "visible"
+ : ""}"
+ >
+ <sketch-todo-panel
+ .visible=${this._todoPanelVisible && this.viewMode === "chat"}
+ ></sketch-todo-panel>
</div>
<div
class="diff-view ${this.viewMode === "diff" ? "view-active" : ""}"
@@ -1175,6 +1393,9 @@
this.containerStatusElement.updateLastCommitInfo(this.messages);
}
}
+
+ // Set up chat input height observer for todo panel
+ this.setupChatInputObserver();
}
}
diff --git a/webui/src/web-components/sketch-timeline.ts b/webui/src/web-components/sketch-timeline.ts
index 1b27570..6a0f683 100644
--- a/webui/src/web-components/sketch-timeline.ts
+++ b/webui/src/web-components/sketch-timeline.ts
@@ -74,9 +74,9 @@
}
#jump-to-latest {
display: none;
- position: fixed;
- bottom: 100px;
- right: 0;
+ position: absolute;
+ bottom: 20px;
+ right: 20px;
background: rgb(33, 150, 243);
color: white;
border-radius: 8px;
@@ -85,6 +85,7 @@
font-size: x-large;
opacity: 0.5;
cursor: pointer;
+ z-index: 50;
}
#jump-to-latest:hover {
opacity: 1;
@@ -292,29 +293,31 @@
// Check if messages array is empty and render welcome box if it is
if (this.messages.length === 0) {
return html`
- <div id="scroll-container">
- <div class="welcome-box">
- <h2 class="welcome-box-title">How to use Sketch</h2>
- <p class="welcome-box-content">
- Sketch is an agentic coding assistant.
- </p>
+ <div style="position: relative; height: 100%;">
+ <div id="scroll-container">
+ <div class="welcome-box">
+ <h2 class="welcome-box-title">How to use Sketch</h2>
+ <p class="welcome-box-content">
+ Sketch is an agentic coding assistant.
+ </p>
- <p class="welcome-box-content">
- Sketch has created a container with your repo.
- </p>
+ <p class="welcome-box-content">
+ Sketch has created a container with your repo.
+ </p>
- <p class="welcome-box-content">
- Ask it to implement a task or answer a question in the chat box
- below. It can edit and run your code, all in the container. Sketch
- will create commits in a newly created git branch, which you can
- look at and comment on in the Diff tab. Once you're done, you'll
- find that branch available in your (original) repo.
- </p>
- <p class="welcome-box-content">
- Because Sketch operates a container per session, you can run
- Sketch in parallel to work on multiple ideas or even the same idea
- with different approaches.
- </p>
+ <p class="welcome-box-content">
+ Ask it to implement a task or answer a question in the chat box
+ below. It can edit and run your code, all in the container. Sketch
+ will create commits in a newly created git branch, which you can
+ look at and comment on in the Diff tab. Once you're done, you'll
+ find that branch available in your (original) repo.
+ </p>
+ <p class="welcome-box-content">
+ Because Sketch operates a container per session, you can run
+ Sketch in parallel to work on multiple ideas or even the same idea
+ with different approaches.
+ </p>
+ </div>
</div>
</div>
`;
@@ -325,56 +328,58 @@
this.llmCalls > 0 || (this.toolCalls && this.toolCalls.length > 0);
return html`
- <div id="scroll-container">
- <div class="timeline-container">
- ${repeat(
- this.messages.filter((msg) => !msg.hide_output),
- this.messageKey,
- (message, index) => {
- let previousMessageIndex =
- this.messages.findIndex((m) => m === message) - 1;
- let previousMessage =
- previousMessageIndex >= 0
- ? this.messages[previousMessageIndex]
- : undefined;
-
- // Skip hidden messages when determining previous message
- while (previousMessage && previousMessage.hide_output) {
- previousMessageIndex--;
- previousMessage =
+ <div style="position: relative; height: 100%;">
+ <div id="scroll-container">
+ <div class="timeline-container">
+ ${repeat(
+ this.messages.filter((msg) => !msg.hide_output),
+ this.messageKey,
+ (message, index) => {
+ let previousMessageIndex =
+ this.messages.findIndex((m) => m === message) - 1;
+ let previousMessage =
previousMessageIndex >= 0
? this.messages[previousMessageIndex]
: undefined;
- }
- return html`<sketch-timeline-message
- .message=${message}
- .previousMessage=${previousMessage}
- .open=${false}
- ></sketch-timeline-message>`;
- },
- )}
- ${isThinking
- ? html`
- <div class="thinking-indicator">
- <div class="thinking-bubble">
- <div class="thinking-dots">
- <div class="dot"></div>
- <div class="dot"></div>
- <div class="dot"></div>
+ // Skip hidden messages when determining previous message
+ while (previousMessage && previousMessage.hide_output) {
+ previousMessageIndex--;
+ previousMessage =
+ previousMessageIndex >= 0
+ ? this.messages[previousMessageIndex]
+ : undefined;
+ }
+
+ return html`<sketch-timeline-message
+ .message=${message}
+ .previousMessage=${previousMessage}
+ .open=${false}
+ ></sketch-timeline-message>`;
+ },
+ )}
+ ${isThinking
+ ? html`
+ <div class="thinking-indicator">
+ <div class="thinking-bubble">
+ <div class="thinking-dots">
+ <div class="dot"></div>
+ <div class="dot"></div>
+ <div class="dot"></div>
+ </div>
</div>
</div>
- </div>
- `
- : ""}
+ `
+ : ""}
+ </div>
</div>
- </div>
- <div
- id="jump-to-latest"
- class="${this.scrollingState}"
- @click=${this.scrollToBottom}
- >
- ⇩
+ <div
+ id="jump-to-latest"
+ class="${this.scrollingState}"
+ @click=${this.scrollToBottom}
+ >
+ ⇩
+ </div>
</div>
`;
}
diff --git a/webui/src/web-components/sketch-todo-panel.ts b/webui/src/web-components/sketch-todo-panel.ts
new file mode 100644
index 0000000..d8e34f0
--- /dev/null
+++ b/webui/src/web-components/sketch-todo-panel.ts
@@ -0,0 +1,259 @@
+import { css, html, LitElement } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+import { unsafeHTML } from "lit/directives/unsafe-html.js";
+import { TodoList, TodoItem } from "../types.js";
+
+@customElement("sketch-todo-panel")
+export class SketchTodoPanel extends LitElement {
+ @property()
+ visible: boolean = false;
+
+ @state()
+ private todoList: TodoList | null = null;
+
+ @state()
+ private loading: boolean = false;
+
+ @state()
+ private error: string = "";
+
+ static styles = css`
+ :host {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background-color: transparent; /* Let parent handle background */
+ overflow: hidden; /* Ensure proper clipping */
+ }
+
+ .todo-header {
+ padding: 8px 12px;
+ border-bottom: 1px solid #e0e0e0;
+ background-color: #f5f5f5;
+ font-weight: 600;
+ font-size: 13px;
+ color: #333;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ .todo-icon {
+ width: 14px;
+ height: 14px;
+ color: #666;
+ }
+
+ .todo-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px;
+ padding-bottom: 20px; /* Extra bottom padding for better scrolling */
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ font-size: 12px;
+ line-height: 1.4;
+ /* Ensure scrollbar is always accessible */
+ min-height: 0;
+ }
+
+ .todo-content.loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #666;
+ }
+
+ .todo-content.error {
+ color: #d32f2f;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .todo-content.empty {
+ color: #999;
+ font-style: italic;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ /* Todo item styling */
+ .todo-item {
+ display: flex;
+ align-items: flex-start;
+ padding: 8px;
+ margin-bottom: 6px;
+ border-radius: 4px;
+ background-color: #fff;
+ border: 1px solid #e0e0e0;
+ gap: 8px;
+ }
+
+ .todo-item.queued {
+ border-left: 3px solid #e0e0e0;
+ }
+
+ .todo-item.in-progress {
+ border-left: 3px solid #e0e0e0;
+ }
+
+ .todo-item.completed {
+ border-left: 3px solid #e0e0e0;
+ }
+
+
+
+ .todo-status-icon {
+ font-size: 14px;
+ margin-top: 1px;
+ flex-shrink: 0;
+ }
+
+
+
+ .todo-main {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .todo-content-text {
+ font-size: 12px;
+ line-height: 1.3;
+ color: #333;
+ word-wrap: break-word;
+ }
+
+
+
+ .todo-header-text {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ .todo-count {
+ background-color: #e0e0e0;
+ color: #666;
+ padding: 2px 6px;
+ border-radius: 10px;
+ font-size: 10px;
+ font-weight: normal;
+ }
+
+ /* Loading spinner */
+ .spinner {
+ width: 20px;
+ height: 20px;
+ border: 2px solid #f3f3f3;
+ border-top: 2px solid #3498db;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-right: 8px;
+ }
+
+ @keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+ `;
+
+ updateTodoContent(content: string) {
+ try {
+ if (!content.trim()) {
+ this.todoList = null;
+ } else {
+ this.todoList = JSON.parse(content) as TodoList;
+ }
+ this.loading = false;
+ this.error = "";
+ } catch (error) {
+ console.error("Failed to parse todo content:", error);
+ this.error = "Failed to parse todo data";
+ this.todoList = null;
+ this.loading = false;
+ }
+ }
+
+
+
+ private renderTodoItem(item: TodoItem) {
+ const statusIcon = {
+ queued: '⚪',
+ 'in-progress': '🦉',
+ completed: '✅'
+ }[item.status] || '?';
+
+ return html`
+ <div class="todo-item ${item.status}">
+ <div class="todo-status-icon">${statusIcon}</div>
+ <div class="todo-main">
+ <div class="todo-content-text">${item.task}</div>
+
+ </div>
+ </div>
+ `;
+ }
+
+ render() {
+ if (!this.visible) {
+ return html``;
+ }
+
+ const todoIcon = html`
+ <svg class="todo-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M9 11l3 3L22 4"></path>
+ <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
+ </svg>
+ `;
+
+ let contentElement;
+ if (this.loading) {
+ contentElement = html`
+ <div class="todo-content loading">
+ <div class="spinner"></div>
+ Loading todos...
+ </div>
+ `;
+ } else if (this.error) {
+ contentElement = html`
+ <div class="todo-content error">
+ Error: ${this.error}
+ </div>
+ `;
+ } else if (!this.todoList || !this.todoList.items || this.todoList.items.length === 0) {
+ contentElement = html`
+ <div class="todo-content empty">
+ No todos available
+ </div>
+ `;
+ } else {
+ const totalCount = this.todoList.items.length;
+ const completedCount = this.todoList.items.filter(item => item.status === 'completed').length;
+ const inProgressCount = this.todoList.items.filter(item => item.status === 'in-progress').length;
+
+ contentElement = html`
+ <div class="todo-header">
+ <div class="todo-header-text">
+ ${todoIcon}
+ <span>Sketching...</span>
+ <span class="todo-count">${completedCount}/${totalCount}</span>
+ </div>
+ </div>
+ <div class="todo-content">
+ ${this.todoList.items.map(item => this.renderTodoItem(item))}
+ </div>
+ `;
+ }
+
+ return html`
+ ${contentElement}
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-todo-panel": SketchTodoPanel;
+ }
+}
\ No newline at end of file
diff --git a/webui/src/web-components/sketch-tool-calls.ts b/webui/src/web-components/sketch-tool-calls.ts
index a5afa50..d64b0ca 100644
--- a/webui/src/web-components/sketch-tool-calls.ts
+++ b/webui/src/web-components/sketch-tool-calls.ts
@@ -133,6 +133,16 @@
.open=${open}
.toolCall=${toolCall}
></sketch-tool-card-about-sketch>`;
+ case "todo_write":
+ return html`<sketch-tool-card-todo-write
+ .open=${open}
+ .toolCall=${toolCall}
+ ></sketch-tool-card-todo-write>`;
+ case "todo_read":
+ return html`<sketch-tool-card-todo-read
+ .open=${open}
+ .toolCall=${toolCall}
+ ></sketch-tool-card-todo-read>`;
}
return html`<sketch-tool-card-generic
.open=${open}
diff --git a/webui/src/web-components/sketch-tool-card.ts b/webui/src/web-components/sketch-tool-card.ts
index 584f827..307686d 100644
--- a/webui/src/web-components/sketch-tool-card.ts
+++ b/webui/src/web-components/sketch-tool-card.ts
@@ -742,6 +742,67 @@
}
}
+@customElement("sketch-tool-card-todo-write")
+export class SketchToolCardTodoWrite extends LitElement {
+ @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 || [];
+
+ // Generate circles based on task status
+ const circles = tasks.map(task => {
+ switch(task.status) {
+ case 'completed': return '●'; // full circle
+ case 'in-progress': return '◐'; // half circle
+ case 'queued':
+ default: return '○'; // empty circle
+ }
+ }).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>`;
+ }
+}
+
+@customElement("sketch-tool-card-todo-read")
+export class SketchToolCardTodoRead extends LitElement {
+ @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>`;
+ }
+}
+
@customElement("sketch-tool-card-generic")
export class SketchToolCardGeneric extends LitElement {
@property() toolCall: ToolCall;
@@ -790,6 +851,8 @@
"sketch-tool-card-title": SketchToolCardTitle;
"sketch-tool-card-precommit": SketchToolCardPrecommit;
"sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
+ "sketch-tool-card-todo-write": SketchToolCardTodoWrite;
+ "sketch-tool-card-todo-read": SketchToolCardTodoRead;
// TODO: We haven't implemented this for browser tools.
}
}