From 98de7c23985cea98d3128cb9ba373dfa9c3710ae Mon Sep 17 00:00:00 2001 From: Hubert Zub Date: Fri, 7 Aug 2026 14:06:31 +0000 Subject: [PATCH 1/3] OMNI-2350-remove-tasks-panel Signed-off-by: Hubert Zub --- web/src/shell/AppShell.tsx | 3 --- web/src/shell/WorkspacePanel.tsx | 28 ---------------------------- 2 files changed, 31 deletions(-) diff --git a/web/src/shell/AppShell.tsx b/web/src/shell/AppShell.tsx index 6d4a608480..f4836403d4 100644 --- a/web/src/shell/AppShell.tsx +++ b/web/src/shell/AppShell.tsx @@ -1519,9 +1519,6 @@ export function AppShell() { terminalsLength={railTerminals.length} subagentsWorking={subagentsWorking} agentCount={agentCount} - todosSupported={todosSupported} - todosCompleted={todosCompleted} - todosTotal={todos.length} rootSessionId={rootSessionId} selectedFilePath={selectedFilePath} openFiles={openFiles} diff --git a/web/src/shell/WorkspacePanel.tsx b/web/src/shell/WorkspacePanel.tsx index faefe320e1..b0979da07c 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"; @@ -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 @@ -625,9 +617,6 @@ export function WorkspacePanel({ terminalsLength, subagentsWorking, agentCount, - todosSupported, - todosCompleted, - todosTotal, rootSessionId, selectedFilePath, openFiles, @@ -780,21 +769,6 @@ export function WorkspacePanel({ )} - {todosSupported && todosTotal > 0 && ( - - - - Tasks - - {todosCompleted}/{todosTotal} - - - - )} {showBrowserTab && ( ) : rightRailTab === "subagents" && rootSessionId ? ( - ) : rightRailTab === "todos" && todosSupported ? ( - ) : rightRailTab === "terminals" && showShellsTab ? ( ) : ( From 58399e3d08249df986b68f6f4e11606c575d23dd Mon Sep 17 00:00:00 2001 From: Hubert Zub Date: Fri, 7 Aug 2026 14:19:48 +0000 Subject: [PATCH 2/3] Drop stale todos props from WorkspacePanel test The Tasks tab removal deleted todosSupported/todosCompleted/todosTotal from WorkspacePanelProps, but the test still passed them, breaking tsc -b. Remove them so the build is green. Co-authored-by: Isaac Signed-off-by: Hubert Zub --- web/src/shell/WorkspacePanel.test.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/web/src/shell/WorkspacePanel.test.tsx b/web/src/shell/WorkspacePanel.test.tsx index 8676b417ee..4b5482a82a 100644 --- a/web/src/shell/WorkspacePanel.test.tsx +++ b/web/src/shell/WorkspacePanel.test.tsx @@ -110,9 +110,6 @@ function renderWorkspace( terminalsLength={0} subagentsWorking={0} agentCount={1} - todosSupported={false} - todosCompleted={0} - todosTotal={0} rootSessionId={null} selectedFilePath={overrides.selectedFilePath ?? null} openFiles={overrides.openFiles ?? []} From 697d3175d6c614bd45a1e4fc25942a899bf9a809 Mon Sep 17 00:00:00 2001 From: Hubert Zub Date: Fri, 7 Aug 2026 14:37:17 +0000 Subject: [PATCH 3/3] Remove the Tasks panel from the workspace rail (desktop + mobile) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OMNI-2350 removes the Tasks tab from the right-side workspace panel entirely: the desktop rail tab, the mobile Tasks drawer + FAB entry, and the TodoPanel component. Drops the "todos" RightRailTab member and its persisted-state allow-list entry (sessions that had it stored fall back to Files). The chatStore.todos SSE data slice is left intact — it is the harness data contract, not the panel UI. Co-authored-by: Isaac Signed-off-by: Hubert Zub --- web/src/lib/events.ts | 4 +- web/src/lib/sessionWorkspaceState.ts | 4 +- web/src/shell/AppShell.subagent-nav.test.tsx | 1 - web/src/shell/AppShell.test.tsx | 83 ++----------------- web/src/shell/AppShell.tsx | 59 ++------------ web/src/shell/ChatHeader.test.tsx | 5 -- web/src/shell/ChatHeader.tsx | 26 +----- web/src/shell/TodoPanel.test.tsx | 84 -------------------- web/src/shell/TodoPanel.tsx | 76 ------------------ web/src/shell/WorkspacePanel.test.tsx | 3 - web/src/shell/WorkspacePanel.tsx | 6 +- web/src/shell/railTabs.ts | 2 +- 12 files changed, 21 insertions(+), 332 deletions(-) delete mode 100644 web/src/shell/TodoPanel.test.tsx delete mode 100644 web/src/shell/TodoPanel.tsx diff --git a/web/src/lib/events.ts b/web/src/lib/events.ts index 80c2450cb4..6fe2f81e62 100644 --- a/web/src/lib/events.ts +++ b/web/src/lib/events.ts @@ -569,8 +569,8 @@ export interface SessionAgentChangedEvent { * Each todo item has: * - `content`: the task description string * - `status`: `"pending"` | `"in_progress"` | `"completed"` - * - `activeForm`: present-continuous form of the task (e.g. `"Running tests"`). - * Shown by the TodoPanel under in-progress items when distinct from `content`. + * - `activeForm`: present-continuous form of the task (e.g. `"Running tests"`), + * the present-continuous label for an in-progress item when distinct from `content`. */ export interface SessionTodosEvent { type: "session_todos"; diff --git a/web/src/lib/sessionWorkspaceState.ts b/web/src/lib/sessionWorkspaceState.ts index 3d1684903e..268c1f1091 100644 --- a/web/src/lib/sessionWorkspaceState.ts +++ b/web/src/lib/sessionWorkspaceState.ts @@ -8,14 +8,14 @@ import type { RightRailTab } from "@/shell/railTabs"; -const RAIL_TABS: readonly RightRailTab[] = ["files", "subagents", "terminals", "todos", "browser"]; +const RAIL_TABS: readonly RightRailTab[] = ["files", "subagents", "terminals", "browser"]; export interface SessionWorkspaceState { /** Whether the rail was left open in this session. */ open?: boolean; /** User-chosen rail width (px) for this session. */ widthPx?: number; - /** The selected rail tab (Files / Agents / Shells / Tasks). */ + /** The selected rail tab (Files / Agents / Shells). */ rightRailTab?: RightRailTab; /** Ordered list of open file tabs. */ openFiles?: string[]; diff --git a/web/src/shell/AppShell.subagent-nav.test.tsx b/web/src/shell/AppShell.subagent-nav.test.tsx index 9ebb561473..c1f32e238e 100644 --- a/web/src/shell/AppShell.subagent-nav.test.tsx +++ b/web/src/shell/AppShell.subagent-nav.test.tsx @@ -89,7 +89,6 @@ vi.mock("./FileViewer", () => ({ vi.mock("./InlineTerminalsSection", () => ({ InlineTerminalsSection: () =>
, })); -vi.mock("./TodoPanel", () => ({ TodoPanel: () =>
})); vi.mock("./FilesPanelDrawer", () => ({ FilesPanelDrawer: () =>
, })); diff --git a/web/src/shell/AppShell.test.tsx b/web/src/shell/AppShell.test.tsx index efce3c070c..2defd97bf7 100644 --- a/web/src/shell/AppShell.test.tsx +++ b/web/src/shell/AppShell.test.tsx @@ -165,9 +165,6 @@ vi.mock("@/components/blocks/TerminalView", () => ({
{terminalId}
), })); -vi.mock("./TodoPanel", () => ({ - TodoPanel: () =>
, -})); vi.mock("./FilesPanelDrawer", () => ({ FilesPanelDrawer: ({ open, flatView }: { open: boolean; flatView: boolean }) => (
{ // choice carries across sessions. Clear it so a stored preference from one // test can't change another test's default scope. localStorage.clear(); - // The Tasks tab/drawer gates on chatStore.todos; reset so a populated - // todo list from one test doesn't leak into the next. // Reset terminal-first startup signals so one test's terminalPending / // failed status can't leak into another's terminalStartingUp. useChatStore.setState({ - todos: [], terminalPending: false, sessionStatus: "idle", status: "idle", @@ -1804,10 +1798,10 @@ describe("Right workspace card visibility", () => { }); it("keeps the card mounted with Agents as the only tab for a minimal agent", () => { - // A no-os_env agent (available: false) with no shells and no todos + // A no-os_env agent (available: false) with no shells // still has the unconditional Agents tab (the panel lists at least // the main agent), so the card mounts, the Agents tab is selected - // by the fallback, and Files/Shells/Tasks are absent. An unmounted + // by the fallback, and Files/Shells are absent. An unmounted // card here means the always-visible Agents rule regressed. useEnvironmentMock.mockReturnValue({ data: { available: false, root: null, home: null }, @@ -2220,7 +2214,7 @@ describe("AppShell URL sync — file param", () => { }); it("restores the file viewer into the desktop rail on a ?file= reload", () => { - // Regression (E2E reload-persistence): the Subagents/Terminals/Todos + // Regression (E2E reload-persistence): the Subagents/Terminals // panels are checked before the file viewer in the rail content // precedence. A ?file= reload must pull the rail to Files so the inline // viewer renders instead of another panel shadowing it. @@ -2784,25 +2778,17 @@ describe("Mobile session menu", () => { isLoading: false, error: null, }); - useChatStore.setState({ - todos: [ - { content: "do a thing", status: "completed", activeForm: "doing a thing" }, - { content: "do another", status: "pending", activeForm: "doing another" }, - ], - }); - renderShell("/c/conv_native"); openSessionMenu(); // Mirror of the desktop rail's tab strip for a native-wrapper session: - // Files · Agents · Tasks. Shells is absent because the only terminal is + // Files · Agents. Shells is absent because the only terminal is // the vendor pane (the pill's Terminal view — excluded from the shell // inventory) and the mocked agent declares no terminals. An unexpected // Shells entry means the vendor pane leaked into the inventory. expect(screen.getByRole("menuitem", { name: /^Files$/i })).toBeInTheDocument(); expect(screen.queryByRole("menuitem", { name: /Shells/i })).toBeNull(); expect(screen.getByRole("menuitem", { name: /Agents/i })).toBeInTheDocument(); - expect(screen.getByRole("menuitem", { name: /Tasks/i })).toBeInTheDocument(); }); it("keeps the Terminals entry in terminal-first SDK sessions (no native wrapper)", () => { @@ -2934,67 +2920,8 @@ describe("Mobile session menu", () => { expect(drawer).toHaveAttribute("data-flat-view", "false"); }); - it("opens the Tasks drawer for a claude-native session with todos", () => { - useEnvironmentMock.mockReturnValue({ - data: { available: true, root: null }, - isLoading: false, - } as unknown as ReturnType); - mockConversations([ - { - id: "conv_native", - permission_level: null, - labels: { "omnigent.wrapper": "claude-code-native-ui" }, - }, - ]); - useChatStore.setState({ - todos: [{ content: "build the thing", status: "in_progress", activeForm: "building" }], - }); - - renderShell("/c/conv_native"); - - expect(screen.getByTestId("todos-panel-drawer")).toHaveAttribute("data-state", "closed"); - expect(screen.queryByTestId("todo-panel")).toBeNull(); - - openSessionMenu(); - fireEvent.click(screen.getByRole("menuitem", { name: /Tasks/i })); - - // Failure: openTodosPanel didn't set todosPanelOpen, or the Tasks entry - // was gated out despite isClaudeNative + a non-empty todo list. - expect(screen.getByTestId("todos-panel-drawer")).toHaveAttribute("data-state", "open"); - expect(screen.getByTestId("todo-panel")).toBeInTheDocument(); - }); - - it.each([ - ["codex-native", "conv_codex", "codex-native-ui"], - ["pi-native", "conv_pi", "pi-native-ui"], - ])("opens the Tasks drawer for a %s session with todos", (_harness, id, wrapper) => { - useEnvironmentMock.mockReturnValue({ - data: { available: true, root: null }, - isLoading: false, - } as unknown as ReturnType); - mockConversations([ - { - id, - permission_level: null, - labels: { "omnigent.wrapper": wrapper }, - }, - ]); - useChatStore.setState({ - todos: [{ content: "Locate CLI parser", status: "in_progress", activeForm: "Locating" }], - }); - - renderShell(`/c/${id}`); - expect(screen.getByTestId("todos-panel-drawer")).toHaveAttribute("data-state", "closed"); - - openSessionMenu(); - fireEvent.click(screen.getByRole("menuitem", { name: /Tasks/i })); - - expect(screen.getByTestId("todos-panel-drawer")).toHaveAttribute("data-state", "open"); - expect(screen.getByTestId("todo-panel")).toBeInTheDocument(); - }); - it("keeps the FAB with only the Agents entry for a minimal agent", () => { - // available:false → no files; no shells, no todos, no debug. The + // available:false → no files; no shells, no debug. The // Agents entry is unconditional (badge = 1, the main agent), so the // FAB still renders with exactly that entry. A missing FAB means // the always-visible Agents rule regressed on mobile. diff --git a/web/src/shell/AppShell.tsx b/web/src/shell/AppShell.tsx index f4836403d4..35d20dc6f7 100644 --- a/web/src/shell/AppShell.tsx +++ b/web/src/shell/AppShell.tsx @@ -84,7 +84,6 @@ import { type TerminalFirstContextValue, } from "./TerminalFirstContext"; import { TerminalsPanel } from "./TerminalsPanel"; -import { TodoPanel } from "./TodoPanel"; import { PermissionsModal } from "@/components/PermissionsModal"; import { KeyboardShortcutsDialog } from "@/components/KeyboardShortcutsDialog"; import { CommandPalette } from "./CommandPalette"; @@ -270,7 +269,6 @@ export function AppShell() { // on a phone they open as full-screen overlays from the session-menu FAB. const [subagentsPanelOpen, setSubagentsPanelOpen] = useState(false); const [shellsPanelOpen, setShellsPanelOpen] = useState(false); - const [todosPanelOpen, setTodosPanelOpen] = useState(false); // The right "Workspace" rail (WorkspacePanel) remembers its open/closed // state per session. A brand-new session (no saved `open`) follows the // Appearance "Workspace panel" default; reopening a session restores how @@ -378,15 +376,11 @@ export function AppShell() { const sessionLabels = { ...activeConv?.labels, ...activeSession?.labels }; const terminalFirst = sessionLabels["omnigent.ui"] === "terminal"; const isClaudeNative = sessionLabels["omnigent.wrapper"] === "claude-code-native-ui"; - const todos = useChatStore((s) => s.todos); - // The session.todos contract is harness-agnostic; show Tasks when it has data. - const todosSupported = todos.length > 0; // Native-CLI wrapper of either family. Keys harness behavior gates // (composer slash commands, `/model`); terminal-first SDK sessions // (embedded Omnigent REPL terminal) have NO wrapper label and must // keep regular chat behavior. See TerminalFirstContext.tsx. const isNativeWrapper = isNativeWrapperLabel(sessionLabels["omnigent.wrapper"]); - const todosCompleted = todos.filter((t) => t.status === "completed").length; // Used for the header "Back to parent" link, which is hidden on // top-level sessions. The Subagents tab itself is always visible — // it lists the root's children plus a "main" entry, so the user @@ -550,24 +544,23 @@ export function AppShell() { // ``railTerminals`` starts empty while the agent loads, so native // sessions don't flash the tab. terminals: !hideTerminalsTab && railTerminals.length > 0, - todos: todosSupported && todos.length > 0, }) as const, - [showFilesPanel, hideTerminalsTab, railTerminals.length, todosSupported, todos.length], + [showFilesPanel, hideTerminalsTab, railTerminals.length], ); // Whether the rail has anything at all to show. When false the workspace // card doesn't mount and the header hides its collapse toggle — a - // no-filesystem agent with no terminals/sub-agents/todos would otherwise + // no-filesystem agent with no terminals/sub-agents would otherwise // render an empty white card with no way to dismiss it. const hasRailContent = Object.values(railTabsAvailable).some(Boolean); // Keep the selected tab valid. When the current tab disappears — files // panel turns off, or the Shells tab hides (native wrapper / no shell // and no shell access) — fall back to the first still-visible tab in - // display order (Files · Agents · Shells · Tasks · Browser). Picking the first + // display order (Files · Agents · Shells · Browser). Picking the first // available (rather than ping-ponging between two effects) keeps this // convergent even when several tabs vanish at once. useEffect(() => { if (railTabsAvailable[rightRailTab]) return; - const next = (["files", "subagents", "terminals", "todos", "browser"] as const).find( + const next = (["files", "subagents", "terminals", "browser"] as const).find( (t) => railTabsAvailable[t], ); if (next) setRightRailTab(next); @@ -723,7 +716,6 @@ export function AppShell() { setFilesPanelOpen(false); setSubagentsPanelOpen(false); setShellsPanelOpen(false); - setTodosPanelOpen(false); setFilesPanelShowHidden(false); if (!conversationId) { // No session → no rail; false (not the open default) so rail-gated @@ -791,7 +783,7 @@ export function AppShell() { if (wasMaximized) restoreSidebarAfterMaximize(); return false; }); - // A selected file must be visible in the rail. The Agents/Todos/Terminals + // A selected file must be visible in the rail. The Agents/Terminals // tabs don't render the inline viewer, so pull the rail to Files. if (nextSelected && nextTab !== "files") { nextTab = "files"; @@ -901,13 +893,10 @@ export function AppShell() { setExecutionLogsKey(null); // close execution-logs panel setFilesPanelOpen(false); // close files drawer so the viewer is unobscured setSubagentsPanelOpen(false); // close mobile agents drawer - setTodosPanelOpen(false); // close mobile tasks drawer // Pull the rail to the Files tab when parked on a tab where the viewer - // won't render (Terminals, Subagents, Todos). The Files tab surfaces the + // won't render (Terminals, Subagents). The Files tab surfaces the // FileViewer inline, so leave it undisturbed. - setRightRailTab((prev) => - prev === "terminals" || prev === "subagents" || prev === "todos" ? "files" : prev, - ); + setRightRailTab((prev) => (prev === "terminals" || prev === "subagents" ? "files" : prev)); // Reveal the rail so the viewer is actually visible — a session the user // collapsed (or one that started collapsed via the Appearance default) // would otherwise route the file into an invisible panel. Persist @@ -1116,7 +1105,6 @@ export function AppShell() { setFilesPanelOpen(false); // close files drawer setSubagentsPanelOpen(false); // close mobile agents drawer setShellsPanelOpen(false); // close mobile shells drawer - setTodosPanelOpen(false); // close mobile tasks drawer setPanelInitialKey(key); } @@ -1136,7 +1124,6 @@ export function AppShell() { setFilesPanelOpen(false); setSubagentsPanelOpen(false); setShellsPanelOpen(false); - setTodosPanelOpen(false); setRightPanelOpen(true); if (conversationId) writeSessionWorkspaceState(conversationId, { open: true }); }, @@ -1184,7 +1171,6 @@ export function AppShell() { setFilesPanelOpen(false); // close files drawer setSubagentsPanelOpen(false); // close mobile agents drawer setShellsPanelOpen(false); // close mobile shells drawer - setTodosPanelOpen(false); // close mobile tasks drawer setExecutionLogsKey(key); } @@ -1198,7 +1184,6 @@ export function AppShell() { setExecutionLogsKey(null); // close execution-logs panel setSubagentsPanelOpen(false); // close mobile agents drawer setShellsPanelOpen(false); // close mobile shells drawer - setTodosPanelOpen(false); // close mobile tasks drawer setFilesPanelOpen(true); } @@ -1211,7 +1196,6 @@ export function AppShell() { setExecutionLogsKey(null); // close execution-logs panel setFilesPanelOpen(false); // close files drawer setShellsPanelOpen(false); // close mobile shells drawer - setTodosPanelOpen(false); // close mobile tasks drawer setSubagentsPanelOpen(true); } @@ -1225,23 +1209,9 @@ export function AppShell() { setExecutionLogsKey(null); // close execution-logs panel setFilesPanelOpen(false); // close files drawer setSubagentsPanelOpen(false); // close mobile agents drawer - setTodosPanelOpen(false); // close mobile tasks drawer setShellsPanelOpen(true); } - // Mobile FAB → "Tasks" opens the todo list (the desktop rail's Tasks tab) - // as a full-screen drawer. - function openTodosPanel() { - setSelectedFilePath(null); // close file viewer - clearFileViewerUrl(); - setPanelInitialKey(null); // close terminals panel - setExecutionLogsKey(null); // close execution-logs panel - setFilesPanelOpen(false); // close files drawer - setSubagentsPanelOpen(false); // close mobile agents drawer - setShellsPanelOpen(false); // close mobile shells drawer - setTodosPanelOpen(true); - } - function openMainExecutionLog() { // Mobile FAB → "Execution logs" jumps straight to the main thread. // Children are reachable via the panel's tab switcher. @@ -1453,7 +1423,6 @@ export function AppShell() { filesPanelOpen, subagentsPanelOpen, shellsPanelOpen, - todosPanelOpen, hideTerminalsTab, // Mobile: reachable when a shell exists OR the agent // declares shell access (so the drawer's "+ New shell" row @@ -1462,9 +1431,6 @@ export function AppShell() { showShellsTab: !hideTerminalsTab && (railTerminals.length > 0 || agentSupportsShells), terminalsLength: railTerminals.length, - todosSupported, - todosCompleted, - todosTotal: todos.length, debugMode, changedCount, subagentsWorking, @@ -1472,7 +1438,6 @@ export function AppShell() { onOpenFiles: openFilesPanel, onOpenShells: openShellsPanel, onOpenSubagents: openSubagentsPanel, - onOpenTodos: openTodosPanel, onOpenMainExecutionLog: openMainExecutionLog, }} /> @@ -1612,16 +1577,6 @@ export function AppShell() { /> )} - {conversationId && ( - setTodosPanelOpen(false)} - testId="todos-panel-drawer" - > - - - )} {/* Mobile-only push panel — on desktop the viewer lives inside the inline aside. */} {conversationId && selectedFilePath !== null && (
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 4b5482a82a..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}
diff --git a/web/src/shell/WorkspacePanel.tsx b/web/src/shell/WorkspacePanel.tsx index b0979da07c..2c2bb8b5a9 100644 --- a/web/src/shell/WorkspacePanel.tsx +++ b/web/src/shell/WorkspacePanel.tsx @@ -254,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. @@ -592,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 @@ -689,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 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