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();
   }
 }