diff --git a/web/src/shell/ChatHeader.test.tsx b/web/src/shell/ChatHeader.test.tsx
index 5267ca19e9..05626ea5dc 100644
--- a/web/src/shell/ChatHeader.test.tsx
+++ b/web/src/shell/ChatHeader.test.tsx
@@ -21,13 +21,9 @@ const mobileMenu = {
filesPanelOpen: false,
subagentsPanelOpen: false,
shellsPanelOpen: false,
- todosPanelOpen: false,
hideTerminalsTab: false,
showShellsTab: false,
terminalsLength: 0,
- todosSupported: false,
- todosCompleted: 0,
- todosTotal: 0,
debugMode: false,
changedCount: 0,
subagentsWorking: 0,
@@ -35,7 +31,6 @@ const mobileMenu = {
onOpenFiles: () => {},
onOpenShells: () => {},
onOpenSubagents: () => {},
- onOpenTodos: () => {},
onOpenMainExecutionLog: () => {},
};
diff --git a/web/src/shell/ChatHeader.tsx b/web/src/shell/ChatHeader.tsx
index 8c68ea58e5..dca4f9f167 100644
--- a/web/src/shell/ChatHeader.tsx
+++ b/web/src/shell/ChatHeader.tsx
@@ -5,7 +5,6 @@ import {
FileIcon,
InfoIcon,
ListIcon,
- ListTodoIcon,
PanelLeftIcon,
PanelRightCloseIcon,
PanelRightIcon,
@@ -51,20 +50,12 @@ interface MobileSessionMenuProps {
subagentsPanelOpen: boolean;
/** True while the mobile shells drawer is open. */
shellsPanelOpen: boolean;
- /** True while the mobile tasks drawer is open. */
- todosPanelOpen: boolean;
/** Hide the Shells entry (claude-native sub-agents only). */
hideTerminalsTab: boolean;
/** Whether the Shells entry is available. */
showShellsTab: boolean;
/** Number of open terminals (entry badge). */
terminalsLength: number;
- /** Whether the session publishes a todo list (gates the Tasks entry). */
- todosSupported: boolean;
- /** Completed todo count (Tasks entry badge numerator). */
- todosCompleted: number;
- /** Total todo count (Tasks entry badge denominator + visibility). */
- todosTotal: number;
/** Debug mode — surfaces the Logs entry. */
debugMode: boolean;
/** Changed-file count (Files entry badge). */
@@ -82,8 +73,6 @@ interface MobileSessionMenuProps {
onOpenShells: () => void;
/** Open the mobile agents drawer. */
onOpenSubagents: () => void;
- /** Open the mobile tasks drawer. */
- onOpenTodos: () => void;
/** Open the main execution-log push panel. */
onOpenMainExecutionLog: () => void;
}
@@ -131,7 +120,7 @@ interface ChatHeaderProps {
showFilesPanel: boolean;
/**
* Whether the right workspace rail has at least one available tab
- * (files, terminals, sub-agents, or todos). Gates the desktop
+ * (files, terminals, or sub-agents). Gates the desktop
* collapse toggle — with no rail content the panel doesn't mount
* (see AppShell), so a toggle would flip an invisible card.
*/
@@ -421,7 +410,6 @@ export function ChatHeader({
!mobileMenu.filesPanelOpen &&
!mobileMenu.subagentsPanelOpen &&
!mobileMenu.shellsPanelOpen &&
- !mobileMenu.todosPanelOpen &&
(hasRailContent || mobileMenu.debugMode) && (
@@ -495,18 +483,6 @@ export function ChatHeader({
)}
)}
- {mobileMenu.todosSupported && mobileMenu.todosTotal > 0 && (
-
-
- Tasks
-
- {mobileMenu.todosCompleted}/{mobileMenu.todosTotal}
-
-
- )}
{mobileMenu.debugMode && (
s.todos). Mock it to feed a
-// controllable todos array per test.
-const h = vi.hoisted(() => ({ todos: [] as TodoItem[] }));
-vi.mock("@/store/chatStore", () => ({
- useChatStore: (selector: (s: { todos: TodoItem[] }) => unknown) => selector({ todos: h.todos }),
-}));
-
-import { TodoPanel } from "./TodoPanel";
-
-afterEach(() => {
- cleanup();
- h.todos = [];
-});
-
-describe("TodoPanel", () => {
- it("renders nothing when the todo list is empty", () => {
- // WHY: the panel must occupy no space for sessions with no todos — it
- // returns null, so the container has no DOM children.
- h.todos = [];
- const { container } = render();
- expect(container.firstChild).toBeNull();
- });
-
- it("renders one list item per todo with its content", () => {
- // WHY: confirms the map over todos renders every item's content text.
- h.todos = [
- { content: "Write tests", status: "pending", activeForm: "Writing tests" },
- { content: "Ship it", status: "completed", activeForm: "Shipping it" },
- ];
- render();
- expect(screen.getByText("Write tests")).toBeInTheDocument();
- expect(screen.getByText("Ship it")).toBeInTheDocument();
- expect(screen.getAllByRole("listitem")).toHaveLength(2);
- });
-
- it("strikes through and dims a completed todo", () => {
- // WHY: completed todos get line-through + opacity-50; a regression in the
- // status-conditional classes would leave them looking active.
- h.todos = [{ content: "Done thing", status: "completed", activeForm: "Doing thing" }];
- render();
- const span = screen.getByText("Done thing");
- expect(span.className).toContain("line-through");
- expect(span.closest("li")?.className).toContain("opacity-50");
- });
-
- it("shows the activeForm subtitle for an in_progress todo when it differs", () => {
- // WHY: an in-progress item surfaces its activeForm ("Doing X") under the
- // content — this is the live-status affordance.
- h.todos = [{ content: "Build feature", status: "in_progress", activeForm: "Building feature" }];
- render();
- expect(screen.getByText("Build feature")).toBeInTheDocument();
- expect(screen.getByText("Building feature")).toBeInTheDocument();
- });
-
- it("omits the activeForm subtitle when it equals the content", () => {
- // WHY: the guard `activeForm !== content` prevents a redundant duplicate
- // line; identical text must appear exactly once.
- h.todos = [{ content: "Same text", status: "in_progress", activeForm: "Same text" }];
- render();
- expect(screen.getAllByText("Same text")).toHaveLength(1);
- });
-
- it("does not show the activeForm subtitle for a non-in_progress todo", () => {
- // WHY: the subtitle is gated on in_progress; a pending todo with a distinct
- // activeForm must not render it.
- h.todos = [{ content: "Pending thing", status: "pending", activeForm: "Pending action" }];
- render();
- expect(screen.queryByText("Pending action")).toBeNull();
- });
-});
diff --git a/web/src/shell/TodoPanel.tsx b/web/src/shell/TodoPanel.tsx
deleted file mode 100644
index a60dea6d11..0000000000
--- a/web/src/shell/TodoPanel.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import { CheckCircle2Icon, CircleIcon, CircleDotIcon } from "lucide-react";
-import { useChatStore } from "@/store/chatStore";
-import { cn } from "@/lib/utils";
-
-interface TodoItem {
- content: string;
- status: "pending" | "in_progress" | "completed";
- activeForm: string;
-}
-
-interface TodoPanelProps {
- frameless?: boolean;
-}
-
-function TodoIcon({ status }: { status: TodoItem["status"] }) {
- if (status === "completed") {
- return ;
- }
- if (status === "in_progress") {
- return ;
- }
- return ;
-}
-
-/**
- * Displays the active task list published by any harness.
- *
- * Reads from `useChatStore.todos`, populated by the session snapshot and
- * `session.todos` SSE updates. Renders nothing while the list is empty.
- */
-export function TodoPanel({ frameless = false }: TodoPanelProps) {
- const todos = useChatStore((s) => s.todos);
-
- if (todos.length === 0) return null;
-
- return (
-
-
- {todos.map((todo, i) => (
- -
-
-
-
- {todo.content}
-
- {todo.status === "in_progress" &&
- todo.activeForm &&
- todo.activeForm !== todo.content && (
-
- {todo.activeForm}
-
- )}
-
-
- ))}
-
-
- );
-}
diff --git a/web/src/shell/WorkspacePanel.test.tsx b/web/src/shell/WorkspacePanel.test.tsx
index 8676b417ee..2d6353f64d 100644
--- a/web/src/shell/WorkspacePanel.test.tsx
+++ b/web/src/shell/WorkspacePanel.test.tsx
@@ -27,9 +27,6 @@ vi.mock("./InlineTerminalsSection", () => ({
vi.mock("./SubagentsPanel", () => ({
SubagentsPanel: () => ,
}));
-vi.mock("./TodoPanel", () => ({
- TodoPanel: () => ,
-}));
vi.mock("@/components/BrowserPane/BrowserPane", () => ({
BrowserPane: ({ conversationId }: { conversationId: string }) => (
{conversationId}
@@ -110,9 +107,6 @@ function renderWorkspace(
terminalsLength={0}
subagentsWorking={0}
agentCount={1}
- todosSupported={false}
- todosCompleted={0}
- todosTotal={0}
rootSessionId={null}
selectedFilePath={overrides.selectedFilePath ?? null}
openFiles={overrides.openFiles ?? []}
diff --git a/web/src/shell/WorkspacePanel.tsx b/web/src/shell/WorkspacePanel.tsx
index faefe320e1..2c2bb8b5a9 100644
--- a/web/src/shell/WorkspacePanel.tsx
+++ b/web/src/shell/WorkspacePanel.tsx
@@ -4,7 +4,6 @@ import {
FileIcon,
FilesIcon,
GlobeIcon,
- ListTodoIcon,
Loader2Icon,
MaximizeIcon,
MinimizeIcon,
@@ -38,7 +37,6 @@ import { FileViewer } from "./FileViewer";
import type { ChangedSort } from "./FlatFileList";
import { InlineTerminalsSection } from "./InlineTerminalsSection";
import { SubagentsPanel } from "./SubagentsPanel";
-import { TodoPanel } from "./TodoPanel";
import { useTerminalStatuses } from "./useTerminalStatuses";
import { type RightRailTab, TAB_BADGE_BASE } from "./railTabs";
@@ -256,7 +254,7 @@ function NewTabMenu({
// ---------------------------------------------------------------------------
// FileTabsStrip — open file tabs rendered in the top rail tab strip, as peers
-// of the fixed Files/Terminals/Agents/Tasks tabs. Each tab is a cell with the
+// of the fixed Files/Terminals/Agents tabs. Each tab is a cell with the
// file's basename and an "x" close button. Clicking the cell activates the
// tab (opening its viewer); clicking the x closes it. No own scroll container
// or flex-1: the parent strip's overflow-x-auto scrolls the whole row.
@@ -537,12 +535,6 @@ interface WorkspacePanelProps {
* badge denominator) — starts at 1 for a lone agent.
*/
agentCount: number;
- /** Whether the session publishes a todo list (gates the Tasks tab). */
- todosSupported: boolean;
- /** Number of completed todos (Tasks tab badge numerator). */
- todosCompleted: number;
- /** Total todo count (Tasks tab badge denominator + visibility gate). */
- todosTotal: number;
/**
* The "root" session id for the Agents tab — the active session's
* parent when inside a child, else the active id. May be null while
@@ -600,7 +592,7 @@ interface WorkspacePanelProps {
* WorkspacePanel — the desktop right "Workspace" rail, rendered as a
* floating card (bg-card, rounded, bordered, shadowed) sitting below the
* full-width chat header band. Internally tabbed between Files,
- * Terminals, Agents and Tasks so each can claim the full rail height
+ * Terminals and Agents so each can claim the full rail height
* instead of competing for a vertically-split slot.
*
* Desktop-only (``hidden md:flex``): on mobile the rail's contents are
@@ -625,9 +617,6 @@ export function WorkspacePanel({
terminalsLength,
subagentsWorking,
agentCount,
- todosSupported,
- todosCompleted,
- todosTotal,
rootSessionId,
selectedFilePath,
openFiles,
@@ -700,7 +689,7 @@ export function WorkspacePanel({
className="absolute inset-y-0 left-0 z-10 w-1 cursor-col-resize hover:bg-primary/30 active:bg-primary/50 transition-colors"
/>
)}
- {/* Tab strip, in display order Files · Agents · Shells · Tasks.
+ {/* Tab strip, in display order Files · Agents · Shells.
Files and Agents are always present (the Agents panel lists at
least the main agent). Shells shows whenever AppShell's gate
allows it (the agent declares shell access, or a shell already
@@ -780,21 +769,6 @@ export function WorkspacePanel({
)}
- {todosSupported && todosTotal > 0 && (
-
-
-
- Tasks
-
- {todosCompleted}/{todosTotal}
-
-
-
- )}
{showBrowserTab && (
) : rightRailTab === "subagents" && rootSessionId ? (
- ) : rightRailTab === "todos" && todosSupported ? (
-
) : rightRailTab === "terminals" && showShellsTab ? (
) : (
diff --git a/web/src/shell/railTabs.ts b/web/src/shell/railTabs.ts
index 4aa780c229..fb118e5887 100644
--- a/web/src/shell/railTabs.ts
+++ b/web/src/shell/railTabs.ts
@@ -5,7 +5,7 @@
*/
/** The selectable tabs in the right workspace rail, in display order. */
-export type RightRailTab = "files" | "subagents" | "terminals" | "todos" | "browser";
+export type RightRailTab = "files" | "subagents" | "terminals" | "browser";
/**
* Count/status badge geometry. Fixed height with min-width == height keeps a