{"version":3,"file":"task-panel.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/task-panel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AAyD1E;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;AA+jB3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,gEAAgE;AAChE,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAAC;AAElD,qBAAa,kBAAmB,YAAW,SAAS,EAAE,SAAS;IAC9D,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAa;IAChC,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,IAAI,CAAyB;IACrC,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,QAAQ,CAAS;IAQzB,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,aAAa,CAAuC;IAC5D,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,YAAY,CAAkC;IAEtD,sEAAsE;IACtE,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,QAAQ;IAOhB;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ;IAmBhB,OAAO,UAAS;IAChB,OAAO,CAAC,YAAY,CAAqB;IACzC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,wDAAwD;IACxD,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAEzB,YAAY,EAAE,CAAC,EAAE,GAAG,EAEnB;IAED,UAAU,IAAI,IAAI,CAGjB;IAED,OAAO,CAAC,UAAU;IAIlB,2EAA2E;IAC3E,WAAW,IAAI,MAAM,GAAG,SAAS,CAKhC;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAyB9B;IAED,OAAO,IAAI,aAAa,CAEvB;IAED,OAAO,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAGjC;IAED;;;;OAIG;IACH,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAI1C;IAED;;;;OAIG;IACH,SAAS,CAAC,SAAS,GAAE,SAAS,GAAG,UAAsB,GAAG,aAAa,CAOtE;IAED;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IAmBtB,4CAA4C;IAC5C,OAAO,IAAI,IAAI,CAMd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAgK9B;CACD","sourcesContent":["import type { Component, Focusable, TUI } from \"@kolisachint/hoocode-tui\";\nimport { getKeybindings, matchesKey, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport { formatDurationSecs } from \"../../../core/format-duration.js\";\nimport { formatTokens } from \"../../../core/format-tokens.js\";\nimport type { Task, TaskAgent, TaskAgentKind, TaskAgentState, TaskStatus } from \"../../../core/task-store.js\";\nimport { taskOwnerId, taskStore } from \"../../../core/task-store.js\";\nimport type { ThemeColor } from \"../theme/theme.js\";\nimport { agentColorFor, theme } from \"../theme/theme.js\";\nimport { appKeyLabel, formatKeyText, matchesAppKey, rawKeyHint } from \"./keybinding-hints.js\";\n\nconst TASK_STATUS_ICON: Record<TaskStatus, string> = {\n\t// Hollow = not started, matching the hollow-means-lighter logic of ◇ and the\n\t// selectors' ○/◉ convention; also keeps ● exclusive to the chat's tool\n\t// status dot so the two never read as the same signal.\n\tpending: \"○\",\n\tin_progress: \"◐\",\n\tdone: \"✓\",\n\tfailed: \"✗\",\n\tcancelled: \"⊘\",\n};\n\n/**\n * U+26A0 with VS15 (text presentation). Bare ⚠ carries the Unicode Emoji\n * property, and terminals with emoji font fallback render it as a double-width\n * color glyph while the width math counts 1 cell — misaligning exactly the\n * rows that carry a warning. VS15 forces single-cell text rendering and is\n * itself zero-width, so visibleWidth stays correct.\n */\nconst WARNING_GLYPH = \"⚠︎\";\n\n/** U+25B6 with VS15 — same emoji-presentation hazard as WARNING_GLYPH. */\nconst SELECTED_GLYPH = \"▶︎\";\n\n/**\n * Single-cell marker for MCP-sourced rows, which have no owning agent. Every\n * other row derives its marker from the owner's kind via AGENT_GLYPH (◆ main /\n * ◇ subagent / ▸ team role), so the flat lens attributes a row exactly the way\n * the grouped lenses do. The row also carries a text origin tag before the\n * title (see formatTaskLine).\n */\nconst MCP_SOURCE_GLYPH = \"⧉\";\n\n/**\n * Cadence of the delegated-row run clock. The pane used to animate a braille\n * spinner at 120ms, duplicating the transcript Loader's own spinner (same\n * frames, independent timer, permanently out of phase) and re-rendering the\n * whole component tree eight times a second. The in-progress glyph is now the\n * static ◐ the design's glyph table specifies, and the only motion left is this\n * clock — the same 1s tick the bash block's `Elapsed Ns` uses. It runs only\n * while a subagent/MCP row is live, because those are the only rows that carry\n * a clock at all.\n */\nconst RUN_CLOCK_INTERVAL_MS = 1000;\n\n/** A thin colored left rail groups the pane without a box, the way the design's `border-left` does. */\nconst RAIL = \"▎\";\n\n/**\n * How the same task list is presented:\n * - flat       → one ungrouped list (default)\n * - subagents  → grouped by owning agent (◆ main orchestrator + ◇ workers)\n * - teams      → grouped by named role-agent (▸), with handoff arrows\n */\nexport type TaskPanelView = \"flat\" | \"subagents\" | \"teams\";\n\nconst VIEW_LABEL: Record<TaskPanelView, string> = { flat: \"tasks\", subagents: \"subagents\", teams: \"teams\" };\n\n/**\n * A top-level task the main agent owns directly: its own TodoWrite plan. Source\n * is unset (not a subagent/MCP delegation) and it sits at the root of the forest\n * (not a child merged in from a subagent's subtree). These are exactly the rows\n * the flat (\"tasks\") lens shows.\n */\nfunction isMainTask(task: Task): boolean {\n\t// Source-unset (not a subagent/MCP delegation — note taskOwnerId folds MCP into\n\t// \"main\", so check source directly), not owned by a role agent, and a forest\n\t// root (not a child merged in from a subagent's subtree).\n\treturn task.source === undefined && task.agent === undefined && task.parentTaskId === undefined;\n}\n\n/**\n * Delegated work: a dispatched subagent run or an MCP call. These are the only\n * rows that carry a run clock and the only rows that ever report token usage —\n * both need an attribution boundary, and a TodoWrite plan item has none (see\n * taskElapsedSecs, and note that todo.ts never writes `usage`).\n */\nfunction isDelegated(task: Task): boolean {\n\treturn task.source === \"subagent\" || task.source === \"mcp\";\n}\n\n/**\n * Lenses that currently have content, split by ownership:\n * - flat (\"tasks\")  → only when the main agent has its own TodoWrite plan.\n * - subagents       → when delegated work exists (a registered subagent, or any\n *                     subagent/MCP-sourced task).\n * - teams           → when role agents are registered (hooteams `--team`).\n * The cycle key and the header switcher both skip empty lenses, and an empty\n * flat lens falls through to subagents (see render), so a session that only\n * delegated work opens straight on the task tree with no empty \"tasks\" view.\n */\nfunction availableViews(tasks: readonly Task[], agents: readonly TaskAgent[]): TaskPanelView[] {\n\tconst views: TaskPanelView[] = [];\n\tif (tasks.some(isMainTask)) views.push(\"flat\");\n\tconst hasSubagentWork =\n\t\tagents.some((a) => a.kind === \"subagent\") || tasks.some((t) => t.source === \"subagent\" || t.source === \"mcp\");\n\tif (hasSubagentWork) views.push(\"subagents\");\n\tif (agents.some((a) => a.kind === \"role\")) views.push(\"teams\");\n\treturn views;\n}\n\n/** Owner glyphs: main agent a filled diamond, spawned subagents the hollow counterpart, team roles a triangle. */\nconst AGENT_GLYPH: Record<TaskAgentKind, string> = { main: \"◆\", subagent: \"◇\", role: \"▸\" };\nconst AGENT_GLYPH_COLOR: Record<TaskAgentKind, ThemeColor> = {\n\tmain: \"accent\",\n\tsubagent: \"accent\",\n\trole: \"borderAccent\",\n};\n\n/** Color for an agent's lifecycle `[state]` tag (mirrors the design's .ast-* classes). */\nconst AGENT_STATE_COLOR: Record<TaskAgentState, ThemeColor> = {\n\tactive: \"warning\",\n\trunning: \"warning\",\n\tdone: \"success\",\n\tqueued: \"dim\",\n\tidle: \"dim\",\n\twaiting: \"mdLink\",\n\tfailed: \"error\",\n\t// User-initiated stop: settled and intentional, so muted rather than red.\n\tcancelled: \"dim\",\n};\n\n/** Two-cell indent under a group header, with a faint vertical guide. */\nconst GROUP_INDENT_PLAIN = \"│ \";\n\n/** Overall pane state, derived from the task statuses. Drives the rail color + header stamp. */\ntype PanelState = \"working\" | \"reviewed\" | \"stopped\";\n\n/**\n * Rail color per panel state. State used to be encoded five times over — rail,\n * stamp glyph, stamp word, progress-bar fill and done/total — so the stamp and\n * the bar are gone and the rail is the single channel.\n */\nconst PANEL_STATE_COLOR: Record<PanelState, \"warning\" | \"success\" | \"error\"> = {\n\tworking: \"warning\",\n\treviewed: \"success\",\n\tstopped: \"error\",\n};\n\nfunction panelState(tasks: readonly Task[]): PanelState {\n\tif (tasks.some((t) => t.status === \"failed\")) return \"stopped\";\n\tconst active = tasks.some((t) => t.status === \"in_progress\" || t.status === \"pending\");\n\treturn active ? \"working\" : \"reviewed\";\n}\n\nfunction taskStatusColor(status: TaskStatus): \"dim\" | \"warning\" | \"success\" | \"error\" {\n\tswitch (status) {\n\t\tcase \"in_progress\":\n\t\t\treturn \"warning\";\n\t\tcase \"done\":\n\t\t\treturn \"success\";\n\t\tcase \"failed\":\n\t\t\treturn \"error\";\n\t\tdefault:\n\t\t\t// pending and cancelled: quiet gray — a user-initiated cancel is not an\n\t\t\t// error and must not paint the row (or the rail) red.\n\t\t\treturn \"dim\";\n\t}\n}\n\n/**\n * Wall-clock time a task occupied. A settled task reads its create→final-update\n * span; a task still in progress is measured against the clock so its elapsed\n * time advances while it runs (updatedAt barely moves during a run, which used\n * to freeze the display at ~0s until completion).\n *\n * Only meaningful for delegated rows, and only they call it. A TodoWrite plan\n * item shares its createdAt with every other item in the plan — the tool creates\n * the whole list in one taskStore.batch() — so this would report the age of the\n * PLAN, not of the task: item #3 would include all the time spent on #1 and #2,\n * and the numbers would read as an ascending series of per-item durations that\n * are in fact cumulative. A dispatched run's createdAt is its dispatch time, so\n * its clock is real.\n */\nfunction taskElapsedSecs(task: Task, now: number = Date.now()): number {\n\tconst end = task.status === \"in_progress\" ? now : task.updatedAt;\n\treturn Math.max(0, (end - task.createdAt) / 1000);\n}\n\n/** The agent-type key a roster entry colors by: \"explore#2\" → \"explore\". */\nfunction agentTypeOfName(name: string): string {\n\tconst idx = name.indexOf(\"#\");\n\treturn idx > 0 ? name.slice(0, idx) : name;\n}\n\n/**\n * Ledger header — one tab per lens that has content, each carrying that lens's\n * own done/total. The counts of the lenses you are NOT looking at stay visible,\n * so a `subagents 0/2` tab reports unfinished delegated work while you read the\n * plan.\n *\n * The selected tab fills with `selectedBg` only while its lens has live work,\n * and drops to a flat bold-accent label once everything settles — the fill means\n * \"running\", not merely \"here\". Padding cells are identical filled or not, so\n * cycling never shifts the strip. `dim` muddies against `selectedBg` (#666666 on\n * #3a3a4a), so a selected tab's count steps up to `muted`.\n *\n * The header IS the switcher: with a single lens there is nothing to switch to\n * and the pane renders rows only (see render()). The navigable teams roster is\n * the one exception and keeps its anchor.\n */\nfunction formatLensTabs(\n\ttasks: readonly Task[],\n\tagents: readonly TaskAgent[],\n\twidth: number,\n\tview: TaskPanelView,\n\ttabViews: readonly TaskPanelView[],\n\tshowCycleHint: boolean,\n\tshowFocusHint: boolean,\n): string {\n\tconst tabs = tabViews.map((v) => {\n\t\tconst lensTasks = filterTasksForLens(tasks, agents, v);\n\t\tconst done = lensTasks.filter((t) => t.status === \"done\").length;\n\t\tconst label = VIEW_LABEL[v];\n\t\tconst count = `${done}/${lensTasks.length}`;\n\t\tconst plain = ` ${label} ${count} `;\n\t\tif (v !== view) return { plain, styled: theme.fg(\"dim\", plain) };\n\t\tconst body = ` ${theme.bold(theme.fg(\"accent\", label))} ${theme.fg(\"muted\", count)} `;\n\t\tconst live = lensTasks.some((t) => t.status === \"in_progress\");\n\t\treturn { plain, styled: live ? theme.bg(\"selectedBg\", body) : body };\n\t});\n\n\tconst selected = tabs[tabViews.indexOf(view)] ?? tabs[0];\n\t// Joined without a separator: each tab already carries a pad cell on both\n\t// sides, so neighbours sit two cells apart and two fills never touch.\n\tconst strip = {\n\t\tplain: tabs.map((t) => t.plain).join(\"\"),\n\t\tstyled: tabs.map((t) => t.styled).join(\"\"),\n\t};\n\n\t// The pane names its own keys, which is the whole reason they can be letters\n\t// nobody could guess: alt+l is the task *list* and alt+n names nothing at all,\n\t// and neither has to be recalled while the header is printing them. The focus\n\t// hint only appears where the key does something — the teams lens, unfocused.\n\tconst parts: Array<{ plain: string; styled: string }> = [];\n\tif (showCycleHint) {\n\t\tconst key = appKeyLabel(\"app.tasks.cycleForward\");\n\t\tparts.push({ plain: `${formatKeyText(key)} cycle`, styled: rawKeyHint(key, \"cycle\") });\n\t}\n\tif (showFocusHint) {\n\t\tconst key = appKeyLabel(\"app.team.focus\");\n\t\tparts.push({ plain: `${formatKeyText(key)} focus`, styled: rawKeyHint(key, \"focus\") });\n\t}\n\tconst sep = \"  \";\n\tconst hintPlain = parts.map((p) => p.plain).join(sep);\n\tconst hintStyled = parts.map((p) => p.styled).join(theme.fg(\"muted\", sep));\n\n\t// Narrowing order: drop the hints, then the unselected tabs. Each tab\n\t// closes its own background, so a variant is never cut mid-escape; only the\n\t// final unstyled fallback truncates. This inverts the old priority, where the\n\t// switcher was the FIRST thing dropped — the pane's only navigation affordance\n\t// used to lose the line to a token counter.\n\tif (hintPlain && visibleWidth(strip.plain) + 2 + visibleWidth(hintPlain) <= width) {\n\t\tconst pad = width - visibleWidth(strip.plain) - visibleWidth(hintPlain);\n\t\treturn strip.styled + \" \".repeat(pad) + hintStyled;\n\t}\n\tfor (const variant of [strip, selected]) {\n\t\tif (variant && visibleWidth(variant.plain) <= width) {\n\t\t\treturn variant.styled + \" \".repeat(width - visibleWidth(variant.plain));\n\t\t}\n\t}\n\treturn truncateToWidth(selected?.plain ?? \"\", width, \"…\");\n}\n\nfunction formatTaskLine(\n\ttask: Task,\n\twidth: number,\n\toptions: { grouped?: boolean; owner?: TaskAgent; treePrefix?: string } = {},\n): string {\n\tconst icon = theme.fg(taskStatusColor(task.status), TASK_STATUS_ICON[task.status]);\n\n\t// In grouped views the group header already carries the row's origin, so the\n\t// owner glyph and tag are suppressed; the rows sit on a faint indent guide.\n\tconst grouped = options.grouped === true;\n\tconst indent = grouped ? theme.fg(\"borderMuted\", GROUP_INDENT_PLAIN) : \"\";\n\n\t// Owner marker between the status icon and the id, derived from the owning\n\t// agent's kind so the flat lens attributes rows the same way the grouped\n\t// lenses do (a roster-less owner falls back on the task's source). MCP rows\n\t// have no owning agent and keep their own ⧉ marker. Main-owned rows carry NO\n\t// glyph in the flat lens — the pane's rail already says \"main\", so a ◆ on\n\t// every plan row was pure noise; the glyph appears only where it\n\t// disambiguates (subagent runs, MCP calls, mixed trees).\n\tconst isMcp = task.source === \"mcp\";\n\tconst ownerKind = options.owner?.kind ?? (task.source === \"subagent\" ? \"subagent\" : \"main\");\n\tconst showOwnerGlyph = !grouped && (isMcp || ownerKind !== \"main\");\n\tconst sourceGlyph = isMcp ? MCP_SOURCE_GLYPH : AGENT_GLYPH[ownerKind];\n\t// Subagent rows carry their agent's identity color on the glyph (and tag\n\t// below) — the same hue as the chat's `Agent [type]` line and TaskOutput —\n\t// so a user can trace one agent across the whole TUI at a glance.\n\tconst agentTypeName = !isMcp && ownerKind === \"subagent\" ? (task.subagentMode ?? task.agent) : undefined;\n\tconst styledSource = !showOwnerGlyph\n\t\t? \"\"\n\t\t: agentTypeName\n\t\t\t? theme.fg(agentColorFor(agentTypeName), sourceGlyph)\n\t\t\t: theme.fg(\"dim\", sourceGlyph);\n\n\t// Origin tag prefixed to the title, naming who runs the row: the subagent\n\t// type (\"[explore]\"), the team role's name (\"[planner]\"), or the MCP server\n\t// (\"[github]\"; \"[MCP]\" when no server label was recorded). Drawn in accent,\n\t// parallel to the chat's `Agent [explore]` / `MCP [server › tool]`. Grouped\n\t// rows drop it — the group header carries the origin — except MCP rows,\n\t// which group under main without being main's own work.\n\tlet tag = \"\";\n\tlet tagColor: ThemeColor = \"accent\";\n\tif (isMcp) {\n\t\ttag = `[${task.subagentMode ?? \"MCP\"}]`;\n\t\ttagColor = \"mcp\";\n\t} else if (!grouped) {\n\t\tif (task.subagentMode) {\n\t\t\ttag = `[${task.subagentMode}]`;\n\t\t\ttagColor = agentColorFor(task.subagentMode);\n\t\t} else if (ownerKind === \"role\" && options.owner) {\n\t\t\ttag = `[${options.owner.name}]`;\n\t\t\ttagColor = agentColorFor(options.owner.name);\n\t\t}\n\t}\n\tconst styledTag = tag ? `${theme.fg(tagColor, tag)} ` : \"\";\n\tconst title = task.title;\n\t// The title carries the line. Done titles fade to muted\n\t// (settled work), pending dim (not started), active goes bold, failed turns red.\n\tlet styledTitle: string;\n\tswitch (task.status) {\n\t\tcase \"done\":\n\t\t\tstyledTitle = theme.fg(\"muted\", title);\n\t\t\tbreak;\n\t\tcase \"pending\":\n\t\t\tstyledTitle = theme.fg(\"dim\", title);\n\t\t\tbreak;\n\t\tcase \"failed\":\n\t\t\tstyledTitle = theme.fg(\"error\", title);\n\t\t\tbreak;\n\t\tcase \"cancelled\":\n\t\t\tstyledTitle = theme.fg(\"dim\", theme.strikethrough(title));\n\t\t\tbreak;\n\t\tcase \"in_progress\":\n\t\t\tstyledTitle = theme.bold(title);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tstyledTitle = title;\n\t}\n\n\t// Right column: settled rows carry their token usage; an active row carries the\n\t// owning agent's live tool activity (fed by the pool's task_progress events) so\n\t// a delegated row reads \"⋯ grep\" rather than looking stuck. Only delegated rows\n\t// add a run clock — see taskElapsedSecs for why a plan item has no honest one.\n\t// Pending and cancelled rows say nothing: ○ and a struck ⊘ title already do,\n\t// and \"queued\"/\"cancelled\" only repeated the glyph in words.\n\tlet rightPlain = \"\";\n\tlet rightStyled = \"\";\n\tif (task.status === \"done\" || task.status === \"failed\") {\n\t\tlet tokenText = \"\";\n\t\tif (task.usage) {\n\t\t\tconst totalTok = task.usage.input + task.usage.output;\n\t\t\tif (totalTok > 0) tokenText = formatTokens(totalTok);\n\t\t}\n\t\tif (tokenText) {\n\t\t\trightPlain = tokenText;\n\t\t\trightStyled = theme.fg(\"muted\", tokenText);\n\t\t}\n\t} else if (task.status === \"in_progress\") {\n\t\tconst activity = options.owner?.activity ? `⋯ ${options.owner.activity}` : \"\";\n\t\tconst runFor = isDelegated(task) ? formatDurationSecs(taskElapsedSecs(task)) : \"\";\n\t\tif (activity && runFor) {\n\t\t\trightPlain = `${activity} · ${runFor}`;\n\t\t\trightStyled = theme.fg(\"warning\", activity) + theme.fg(\"dim\", ` · ${runFor}`);\n\t\t} else if (activity) {\n\t\t\trightPlain = activity;\n\t\t\trightStyled = theme.fg(\"warning\", activity);\n\t\t} else if (runFor) {\n\t\t\trightPlain = runFor;\n\t\t\trightStyled = theme.fg(\"dim\", runFor);\n\t\t}\n\t}\n\n\t// A warning note (e.g. inherited-model fallback, exhaustion skip) takes over the\n\t// right column as a ⚠ cue, replacing the usage/status stamp for that row.\n\tif (task.note) {\n\t\trightPlain = `${WARNING_GLYPH} ${task.note}`;\n\t\trightStyled = theme.fg(\"warning\", rightPlain);\n\t}\n\n\tconst rightWidth = rightPlain ? visibleWidth(rightPlain) + 1 : 0;\n\tconst leftWidth = Math.max(0, width - rightWidth);\n\n\t// truncateToWidth measures visible width (ANSI-aware), so the styled left can be\n\t// truncated against the full left budget directly. Subtracting the prefix here\n\t// (as a prior version did) truncated titles early and unevenly per id width.\n\t// In the subagents tree, a depth-first connector prefix (└─/├─/│) sits before\n\t// the row's glyph; roots pass an empty prefix and read exactly like flat rows.\n\tconst treePrefix = options.treePrefix ? theme.fg(\"borderMuted\", options.treePrefix) : \"\";\n\tconst leftBody = grouped\n\t\t? `${indent}${icon} ${styledTag}${styledTitle}`\n\t\t: `${treePrefix}${icon} ${styledSource ? `${styledSource} ` : \"\"}${styledTag}${styledTitle}`;\n\tconst left = truncateToWidth(leftBody, leftWidth, \"…\");\n\n\t// Pad every row to the full pane width so rows align regardless of whether they\n\t// carry a right column (token usage / running… / queued).\n\tif (!rightPlain) {\n\t\tconst pad = Math.max(0, width - visibleWidth(left));\n\t\treturn left + \" \".repeat(pad);\n\t}\n\n\tconst pad = Math.max(1, width - visibleWidth(left) - visibleWidth(rightPlain));\n\treturn left + \" \".repeat(pad) + rightStyled;\n}\n\n/**\n * Filter tasks and agents to the teams lens: only role agents and the tasks they\n * own. The flat and subagents lenses do their own ownership filtering inline (by\n * source and parentTaskId), so this is teams-specific.\n */\nfunction filterTasksForView(\n\ttasks: readonly Task[],\n\tagents: readonly TaskAgent[],\n\t_view: TaskPanelView,\n): { filteredTasks: readonly Task[]; filteredAgents: readonly TaskAgent[] } {\n\tconst roleIds = new Set(agents.filter((a) => a.kind === \"role\").map((a) => a.id));\n\treturn {\n\t\tfilteredAgents: agents.filter((a) => a.kind === \"role\"),\n\t\tfilteredTasks: tasks.filter((t) => roleIds.has(taskOwnerId(t))),\n\t};\n}\n\n/** One renderable row of the subagents lens: a task plus its tree connector. */\ninterface SubagentTreeRow {\n\ttask: Task;\n\ttreePrefix: string;\n}\n\n/**\n * The subagents lens's rows in render order: a depth-first walk of the\n * delegated forest. Roots are parentless subagent/MCP tasks, plus orphans —\n * children whose parent was dropped from the store (their `parentTaskId` no\n * longer resolves), which used to be counted by the header but never rendered.\n * A visited set guards against `parentTaskId` cycles, so a corrupt link can\n * never hang the render loop. Both the lens filter (header counts) and the\n * renderer consume this list, so count and rendering can never disagree.\n */\nfunction subagentLensRows(tasks: readonly Task[]): SubagentTreeRow[] {\n\tconst ids = new Set<number>();\n\tfor (const task of tasks) ids.add(task.id);\n\tconst childrenByParent = new Map<number, Task[]>();\n\tfor (const task of tasks) {\n\t\tif (task.parentTaskId === undefined) continue;\n\t\tconst siblings = childrenByParent.get(task.parentTaskId);\n\t\tif (siblings) siblings.push(task);\n\t\telse childrenByParent.set(task.parentTaskId, [task]);\n\t}\n\tconst roots = tasks.filter(\n\t\t(t) =>\n\t\t\t(t.parentTaskId === undefined && (t.source === \"subagent\" || t.source === \"mcp\")) ||\n\t\t\t(t.parentTaskId !== undefined && !ids.has(t.parentTaskId)),\n\t);\n\n\tconst rows: SubagentTreeRow[] = [];\n\tconst visited = new Set<number>();\n\tconst walk = (task: Task, prefix: string, isLast: boolean, isRoot: boolean): void => {\n\t\tif (visited.has(task.id)) return;\n\t\tvisited.add(task.id);\n\t\trows.push({ task, treePrefix: isRoot ? \"\" : `${prefix}${isLast ? \"└─ \" : \"├─ \"}` });\n\t\tconst kids = childrenByParent.get(task.id) ?? [];\n\t\tconst childPrefix = isRoot ? \"\" : `${prefix}${isLast ? \"   \" : \"│  \"}`;\n\t\tfor (let i = 0; i < kids.length; i++) {\n\t\t\twalk(kids[i] as Task, childPrefix, i === kids.length - 1, false);\n\t\t}\n\t};\n\tfor (const root of roots) walk(root, \"\", true, true);\n\treturn rows;\n}\n\n/**\n * The flat (\"tasks\") lens's rows in render order: the main agent's TodoWrite\n * plan, with each subagent run nested (└─/├─) under the plan item it was\n * dispatched for (`linkedTaskId`, recorded when exactly one item was\n * in_progress). This is the visual tie between the plan and the agents\n * executing it — previously a spawn was linked in spirit but invisible here,\n * living only in the subagents lens. Runs with no link (or a dangling one —\n * their todo was replaced) stay out of this lens; they remain visible in the\n * subagents tree.\n */\nfunction flatLensRows(tasks: readonly Task[]): SubagentTreeRow[] {\n\tconst linkedByTodo = new Map<number, Task[]>();\n\tfor (const task of tasks) {\n\t\tif (task.linkedTaskId === undefined || task.source !== \"subagent\") continue;\n\t\tconst siblings = linkedByTodo.get(task.linkedTaskId);\n\t\tif (siblings) siblings.push(task);\n\t\telse linkedByTodo.set(task.linkedTaskId, [task]);\n\t}\n\tconst rows: SubagentTreeRow[] = [];\n\tfor (const task of tasks) {\n\t\tif (!isMainTask(task)) continue;\n\t\trows.push({ task, treePrefix: \"\" });\n\t\tconst linked = linkedByTodo.get(task.id) ?? [];\n\t\tfor (let i = 0; i < linked.length; i++) {\n\t\t\trows.push({ task: linked[i] as Task, treePrefix: i === linked.length - 1 ? \"└─ \" : \"├─ \" });\n\t\t}\n\t}\n\treturn rows;\n}\n\n/**\n * Scope the full task list to the tasks visible in the given lens. The header,\n * state stamp, and done/total count are derived from this subset so they match\n * exactly what the user sees in the current view.\n */\nfunction filterTasksForLens(\n\ttasks: readonly Task[],\n\tagents: readonly TaskAgent[],\n\tview: TaskPanelView,\n): readonly Task[] {\n\tswitch (view) {\n\t\tcase \"flat\":\n\t\t\t// Same walk the renderer uses (plan items + their linked runs), so the\n\t\t\t// header's done/total always matches the visible rows.\n\t\t\treturn flatLensRows(tasks).map((row) => row.task);\n\t\tcase \"subagents\":\n\t\t\t// Exactly the rows the tree renders (same walk), so the header's\n\t\t\t// done/total always matches the visible rows.\n\t\t\treturn subagentLensRows(tasks).map((row) => row.task);\n\t\tcase \"teams\": {\n\t\t\tconst roleIds = new Set<string>();\n\t\t\tfor (const a of agents) {\n\t\t\t\tif (a.kind === \"role\") roleIds.add(a.id);\n\t\t\t}\n\t\t\treturn tasks.filter((t) => roleIds.has(taskOwnerId(t)));\n\t\t}\n\t}\n}\n\n/** Fallback group metadata when a task's owner has no roster entry. */\nfunction defaultAgentMeta(id: string): TaskAgent {\n\treturn id === \"main\"\n\t\t? { id, name: \"main\", role: \"orchestrator\", kind: \"main\" }\n\t\t: { id, name: id, role: \"subagent\", kind: \"subagent\" };\n}\n\n/**\n * Partition the flat task list into owner groups. An explicit task.agent wins;\n * otherwise a subagent-sourced task falls into a generic \"subagent\" group and\n * everything else into \"main\". Group order is deterministic — main first, then\n * roster order, then stragglers — never reordered by status.\n */\nfunction groupTasks(\n\ttasks: readonly Task[],\n\tagents: readonly TaskAgent[],\n): Array<{ id: string; meta: TaskAgent; items: Task[] }> {\n\tconst meta = new Map<string, TaskAgent>(agents.map((a) => [a.id, a]));\n\tconst groups = new Map<string, Task[]>();\n\tfor (const task of tasks) {\n\t\tconst owner = taskOwnerId(task);\n\t\tconst items = groups.get(owner);\n\t\tif (items) items.push(task);\n\t\telse groups.set(owner, [task]);\n\t}\n\tconst order: string[] = [];\n\tconst ordered = new Set<string>();\n\tconst push = (id: string) => {\n\t\tif (!ordered.has(id)) {\n\t\t\tordered.add(id);\n\t\t\torder.push(id);\n\t\t}\n\t};\n\tif (groups.has(\"main\")) push(\"main\");\n\tfor (const agent of agents) {\n\t\tif (groups.has(agent.id)) push(agent.id);\n\t}\n\tfor (const id of groups.keys()) push(id);\n\treturn order.map((id) => ({\n\t\tid,\n\t\tmeta: meta.get(id) ?? defaultAgentMeta(id),\n\t\titems: groups.get(id) ?? [],\n\t}));\n}\n\n/**\n * Group header for the grouped views: owner glyph + bold name + role, the\n * agent's lifecycle `[state]` tag, an optional handoff arrow (teams), then the\n * agent's own token/cost totals + done/total on the right. Mirrors the footer's\n * \"every number accounted for\" stance, but per agent.\n */\nfunction formatGroupHeader(meta: TaskAgent, items: readonly Task[], width: number, selected = false): string {\n\t// A focused role row swaps its ▸ for a filled ▶ in accent — the team-focus\n\t// selection cursor (the owner-kind glyph mapping itself is unchanged).\n\t// Spawned/team agents carry their identity color on the glyph and name\n\t// (hashed from the agent type, \"explore#2\" → \"explore\"), matching the same\n\t// agent's color in the chat and TaskOutput; main keeps the accent.\n\tconst identityColor: ThemeColor =\n\t\tmeta.kind === \"main\" ? AGENT_GLYPH_COLOR[meta.kind] : agentColorFor(agentTypeOfName(meta.name));\n\tconst glyph = selected\n\t\t? theme.fg(\"accent\", SELECTED_GLYPH)\n\t\t: theme.fg(identityColor, AGENT_GLYPH[meta.kind] ?? AGENT_GLYPH.subagent);\n\tconst name = selected\n\t\t? theme.bold(theme.fg(\"accent\", meta.name))\n\t\t: meta.kind === \"main\"\n\t\t\t? theme.bold(meta.name)\n\t\t\t: theme.bold(theme.fg(identityColor, meta.name));\n\t// Roles read as a dim \"· role\" suffix for spawned/team agents; the main\n\t// orchestrator's role sits brighter (muted), matching the design's .grp-role.\n\tconst role = meta.role\n\t\t? meta.kind === \"main\"\n\t\t\t? ` ${theme.fg(\"muted\", meta.role)}`\n\t\t\t: theme.fg(\"dim\", ` · ${meta.role}`)\n\t\t: \"\";\n\tconst state = meta.state ? ` ${theme.fg(AGENT_STATE_COLOR[meta.state] ?? \"dim\", `[${meta.state}]`)}` : \"\";\n\t// Live activity for a running subagent (e.g. the tool it's executing). Empty\n\t// string means idle/between-tools, so it disappears rather than lingering stale.\n\tconst activity = meta.activity ? theme.fg(\"dim\", ` ⋯ ${meta.activity}`) : \"\";\n\tconst handoff = meta.handoff ? ` ${theme.fg(\"dim\", meta.handoff)}` : \"\";\n\n\tconst done = items.filter((t) => t.status === \"done\").length;\n\tconst countPlain = `${done}/${items.length}`;\n\tconst count = theme.fg(\"muted\", `${done}`) + theme.fg(\"dim\", \"/\") + theme.fg(\"muted\", `${items.length}`);\n\tconst stats = meta.stats;\n\tlet rightPlain = countPlain;\n\tlet rightStyled = count;\n\tif (stats && (stats.input > 0 || stats.output > 0 || stats.cost > 0)) {\n\t\tconst statsPlain = `↑${formatTokens(stats.input)} ↓${formatTokens(stats.output)} · $${stats.cost.toFixed(3)}`;\n\t\trightPlain = `${statsPlain}  ${countPlain}`;\n\t\trightStyled = `${theme.fg(\"dim\", statsPlain)}  ${count}`;\n\t}\n\n\tconst rightWidth = visibleWidth(rightPlain) + 1;\n\tconst leftWidth = Math.max(0, width - rightWidth);\n\tconst left = truncateToWidth(`${glyph} ${name}${role}${state}${activity}${handoff}`, leftWidth, \"…\");\n\tconst pad = Math.max(1, width - visibleWidth(left) - visibleWidth(rightPlain));\n\treturn left + \" \".repeat(pad) + rightStyled;\n}\n\n/**\n * Task panel rendered just above the editor prompt.\n *\n * - A state-colored left rail groups the pane (working=warning, reviewed=success,\n *   stopped=error) without drawing a box.\n * - A tab strip tops the list — one tab per lens with content, each carrying its\n *   own done/total — but only when there is more than one lens to switch to (or\n *   the navigable teams roster is showing). A single-lens pane is rows only.\n * - Shows all tasks with all statuses (pending / in_progress / done / failed).\n *   The active row carries the static ◐ of the design's glyph table; only\n *   delegated rows animate, and only their 1s run clock.\n * - A single-cell owner glyph (◇ subagent / ▸ team role / ⧉ MCP) sits before\n *   the id where it disambiguates; main-owned rows carry no glyph in the flat\n *   lens (the rail already attributes the pane to the main agent). A text\n *   origin tag before the title names the owner: the subagent type\n *   (\"[explore]\"), the team role (\"[planner]\", fed by `--team <url>`), or the\n *   MCP server (\"[github]\").\n * - Three views split by ownership (cycled via app.tasks.cycleForward, shown as a\n *   `tasks · subagents · teams` switcher in the header):\n *     - flat (\"tasks\") — the main agent's own TodoWrite plan, with each\n *       dispatched subagent run nested under the plan item it was dispatched\n *       for (linkedTaskId), so the plan and the agents executing it read as\n *       one picture;\n *     - subagents — a recursive task tree over delegated work, where a subagent\n *       that spawned a subagent shows its nested tasks (roots are the dispatched\n *       subagents and direct MCP calls; children link via parentTaskId, merged\n *       across the process boundary). Each task is its own node keeping its\n *       [subagentMode]/[server] tag, with depth drawn by └─/├─/│ connectors; a\n *       run with only top-level tasks reads flat (no extra indent);\n *     - teams — grouped by named role-agent with handoff arrows.\n *   The cycle is adaptive: empty lenses are skipped and dropped from the\n *   switcher, which hides entirely when only one lens has content. An empty flat\n *   lens (no main task) falls through to subagents when delegated work exists.\n * - LIFO within the window: newest tasks appear at the bottom (closest to the prompt).\n * - Finished tasks carry their wall-clock cost and stay visible until the next\n *   user message arrives (see taskStore.reset()), not the moment they finish.\n * - Collapses to zero lines when there are no tasks — unless a team roster is\n *   registered (`--team`), in which case the empty flat lens falls through to\n *   teams and every role renders as a placeholder group, so idle roles are\n *   visible from startup.\n */\n/** Every row the ledger has, or its counts alone on one row. */\nexport type TaskPanelDensity = \"full\" | \"summary\";\n\nexport class TaskPanelComponent implements Component, Focusable {\n\tprivate readonly ui: TUI | null;\n\tprivate runClockTimer: ReturnType<typeof setInterval> | null = null;\n\tprivate view: TaskPanelView = \"flat\";\n\tprivate density: TaskPanelDensity = \"full\";\n\tprivate disposed = false;\n\n\t// Render memoization. Row strings and derived structures (agentById, the\n\t// subagents tree walk) are cached and invalidated by the store's mutation\n\t// version + pane width. Live tool activity arrives via patchAgent, which bumps\n\t// that version, so an in-progress row is refreshed by the same mechanism as\n\t// everything else — only the delegated run clock moves between mutations and\n\t// has to bypass the cache (see taskLine).\n\tprivate memoVersion = -1;\n\tprivate memoWidth = -1;\n\tprivate lineMemo = new Map<string, string>();\n\tprivate agentByIdMemo: Map<string, TaskAgent> | null = null;\n\tprivate subagentRowsMemo: SubagentTreeRow[] | null = null;\n\tprivate flatRowsMemo: SubagentTreeRow[] | null = null;\n\n\t/** Drop all memoized render state when the store or width changed. */\n\tprivate syncMemo(width: number): void {\n\t\tconst version = taskStore.version();\n\t\tif (version === this.memoVersion && width === this.memoWidth) return;\n\t\tthis.memoVersion = version;\n\t\tthis.memoWidth = width;\n\t\tthis.lineMemo.clear();\n\t\tthis.agentByIdMemo = null;\n\t\tthis.subagentRowsMemo = null;\n\t\tthis.flatRowsMemo = null;\n\t}\n\n\tprivate agentById(agents: readonly TaskAgent[]): Map<string, TaskAgent> {\n\t\tif (!this.agentByIdMemo) {\n\t\t\tthis.agentByIdMemo = new Map(agents.map((a) => [a.id, a]));\n\t\t}\n\t\treturn this.agentByIdMemo;\n\t}\n\n\tprivate subagentRows(tasks: readonly Task[]): SubagentTreeRow[] {\n\t\tif (!this.subagentRowsMemo) {\n\t\t\tthis.subagentRowsMemo = subagentLensRows(tasks);\n\t\t}\n\t\treturn this.subagentRowsMemo;\n\t}\n\n\tprivate flatRows(tasks: readonly Task[]): SubagentTreeRow[] {\n\t\tif (!this.flatRowsMemo) {\n\t\t\tthis.flatRowsMemo = flatLensRows(tasks);\n\t\t}\n\t\treturn this.flatRowsMemo;\n\t}\n\n\t/**\n\t * formatTaskLine with caching for rows that are stable between store mutations.\n\t * Only a live delegated row is not: its run clock advances against Date.now()\n\t * with no mutation to invalidate the entry, so it renders fresh on every tick.\n\t * Everything else — including a live main-plan row, whose activity text arrives\n\t * through a store mutation — is cached until the next mutation or width change\n\t * (handled by syncMemo).\n\t */\n\tprivate taskLine(\n\t\ttask: Task,\n\t\twidth: number,\n\t\toptions: { grouped?: boolean; owner?: TaskAgent; treePrefix?: string } = {},\n\t): string {\n\t\tif (task.status === \"in_progress\" && isDelegated(task)) {\n\t\t\treturn formatTaskLine(task, width, options);\n\t\t}\n\t\tconst key = `${task.id}:${options.grouped ? 1 : 0}:${options.treePrefix ?? \"\"}:${options.owner?.kind ?? \"\"}`;\n\t\tconst hit = this.lineMemo.get(key);\n\t\tif (hit !== undefined) return hit;\n\t\tconst line = formatTaskLine(task, width, options);\n\t\tthis.lineMemo.set(key, line);\n\t\treturn line;\n\t}\n\n\t// Team focus mode: when the TUI focuses the panel, role rows become a\n\t// navigable list (↑/↓ select, n nudge, a attach, q/esc back). The selection\n\t// is tracked by role name so a roster reorder doesn't move the cursor.\n\tfocused = false;\n\tprivate selectedRole: string | undefined;\n\t/** Open the inline nudge editor for the selected role. */\n\tonNudge?: (role: string) => void;\n\t/** Open the attach side panel for the selected role. */\n\tonAttach?: (role: string) => void;\n\t/** Leave team focus (focus returns to the main editor). */\n\tonExitFocus?: () => void;\n\n\tconstructor(ui?: TUI) {\n\t\tthis.ui = ui ?? null;\n\t}\n\n\tinvalidate(): void {\n\t\t// Force the next render to rebuild its memoized rows/structures.\n\t\tthis.memoVersion = -1;\n\t}\n\n\tprivate roleAgents(): TaskAgent[] {\n\t\treturn taskStore.agents().filter((a) => a.kind === \"role\");\n\t}\n\n\t/** The role the team-focus cursor sits on (clamped to the live roster). */\n\tfocusedRole(): string | undefined {\n\t\tconst roles = this.roleAgents();\n\t\tif (roles.length === 0) return undefined;\n\t\tconst match = roles.find((a) => a.name === this.selectedRole);\n\t\treturn (match ?? roles[0]).name;\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst roles = this.roleAgents();\n\t\tif (roles.length === 0) {\n\t\t\tthis.onExitFocus?.();\n\t\t\treturn;\n\t\t}\n\t\tconst keybindings = getKeybindings();\n\t\tconst index = Math.max(\n\t\t\t0,\n\t\t\troles.findIndex((a) => a.name === this.selectedRole),\n\t\t);\n\t\tif (keybindings.matches(data, \"tui.select.up\")) {\n\t\t\tthis.selectedRole = roles[Math.max(0, index - 1)].name;\n\t\t} else if (keybindings.matches(data, \"tui.select.down\")) {\n\t\t\tthis.selectedRole = roles[Math.min(roles.length - 1, index + 1)].name;\n\t\t} else if (matchesAppKey(data, \"app.team.nudge\")) {\n\t\t\tconst role = this.focusedRole();\n\t\t\tif (role) this.onNudge?.(role);\n\t\t} else if (matchesAppKey(data, \"app.team.attach\")) {\n\t\t\tconst role = this.focusedRole();\n\t\t\tif (role) this.onAttach?.(role);\n\t\t} else if (matchesKey(data, \"q\") || keybindings.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.onExitFocus?.();\n\t\t}\n\t\tthis.ui?.requestRender();\n\t}\n\n\tgetView(): TaskPanelView {\n\t\treturn this.view;\n\t}\n\n\tsetView(view: TaskPanelView): void {\n\t\tthis.view = view;\n\t\tthis.ui?.requestRender();\n\t}\n\n\t/**\n\t * How many rows the ledger takes. Driven by `ChromeController`, not by the\n\t * pane itself — the pane knows what it has to say, the chrome knows how much\n\t * room there is to say it in.\n\t */\n\tsetDensity(density: TaskPanelDensity): void {\n\t\tif (this.density === density) return;\n\t\tthis.density = density;\n\t\tthis.ui?.requestRender();\n\t}\n\n\t/**\n\t * Advance to the next view lens with content (flat → subagents → teams →\n\t * flat), skipping empty lenses. With nothing delegated this is a no-op on\n\t * flat; a stale view (its lens emptied since selection) snaps back to flat.\n\t */\n\tcycleView(direction: \"forward\" | \"backward\" = \"forward\"): TaskPanelView {\n\t\tconst available = availableViews(taskStore.list(), taskStore.agents());\n\t\tconst idx = available.indexOf(this.view);\n\t\tconst step = direction === \"forward\" ? 1 : -1;\n\t\tthis.view = available[(idx + step + available.length) % available.length] ?? \"flat\";\n\t\tthis.ui?.requestRender();\n\t\treturn this.view;\n\t}\n\n\t/**\n\t * Tick the run clock only while a delegated row is live. Nothing else in the\n\t * pane moves, so with no subagent or MCP call running there is no timer at all\n\t * — a stale in-progress plan item can no longer pin the process to a render\n\t * loop the way the old 120ms spinner did.\n\t */\n\tprivate ensureRunClock(active: boolean): void {\n\t\tif (this.disposed) {\n\t\t\tif (this.runClockTimer) {\n\t\t\t\tclearInterval(this.runClockTimer);\n\t\t\t\tthis.runClockTimer = null;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (active && this.ui && !this.runClockTimer) {\n\t\t\tthis.runClockTimer = setInterval(() => {\n\t\t\t\tthis.ui?.requestRender();\n\t\t\t}, RUN_CLOCK_INTERVAL_MS);\n\t\t\tthis.runClockTimer.unref?.();\n\t\t} else if (!active && this.runClockTimer) {\n\t\t\tclearInterval(this.runClockTimer);\n\t\t\tthis.runClockTimer = null;\n\t\t}\n\t}\n\n\t/** Stop the run clock. Call on teardown. */\n\tdispose(): void {\n\t\tif (this.runClockTimer) {\n\t\t\tclearInterval(this.runClockTimer);\n\t\t\tthis.runClockTimer = null;\n\t\t}\n\t\tthis.disposed = true;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.disposed) return [];\n\n\t\tthis.syncMemo(width);\n\t\tconst tasks = taskStore.list();\n\t\tconst allAgents = taskStore.agents();\n\n\t\t// Resolve the lens: keep the stored view while it still has content, else\n\t\t// fall through to the first available lens (flat → subagents → teams). The\n\t\t// stored view is untouched so an explicit setView choice resumes once its\n\t\t// content returns. This is how an empty flat lens (no main/TodoWrite task)\n\t\t// falls through to the subagents tree when only delegated work exists, and\n\t\t// how an empty pane falls through to the teams roster at startup.\n\t\tconst available = availableViews(tasks, allAgents);\n\t\tlet view: TaskPanelView = available.includes(this.view) ? this.view : (available[0] ?? \"flat\");\n\n\t\t// Team focus always operates on the teams lens — the focused role list is\n\t\t// exactly what the lens renders, so the cursor is never invisible.\n\t\tif (this.focused) view = \"teams\";\n\n\t\t// In teams view the roster itself is content: role agents render as\n\t\t// placeholder groups even without tasks (idle roles at startup, queued\n\t\t// upcoming work).\n\t\tconst hasRoleRoster = view === \"teams\" && allAgents.some((a) => a.kind === \"role\");\n\n\t\tif (tasks.length === 0 && !hasRoleRoster) {\n\t\t\tthis.ensureRunClock(false);\n\t\t\treturn [];\n\t\t}\n\n\t\t// Scope all visual state to the tasks visible in the current lens so the\n\t\t// animation, rail color, and header count match exactly what the user sees.\n\t\t// The flat and subagents lenses derive from their memoized row walks so\n\t\t// filter and render share one computation (one source of truth for counts).\n\t\tconst lensTasks =\n\t\t\tview === \"subagents\"\n\t\t\t\t? this.subagentRows(tasks).map((row) => row.task)\n\t\t\t\t: view === \"flat\"\n\t\t\t\t\t? this.flatRows(tasks).map((row) => row.task)\n\t\t\t\t\t: filterTasksForLens(tasks, allAgents, view);\n\t\tthis.ensureRunClock(lensTasks.some((t) => t.status === \"in_progress\" && isDelegated(t)));\n\n\t\tconst railColor = PANEL_STATE_COLOR[panelState(lensTasks)];\n\t\tconst gutter = `${theme.fg(railColor, RAIL)} `;\n\t\tconst inner = Math.max(0, width - visibleWidth(RAIL) - 1);\n\n\t\t// `compact` chrome buys transcript rows with ledger rows: the tab strip\n\t\t// already carries every lens's done/total and the rail already carries the\n\t\t// panel's state, so one row still says what is running and how far in it is.\n\t\t// This is a stop on the chrome dial, not something the pane decides for\n\t\t// itself mid-turn — a turn is exactly when the rows are worth having.\n\t\tif (this.density === \"summary\") {\n\t\t\tconst tabViews = available.includes(view) ? available : [...available, view];\n\t\t\treturn [gutter + formatLensTabs(tasks, allAgents, inner, view, tabViews, false, false)];\n\t\t}\n\n\t\t// The header IS the switcher, so a single-lens pane renders rows only. The\n\t\t// teams roster is the exception: it is navigable (app.team.focus), so it keeps\n\t\t// an anchor even alone. The cycle hint is suppressed when there is nothing to\n\t\t// cycle to, and in team focus, which locks the view to teams.\n\t\tconst lines: string[] = [];\n\t\tif (available.length >= 2 || view === \"teams\") {\n\t\t\tconst tabViews = available.includes(view) ? available : [...available, view];\n\t\t\tconst showCycleHint = available.length >= 2 && !this.focused;\n\t\t\t// alt+n only does something on the teams lens, so it is only advertised\n\t\t\t// there — a key hint for a key that would do nothing teaches the wrong thing.\n\t\t\tconst showFocusHint = view === \"teams\" && !this.focused && this.roleAgents().length > 0;\n\t\t\tlines.push(gutter + formatLensTabs(tasks, allAgents, inner, view, tabViews, showCycleHint, showFocusHint));\n\t\t}\n\n\t\tif (view === \"flat\") {\n\t\t\t// The main agent's TodoWrite plan, with each dispatched run nested under\n\t\t\t// the plan item it executes (see flatLensRows). Resolve each row's owner\n\t\t\t// from the roster so the glyph/tag reflect the owning agent's kind and a\n\t\t\t// nested run shows its live activity + timer.\n\t\t\tconst agentById = this.agentById(allAgents);\n\t\t\tfor (const { task, treePrefix } of this.flatRows(tasks)) {\n\t\t\t\tlines.push(gutter + this.taskLine(task, inner, { treePrefix, owner: agentById.get(taskOwnerId(task)) }));\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\tif (view === \"subagents\") {\n\t\t\t// A recursive task tree over the delegated forest: roots are the main\n\t\t\t// agent's dispatched subagents (and direct MCP calls), children are the\n\t\t\t// tasks they spawned in turn (linked by parentTaskId, merged across the\n\t\t\t// process boundary), so a subagent that spawned a subagent is visible.\n\t\t\t// Each task is its own node keeping its [subagentMode]/[server] tag; depth\n\t\t\t// is drawn with └─/├─/│ connectors (see subagentLensRows for orphan and\n\t\t\t// cycle handling). With only top-level tasks the roots carry an empty\n\t\t\t// prefix and read exactly like flat rows (no extra indent). Owners are\n\t\t\t// resolved so an in-progress row shows the run's live tool activity\n\t\t\t// (⋯ grep) instead of a static \"running…\".\n\t\t\tconst agentById = this.agentById(allAgents);\n\t\t\tfor (const { task, treePrefix } of this.subagentRows(tasks)) {\n\t\t\t\tlines.push(\n\t\t\t\t\tgutter +\n\t\t\t\t\t\tthis.taskLine(task, inner, {\n\t\t\t\t\t\t\ttreePrefix,\n\t\t\t\t\t\t\towner: agentById.get(taskOwnerId(task)),\n\t\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\t// teams view: role-agent groups with handoff connectors and queued placeholders.\n\t\tconst { filteredTasks, filteredAgents } = filterTasksForView(tasks, allAgents, view);\n\t\tconst groups = groupTasks(filteredTasks, filteredAgents);\n\t\tconst groupIds = new Set(groups.map((g) => g.id));\n\t\t// Role agents with no tasks still get a group header: idle roles are the\n\t\t// roster at startup, queued ones upcoming work, done/failed ones the\n\t\t// state they settled in after reset() dropped their tasks.\n\t\tfor (const agent of filteredAgents) {\n\t\t\tif (!groupIds.has(agent.id)) {\n\t\t\t\tgroups.push({ id: agent.id, meta: agent, items: [] });\n\t\t\t}\n\t\t}\n\t\tconst cursorRole = this.focused ? this.focusedRole() : undefined;\n\t\tfor (const group of groups) {\n\t\t\tconst selected = cursorRole !== undefined && group.meta.kind === \"role\" && group.meta.name === cursorRole;\n\t\t\tlines.push(gutter + formatGroupHeader(group.meta, group.items, inner, selected));\n\t\t\tfor (const task of group.items) {\n\t\t\t\tlines.push(gutter + this.taskLine(task, inner, { grouped: true }));\n\t\t\t}\n\t\t\t// Forward-handoff connector: emit \"└──→ name\" only for \"→ name\" arrows\n\t\t\t// (not back-references \"← name\"), and only when the target exists in the\n\t\t\t// visible role roster.\n\t\t\tconst { handoff } = group.meta;\n\t\t\tif (handoff) {\n\t\t\t\tconst arrowIdx = handoff.indexOf(\"→ \");\n\t\t\t\tif (arrowIdx !== -1) {\n\t\t\t\t\tconst nextName = handoff.slice(arrowIdx + 2).trim();\n\t\t\t\t\tif (filteredAgents.some((a) => a.name === nextName)) {\n\t\t\t\t\t\tconst connectorPrefix = `${GROUP_INDENT_PLAIN}  └──→ `;\n\t\t\t\t\t\tconst connectorPad = Math.max(0, inner - visibleWidth(connectorPrefix) - visibleWidth(nextName));\n\t\t\t\t\t\tlines.push(\n\t\t\t\t\t\t\tgutter +\n\t\t\t\t\t\t\t\ttheme.fg(\"borderMuted\", connectorPrefix) +\n\t\t\t\t\t\t\t\ttheme.fg(\"dim\", nextName) +\n\t\t\t\t\t\t\t\t\" \".repeat(connectorPad),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (this.focused) {\n\t\t\t// House hint style (see keybinding-hints): dim key + muted description,\n\t\t\t// muted · separators. Arrows and q/esc are literal by convention; the\n\t\t\t// nudge/attach keys resolve from the live keybinding config.\n\t\t\tconst sep = theme.fg(\"muted\", \" · \");\n\t\t\tconst hint = [\n\t\t\t\trawKeyHint(\"↑/↓\", \"select\"),\n\t\t\t\trawKeyHint(appKeyLabel(\"app.team.nudge\"), \"nudge\"),\n\t\t\t\trawKeyHint(appKeyLabel(\"app.team.attach\"), \"attach\"),\n\t\t\t\trawKeyHint(\"q/esc\", \"back\"),\n\t\t\t].join(sep);\n\t\t\tlines.push(gutter + truncateToWidth(hint, inner, \"…\"));\n\t\t}\n\t\treturn lines;\n\t}\n}\n"]}