loop: add todo checklist

This should improve Sketch's executive function and user communication.
diff --git a/webui/src/types.ts b/webui/src/types.ts
index 0e668ce..5031739 100644
--- a/webui/src/types.ts
+++ b/webui/src/types.ts
@@ -46,6 +46,7 @@
 	elapsed?: Duration | null;
 	turnDuration?: Duration | null;
 	hide_output?: boolean;
+	todo_content?: string | null;
 	idx: number;
 }
 
@@ -85,6 +86,17 @@
 	inside_os?: string;
 	outside_working_dir?: string;
 	inside_working_dir?: string;
+	todo_content?: string;
+}
+
+export interface TodoItem {
+	id: string;
+	task: string;
+	status: string;
+}
+
+export interface TodoList {
+	items: TodoItem[] | null;
 }
 
 export interface MultipleChoiceOption {
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.
   }
 }