@kolisachint/hoocode-agent 0.4.36 → 0.4.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +1 -1
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/core/lifeguard.d.ts +11 -0
  6. package/dist/core/lifeguard.d.ts.map +1 -1
  7. package/dist/core/lifeguard.js +68 -3
  8. package/dist/core/lifeguard.js.map +1 -1
  9. package/dist/core/messages.d.ts +41 -4
  10. package/dist/core/messages.d.ts.map +1 -1
  11. package/dist/core/messages.js +67 -11
  12. package/dist/core/messages.js.map +1 -1
  13. package/dist/core/sdk.d.ts.map +1 -1
  14. package/dist/core/sdk.js +2 -1
  15. package/dist/core/sdk.js.map +1 -1
  16. package/dist/core/subagent-pool.d.ts +2 -1
  17. package/dist/core/subagent-pool.d.ts.map +1 -1
  18. package/dist/core/subagent-pool.js +2 -1
  19. package/dist/core/subagent-pool.js.map +1 -1
  20. package/dist/core/task-store.d.ts +6 -1
  21. package/dist/core/task-store.d.ts.map +1 -1
  22. package/dist/core/task-store.js +3 -0
  23. package/dist/core/task-store.js.map +1 -1
  24. package/dist/core/tools/subagent.d.ts.map +1 -1
  25. package/dist/core/tools/subagent.js +3 -2
  26. package/dist/core/tools/subagent.js.map +1 -1
  27. package/dist/extensions/core/hoo-core.d.ts.map +1 -1
  28. package/dist/extensions/core/hoo-core.js +47 -14
  29. package/dist/extensions/core/hoo-core.js.map +1 -1
  30. package/dist/modes/interactive/components/task-panel.d.ts +3 -1
  31. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
  32. package/dist/modes/interactive/components/task-panel.js +27 -5
  33. package/dist/modes/interactive/components/task-panel.js.map +1 -1
  34. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  35. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  36. package/examples/extensions/sandbox/package.json +1 -1
  37. package/examples/extensions/with-deps/package.json +1 -1
  38. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"task-panel.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/task-panel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AAyP/D;;;;;;;;;;;;;;GAcG;AACH,qBAAa,kBAAmB,YAAW,SAAS;IACnD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAa;IAChC,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,cAAc,CAA+C;IAErE,YAAY,EAAE,CAAC,EAAE,GAAG,EAEnB;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,6EAA6E;IAC7E,OAAO,CAAC,eAAe;IAcvB,gDAAgD;IAChD,OAAO,IAAI,IAAI,CAKd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAqB9B;CACD","sourcesContent":["import type { Component, TUI } from \"@kolisachint/hoocode-tui\";\nimport { truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { Task, TaskStatus } from \"../../../core/task-store.js\";\nimport { taskStore } from \"../../../core/task-store.js\";\nimport { theme } from \"../theme/theme.js\";\n\nconst TASK_STATUS_ICON: Record<TaskStatus, string> = {\n\tpending: \"●\",\n\tin_progress: \"◐\",\n\tdone: \"✓\",\n\tfailed: \"✗\",\n};\n\n/** Braille spinner frames + cadence, matched to the TUI Loader so the active row animates in step. */\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst SPINNER_INTERVAL_MS = 80;\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/** Cells in the deterministic progress bar (matches the design's 14-cell track). */\nconst PROGRESS_CELLS = 14;\n\n/** Overall pane state, derived from the task statuses. Drives the rail color + header stamp. */\ntype PanelState = \"working\" | \"reviewed\" | \"stopped\";\n\ninterface StatePresentation {\n\treadonly icon: string;\n\treadonly label: string;\n\treadonly color: \"warning\" | \"success\" | \"error\";\n}\n\nconst STATE_PRESENTATION: Record<PanelState, StatePresentation> = {\n\tworking: { icon: \"◐\", label: \"working\", color: \"warning\" },\n\treviewed: { icon: \"✓\", label: \"reviewed\", color: \"success\" },\n\tstopped: { icon: \"✗\", label: \"stopped\", color: \"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\treturn \"dim\";\n\t}\n}\n\n/** Format a duration in seconds into a compact, terminal-friendly string. */\nfunction formatDuration(secs: number): string {\n\tconst s = Math.max(0, secs);\n\tif (s < 10) return `${s.toFixed(1)}s`;\n\tif (s < 60) return `${Math.round(s)}s`;\n\tconst mins = Math.floor(s / 60);\n\tconst rem = Math.round(s % 60);\n\treturn `${mins}m${rem.toString().padStart(2, \"0\")}s`;\n}\n\n/** Wall-clock time a task occupied, derived from its create/update stamps. */\nfunction taskElapsedSecs(task: Task): number {\n\treturn Math.max(0, (task.updatedAt - task.createdAt) / 1000);\n}\n\n/** Sum the token + cost usage reported by the tasks shown this turn. */\nfunction sumTurnUsage(tasks: readonly Task[]): { input: number; output: number; cost: number } | null {\n\tlet input = 0;\n\tlet output = 0;\n\tlet cost = 0;\n\tfor (const task of tasks) {\n\t\tif (!task.usage) continue;\n\t\tinput += task.usage.input;\n\t\toutput += task.usage.output;\n\t\tcost += task.usage.cost;\n\t}\n\tif (input === 0 && output === 0 && cost === 0) return null;\n\treturn { input, output, cost };\n}\n\n/**\n * Deterministic block-glyph progress bar: a heavy run (━) for the completed\n * fraction over a dim track. In-progress tasks count as half, so the bar moves\n * the moment work starts. Fraction is the only input — no animation, no guess.\n */\nfunction progressBar(done: number, active: number, total: number): { plain: string; styled: string } {\n\tconst ratio = total > 0 ? Math.max(0, Math.min(1, (done + active * 0.5) / total)) : 0;\n\tconst filled = Math.round(ratio * PROGRESS_CELLS);\n\tconst fill = \"━\".repeat(filled);\n\tconst track = \"━\".repeat(PROGRESS_CELLS - filled);\n\treturn {\n\t\tplain: fill + track,\n\t\tstyled: theme.fg(\"success\", fill) + theme.fg(\"dim\", track),\n\t};\n}\n\n/**\n * Ledger header: a state stamp (◐ working / ✓ reviewed / ✗ stopped) + a\n * deterministic progress bar and done/total count on the left, and the per-turn\n * token + elapsed + cost delta (summed across the tasks below) on the right.\n */\nfunction formatHeader(tasks: readonly Task[], width: number, state: PanelState, totalSecs: number): string {\n\tconst total = tasks.length;\n\tconst done = tasks.filter((t) => t.status === \"done\").length;\n\tconst active = tasks.filter((t) => t.status === \"in_progress\").length;\n\n\tconst { icon, label, color } = STATE_PRESENTATION[state];\n\tconst stampPlain = `${icon} ${label.toUpperCase()}`;\n\tconst stamp = `${theme.fg(color, icon)} ${theme.bold(theme.fg(color, label.toUpperCase()))}`;\n\n\tconst bar = progressBar(done, active, total);\n\tconst countPlain = `${done}/${total}`;\n\tconst count = theme.fg(\"muted\", `${done}`) + theme.fg(\"dim\", \"/\") + theme.fg(\"muted\", `${total}`);\n\n\t// Left cluster has a full form (stamp · bar · count) and a compact fallback\n\t// (stamp · count) that drops the bar when the terminal is too narrow.\n\tconst leftFullPlain = `${stampPlain} ${bar.plain} ${countPlain}`;\n\tconst leftFull = `${stamp} ${bar.styled} ${count}`;\n\tconst leftMinPlain = `${stampPlain} ${countPlain}`;\n\tconst leftMin = `${stamp} ${count}`;\n\n\tconst turn = sumTurnUsage(tasks);\n\tlet turnPlain = \"\";\n\tlet turnText = \"\";\n\tif (turn) {\n\t\tconst inTok = formatTokens(turn.input);\n\t\tconst outTok = formatTokens(turn.output);\n\t\tconst elapsed = formatDuration(totalSecs);\n\t\tconst showCost = turn.cost > 0;\n\t\tconst costStr = showCost ? `$${turn.cost.toFixed(3)}` : \"\";\n\t\tturnPlain = `turn ↑${inTok} ↓${outTok} · ${elapsed}${showCost ? ` · ${costStr}` : \"\"}`;\n\t\t// Turn delta: muted framing, numbers one step brighter (bold), separators dim.\n\t\tturnText =\n\t\t\ttheme.fg(\"muted\", \"turn ↑\") +\n\t\t\ttheme.bold(inTok) +\n\t\t\ttheme.fg(\"muted\", \" ↓\") +\n\t\t\ttheme.bold(outTok) +\n\t\t\ttheme.fg(\"dim\", \" · \") +\n\t\t\ttheme.fg(\"muted\", elapsed) +\n\t\t\t(showCost ? theme.fg(\"dim\", \" · \") + theme.bold(costStr) : \"\");\n\t}\n\n\tif (turnPlain) {\n\t\tif (visibleWidth(leftFullPlain) + 2 + visibleWidth(turnPlain) <= width) {\n\t\t\tconst pad = Math.max(2, width - visibleWidth(leftFullPlain) - visibleWidth(turnPlain));\n\t\t\treturn leftFull + \" \".repeat(pad) + turnText;\n\t\t}\n\t\tif (visibleWidth(leftMinPlain) + 2 + visibleWidth(turnPlain) <= width) {\n\t\t\tconst pad = Math.max(2, width - visibleWidth(leftMinPlain) - visibleWidth(turnPlain));\n\t\t\treturn leftMin + \" \".repeat(pad) + turnText;\n\t\t}\n\t}\n\tif (visibleWidth(leftFullPlain) <= width) return leftFull;\n\treturn truncateToWidth(leftMin, width, \"…\");\n}\n\nfunction formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\treturn `${(count / 1000000).toFixed(1)}M`;\n}\n\nfunction formatTaskLine(task: Task, width: number, frame: number): string {\n\tconst isProgress = task.status === \"in_progress\";\n\tconst iconGlyph = isProgress\n\t\t? (SPINNER_FRAMES[frame] ?? TASK_STATUS_ICON.in_progress)\n\t\t: TASK_STATUS_ICON[task.status];\n\tconst icon = theme.fg(taskStatusColor(task.status), iconGlyph);\n\n\tconst idLabel = `#${task.id}`;\n\tconst title = task.title;\n\t// The id recedes (dim); 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\tconst styledId = theme.fg(\"dim\", idLabel);\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 \"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 audit stamp (tokens + elapsed); the\n\t// active row reads `running…`, pending rows read `queued`.\n\tlet rightPlain = \"\";\n\tlet rightStyled = \"\";\n\tif (task.status === \"done\" || task.status === \"failed\") {\n\t\tconst parts: string[] = [];\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\tconst elapsed = formatDuration(taskElapsedSecs(task));\n\t\tif (tokenText) {\n\t\t\tparts.push(tokenText, elapsed);\n\t\t\trightStyled = theme.fg(\"muted\", tokenText) + theme.fg(\"dim\", ` · ${elapsed}`);\n\t\t} else {\n\t\t\tparts.push(elapsed);\n\t\t\trightStyled = theme.fg(\"dim\", elapsed);\n\t\t}\n\t\trightPlain = parts.join(\" · \");\n\t} else if (task.status === \"in_progress\") {\n\t\trightPlain = \"running…\";\n\t\trightStyled = theme.fg(\"warning\", rightPlain);\n\t} else if (task.status === \"pending\") {\n\t\trightPlain = \"queued\";\n\t\trightStyled = theme.fg(\"dim\", rightPlain);\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 = `⚠ ${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\tconst left = truncateToWidth(`${icon} ${styledId} ${styledTitle}`, leftWidth, \"…\");\n\n\tif (!rightPlain) return left;\n\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 ledger header tops the list: a state stamp + deterministic progress bar +\n * done/total count on the left, the per-turn token/elapsed/cost delta on the right.\n * - Shows all tasks with all statuses (pending / in_progress / done / failed).\n * The active row animates a braille spinner; pending rows read `queued`.\n * - Subagent mode is intentionally NOT shown here (e.g. no \"[explore]\" tag).\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.\n */\nexport class TaskPanelComponent implements Component {\n\tprivate readonly ui: TUI | null;\n\tprivate frame = 0;\n\tprivate animationTimer: ReturnType<typeof setInterval> | null = null;\n\n\tconstructor(ui?: TUI) {\n\t\tthis.ui = ui ?? null;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached rendering state.\n\t}\n\n\t/** Run the spinner timer only while a task is active, ticking re-renders. */\n\tprivate ensureAnimation(active: boolean): void {\n\t\tif (active && this.ui && !this.animationTimer) {\n\t\t\tthis.animationTimer = setInterval(() => {\n\t\t\t\tthis.frame = (this.frame + 1) % SPINNER_FRAMES.length;\n\t\t\t\tthis.ui?.requestRender();\n\t\t\t}, SPINNER_INTERVAL_MS);\n\t\t\tthis.animationTimer.unref?.();\n\t\t} else if (!active && this.animationTimer) {\n\t\t\tclearInterval(this.animationTimer);\n\t\t\tthis.animationTimer = null;\n\t\t\tthis.frame = 0;\n\t\t}\n\t}\n\n\t/** Stop the spinner timer. Call on teardown. */\n\tdispose(): void {\n\t\tif (this.animationTimer) {\n\t\t\tclearInterval(this.animationTimer);\n\t\t\tthis.animationTimer = null;\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tconst tasks = taskStore.list();\n\t\tif (tasks.length === 0) {\n\t\t\tthis.ensureAnimation(false);\n\t\t\treturn [];\n\t\t}\n\n\t\tconst hasActive = tasks.some((t) => t.status === \"in_progress\");\n\t\tthis.ensureAnimation(hasActive);\n\n\t\tconst state = panelState(tasks);\n\t\tconst totalSecs = tasks.reduce((sum, t) => sum + taskElapsedSecs(t), 0);\n\t\tconst railColor = STATE_PRESENTATION[state].color;\n\t\tconst gutter = `${theme.fg(railColor, RAIL)} `;\n\t\tconst inner = Math.max(0, width - visibleWidth(RAIL) - 1);\n\n\t\tconst lines: string[] = [gutter + formatHeader(tasks, inner, state, totalSecs)];\n\t\tfor (const task of tasks) {\n\t\t\tlines.push(gutter + formatTaskLine(task, inner, this.frame));\n\t\t}\n\t\treturn lines;\n\t}\n}\n"]}
1
+ {"version":3,"file":"task-panel.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/task-panel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AA4Q/D;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,kBAAmB,YAAW,SAAS;IACnD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAa;IAChC,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,cAAc,CAA+C;IAErE,YAAY,EAAE,CAAC,EAAE,GAAG,EAEnB;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,6EAA6E;IAC7E,OAAO,CAAC,eAAe;IAcvB,gDAAgD;IAChD,OAAO,IAAI,IAAI,CAKd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAyB9B;CACD","sourcesContent":["import type { Component, TUI } from \"@kolisachint/hoocode-tui\";\nimport { truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { Task, TaskSource, TaskStatus } from \"../../../core/task-store.js\";\nimport { taskStore } from \"../../../core/task-store.js\";\nimport { theme } from \"../theme/theme.js\";\n\nconst TASK_STATUS_ICON: Record<TaskStatus, string> = {\n\tpending: \"●\",\n\tin_progress: \"◐\",\n\tdone: \"✓\",\n\tfailed: \"✗\",\n};\n\n/**\n * A single-cell source marker placed before the id so a subagent row and an MCP\n * row are distinguishable at a glance. Plain tasks reserve the cell (blank) to keep\n * the id column aligned. Deliberately a glyph, not a text tag — the pane stays\n * tag-free (no `[explore]`); the glyph just says *where the work came from*.\n */\nconst TASK_SOURCE_GLYPH: Record<TaskSource, string> = {\n\tsubagent: \"⚙\",\n\tmcp: \"⧉\",\n};\n\n/** Braille spinner frames + cadence, matched to the TUI Loader so the active row animates in step. */\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst SPINNER_INTERVAL_MS = 80;\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/** Cells in the deterministic progress bar (matches the design's 14-cell track). */\nconst PROGRESS_CELLS = 14;\n\n/** Overall pane state, derived from the task statuses. Drives the rail color + header stamp. */\ntype PanelState = \"working\" | \"reviewed\" | \"stopped\";\n\ninterface StatePresentation {\n\treadonly icon: string;\n\treadonly label: string;\n\treadonly color: \"warning\" | \"success\" | \"error\";\n}\n\nconst STATE_PRESENTATION: Record<PanelState, StatePresentation> = {\n\tworking: { icon: \"◐\", label: \"working\", color: \"warning\" },\n\treviewed: { icon: \"✓\", label: \"reviewed\", color: \"success\" },\n\tstopped: { icon: \"✗\", label: \"stopped\", color: \"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\treturn \"dim\";\n\t}\n}\n\n/** Format a duration in seconds into a compact, terminal-friendly string. */\nfunction formatDuration(secs: number): string {\n\tconst s = Math.max(0, secs);\n\tif (s < 10) return `${s.toFixed(1)}s`;\n\tif (s < 60) return `${Math.round(s)}s`;\n\tconst mins = Math.floor(s / 60);\n\tconst rem = Math.round(s % 60);\n\treturn `${mins}m${rem.toString().padStart(2, \"0\")}s`;\n}\n\n/** Wall-clock time a task occupied, derived from its create/update stamps. */\nfunction taskElapsedSecs(task: Task): number {\n\treturn Math.max(0, (task.updatedAt - task.createdAt) / 1000);\n}\n\n/** Sum the token + cost usage reported by the tasks shown this turn. */\nfunction sumTurnUsage(tasks: readonly Task[]): { input: number; output: number; cost: number } | null {\n\tlet input = 0;\n\tlet output = 0;\n\tlet cost = 0;\n\tfor (const task of tasks) {\n\t\tif (!task.usage) continue;\n\t\tinput += task.usage.input;\n\t\toutput += task.usage.output;\n\t\tcost += task.usage.cost;\n\t}\n\tif (input === 0 && output === 0 && cost === 0) return null;\n\treturn { input, output, cost };\n}\n\n/**\n * Deterministic block-glyph progress bar: a heavy run (━) for the completed\n * fraction over a dim track. In-progress tasks count as half, so the bar moves\n * the moment work starts. Fraction is the only input — no animation, no guess.\n */\nfunction progressBar(done: number, active: number, total: number): { plain: string; styled: string } {\n\tconst ratio = total > 0 ? Math.max(0, Math.min(1, (done + active * 0.5) / total)) : 0;\n\tconst filled = Math.round(ratio * PROGRESS_CELLS);\n\tconst fill = \"━\".repeat(filled);\n\tconst track = \"━\".repeat(PROGRESS_CELLS - filled);\n\treturn {\n\t\tplain: fill + track,\n\t\tstyled: theme.fg(\"success\", fill) + theme.fg(\"dim\", track),\n\t};\n}\n\n/**\n * Ledger header: a state stamp (◐ working / ✓ reviewed / ✗ stopped) + a\n * deterministic progress bar and done/total count on the left, and the per-turn\n * token + elapsed + cost delta (summed across the tasks below) on the right.\n */\nfunction formatHeader(tasks: readonly Task[], width: number, state: PanelState, totalSecs: number): string {\n\tconst total = tasks.length;\n\tconst done = tasks.filter((t) => t.status === \"done\").length;\n\tconst active = tasks.filter((t) => t.status === \"in_progress\").length;\n\n\tconst { icon, label, color } = STATE_PRESENTATION[state];\n\tconst stampPlain = `${icon} ${label.toUpperCase()}`;\n\tconst stamp = `${theme.fg(color, icon)} ${theme.bold(theme.fg(color, label.toUpperCase()))}`;\n\n\tconst bar = progressBar(done, active, total);\n\tconst countPlain = `${done}/${total}`;\n\tconst count = theme.fg(\"muted\", `${done}`) + theme.fg(\"dim\", \"/\") + theme.fg(\"muted\", `${total}`);\n\n\t// Left cluster has a full form (stamp · bar · count) and a compact fallback\n\t// (stamp · count) that drops the bar when the terminal is too narrow.\n\tconst leftFullPlain = `${stampPlain} ${bar.plain} ${countPlain}`;\n\tconst leftFull = `${stamp} ${bar.styled} ${count}`;\n\tconst leftMinPlain = `${stampPlain} ${countPlain}`;\n\tconst leftMin = `${stamp} ${count}`;\n\n\tconst turn = sumTurnUsage(tasks);\n\tlet turnPlain = \"\";\n\tlet turnText = \"\";\n\tif (turn) {\n\t\tconst inTok = formatTokens(turn.input);\n\t\tconst outTok = formatTokens(turn.output);\n\t\tconst elapsed = formatDuration(totalSecs);\n\t\tconst showCost = turn.cost > 0;\n\t\tconst costStr = showCost ? `$${turn.cost.toFixed(3)}` : \"\";\n\t\tturnPlain = `turn ↑${inTok} ↓${outTok} · ${elapsed}${showCost ? ` · ${costStr}` : \"\"}`;\n\t\t// Turn delta: muted framing, numbers one step brighter (bold), separators dim.\n\t\tturnText =\n\t\t\ttheme.fg(\"muted\", \"turn ↑\") +\n\t\t\ttheme.bold(inTok) +\n\t\t\ttheme.fg(\"muted\", \" ↓\") +\n\t\t\ttheme.bold(outTok) +\n\t\t\ttheme.fg(\"dim\", \" · \") +\n\t\t\ttheme.fg(\"muted\", elapsed) +\n\t\t\t(showCost ? theme.fg(\"dim\", \" · \") + theme.bold(costStr) : \"\");\n\t}\n\n\tif (turnPlain) {\n\t\tif (visibleWidth(leftFullPlain) + 2 + visibleWidth(turnPlain) <= width) {\n\t\t\tconst pad = Math.max(2, width - visibleWidth(leftFullPlain) - visibleWidth(turnPlain));\n\t\t\treturn leftFull + \" \".repeat(pad) + turnText;\n\t\t}\n\t\tif (visibleWidth(leftMinPlain) + 2 + visibleWidth(turnPlain) <= width) {\n\t\t\tconst pad = Math.max(2, width - visibleWidth(leftMinPlain) - visibleWidth(turnPlain));\n\t\t\treturn leftMin + \" \".repeat(pad) + turnText;\n\t\t}\n\t}\n\tif (visibleWidth(leftFullPlain) <= width) return leftFull;\n\treturn truncateToWidth(leftMin, width, \"…\");\n}\n\nfunction formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\treturn `${(count / 1000000).toFixed(1)}M`;\n}\n\nfunction formatTaskLine(task: Task, width: number, frame: number, idColWidth: number): string {\n\tconst isProgress = task.status === \"in_progress\";\n\tconst iconGlyph = isProgress\n\t\t? (SPINNER_FRAMES[frame] ?? TASK_STATUS_ICON.in_progress)\n\t\t: TASK_STATUS_ICON[task.status];\n\tconst icon = theme.fg(taskStatusColor(task.status), iconGlyph);\n\n\t// Source marker between the status icon and the id. Reserve the cell (blank) for\n\t// plain tasks so ids stay column-aligned whether or not a glyph is present.\n\tconst sourceGlyph = task.source ? TASK_SOURCE_GLYPH[task.source] : \" \";\n\tconst styledSource = task.source ? theme.fg(\"dim\", sourceGlyph) : \" \";\n\n\t// Right-pad the id to the shared column width so titles line up across rows even\n\t// when ids differ in digit count (#1 vs #10). Padding is plain spaces inside the\n\t// dim styling, so it adds no visible color.\n\tconst idLabel = `#${task.id}`.padEnd(idColWidth);\n\tconst title = task.title;\n\t// The id recedes (dim); 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\tconst styledId = theme.fg(\"dim\", idLabel);\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 \"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 audit stamp (tokens + elapsed); the\n\t// active row reads `running…`, pending rows read `queued`.\n\tlet rightPlain = \"\";\n\tlet rightStyled = \"\";\n\tif (task.status === \"done\" || task.status === \"failed\") {\n\t\tconst parts: string[] = [];\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\tconst elapsed = formatDuration(taskElapsedSecs(task));\n\t\tif (tokenText) {\n\t\t\tparts.push(tokenText, elapsed);\n\t\t\trightStyled = theme.fg(\"muted\", tokenText) + theme.fg(\"dim\", ` · ${elapsed}`);\n\t\t} else {\n\t\t\tparts.push(elapsed);\n\t\t\trightStyled = theme.fg(\"dim\", elapsed);\n\t\t}\n\t\trightPlain = parts.join(\" · \");\n\t} else if (task.status === \"in_progress\") {\n\t\trightPlain = \"running…\";\n\t\trightStyled = theme.fg(\"warning\", rightPlain);\n\t} else if (task.status === \"pending\") {\n\t\trightPlain = \"queued\";\n\t\trightStyled = theme.fg(\"dim\", rightPlain);\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 = `⚠ ${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\tconst left = truncateToWidth(`${icon} ${styledSource} ${styledId} ${styledTitle}`, leftWidth, \"…\");\n\n\tif (!rightPlain) return left;\n\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 ledger header tops the list: a state stamp + deterministic progress bar +\n * done/total count on the left, the per-turn token/elapsed/cost delta on the right.\n * - Shows all tasks with all statuses (pending / in_progress / done / failed).\n * The active row animates a braille spinner; pending rows read `queued`.\n * - A single-cell source glyph (⚙ subagent / ⧉ MCP) sits before the id so the two\n * kinds of background work are distinguishable. The subagent *mode* tag (e.g.\n * \"[explore]\") is still intentionally NOT shown — the pane stays tag-free.\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.\n */\nexport class TaskPanelComponent implements Component {\n\tprivate readonly ui: TUI | null;\n\tprivate frame = 0;\n\tprivate animationTimer: ReturnType<typeof setInterval> | null = null;\n\n\tconstructor(ui?: TUI) {\n\t\tthis.ui = ui ?? null;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached rendering state.\n\t}\n\n\t/** Run the spinner timer only while a task is active, ticking re-renders. */\n\tprivate ensureAnimation(active: boolean): void {\n\t\tif (active && this.ui && !this.animationTimer) {\n\t\t\tthis.animationTimer = setInterval(() => {\n\t\t\t\tthis.frame = (this.frame + 1) % SPINNER_FRAMES.length;\n\t\t\t\tthis.ui?.requestRender();\n\t\t\t}, SPINNER_INTERVAL_MS);\n\t\t\tthis.animationTimer.unref?.();\n\t\t} else if (!active && this.animationTimer) {\n\t\t\tclearInterval(this.animationTimer);\n\t\t\tthis.animationTimer = null;\n\t\t\tthis.frame = 0;\n\t\t}\n\t}\n\n\t/** Stop the spinner timer. Call on teardown. */\n\tdispose(): void {\n\t\tif (this.animationTimer) {\n\t\t\tclearInterval(this.animationTimer);\n\t\t\tthis.animationTimer = null;\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tconst tasks = taskStore.list();\n\t\tif (tasks.length === 0) {\n\t\t\tthis.ensureAnimation(false);\n\t\t\treturn [];\n\t\t}\n\n\t\tconst hasActive = tasks.some((t) => t.status === \"in_progress\");\n\t\tthis.ensureAnimation(hasActive);\n\n\t\tconst state = panelState(tasks);\n\t\tconst totalSecs = tasks.reduce((sum, t) => sum + taskElapsedSecs(t), 0);\n\t\tconst railColor = STATE_PRESENTATION[state].color;\n\t\tconst gutter = `${theme.fg(railColor, RAIL)} `;\n\t\tconst inner = Math.max(0, width - visibleWidth(RAIL) - 1);\n\n\t\t// Width of the id column, sized to the widest id on screen, so every title\n\t\t// starts at the same column regardless of digit count (#1 vs #10 vs #100).\n\t\tconst idColWidth = tasks.reduce((max, t) => Math.max(max, `#${t.id}`.length), 0);\n\n\t\tconst lines: string[] = [gutter + formatHeader(tasks, inner, state, totalSecs)];\n\t\tfor (const task of tasks) {\n\t\t\tlines.push(gutter + formatTaskLine(task, inner, this.frame, idColWidth));\n\t\t}\n\t\treturn lines;\n\t}\n}\n"]}
@@ -7,6 +7,16 @@ const TASK_STATUS_ICON = {
7
7
  done: "✓",
8
8
  failed: "✗",
9
9
  };
10
+ /**
11
+ * A single-cell source marker placed before the id so a subagent row and an MCP
12
+ * row are distinguishable at a glance. Plain tasks reserve the cell (blank) to keep
13
+ * the id column aligned. Deliberately a glyph, not a text tag — the pane stays
14
+ * tag-free (no `[explore]`); the glyph just says *where the work came from*.
15
+ */
16
+ const TASK_SOURCE_GLYPH = {
17
+ subagent: "⚙",
18
+ mcp: "⧉",
19
+ };
10
20
  /** Braille spinner frames + cadence, matched to the TUI Loader so the active row animates in step. */
11
21
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
12
22
  const SPINNER_INTERVAL_MS = 80;
@@ -147,13 +157,20 @@ function formatTokens(count) {
147
157
  return `${Math.round(count / 1000)}k`;
148
158
  return `${(count / 1000000).toFixed(1)}M`;
149
159
  }
150
- function formatTaskLine(task, width, frame) {
160
+ function formatTaskLine(task, width, frame, idColWidth) {
151
161
  const isProgress = task.status === "in_progress";
152
162
  const iconGlyph = isProgress
153
163
  ? (SPINNER_FRAMES[frame] ?? TASK_STATUS_ICON.in_progress)
154
164
  : TASK_STATUS_ICON[task.status];
155
165
  const icon = theme.fg(taskStatusColor(task.status), iconGlyph);
156
- const idLabel = `#${task.id}`;
166
+ // Source marker between the status icon and the id. Reserve the cell (blank) for
167
+ // plain tasks so ids stay column-aligned whether or not a glyph is present.
168
+ const sourceGlyph = task.source ? TASK_SOURCE_GLYPH[task.source] : " ";
169
+ const styledSource = task.source ? theme.fg("dim", sourceGlyph) : " ";
170
+ // Right-pad the id to the shared column width so titles line up across rows even
171
+ // when ids differ in digit count (#1 vs #10). Padding is plain spaces inside the
172
+ // dim styling, so it adds no visible color.
173
+ const idLabel = `#${task.id}`.padEnd(idColWidth);
157
174
  const title = task.title;
158
175
  // The id recedes (dim); the title carries the line. Done titles fade to muted
159
176
  // (settled work), pending dim (not started), active goes bold, failed turns red.
@@ -217,7 +234,7 @@ function formatTaskLine(task, width, frame) {
217
234
  // truncateToWidth measures visible width (ANSI-aware), so the styled left can be
218
235
  // truncated against the full left budget directly. Subtracting the prefix here
219
236
  // (as a prior version did) truncated titles early and unevenly per id width.
220
- const left = truncateToWidth(`${icon} ${styledId} ${styledTitle}`, leftWidth, "…");
237
+ const left = truncateToWidth(`${icon} ${styledSource} ${styledId} ${styledTitle}`, leftWidth, "…");
221
238
  if (!rightPlain)
222
239
  return left;
223
240
  const pad = Math.max(1, width - visibleWidth(left) - visibleWidth(rightPlain));
@@ -232,7 +249,9 @@ function formatTaskLine(task, width, frame) {
232
249
  * done/total count on the left, the per-turn token/elapsed/cost delta on the right.
233
250
  * - Shows all tasks with all statuses (pending / in_progress / done / failed).
234
251
  * The active row animates a braille spinner; pending rows read `queued`.
235
- * - Subagent mode is intentionally NOT shown here (e.g. no "[explore]" tag).
252
+ * - A single-cell source glyph (⚙ subagent / MCP) sits before the id so the two
253
+ * kinds of background work are distinguishable. The subagent *mode* tag (e.g.
254
+ * "[explore]") is still intentionally NOT shown — the pane stays tag-free.
236
255
  * - LIFO within the window: newest tasks appear at the bottom (closest to the prompt).
237
256
  * - Finished tasks carry their wall-clock cost and stay visible until the next
238
257
  * user message arrives (see taskStore.reset()), not the moment they finish.
@@ -283,9 +302,12 @@ export class TaskPanelComponent {
283
302
  const railColor = STATE_PRESENTATION[state].color;
284
303
  const gutter = `${theme.fg(railColor, RAIL)} `;
285
304
  const inner = Math.max(0, width - visibleWidth(RAIL) - 1);
305
+ // Width of the id column, sized to the widest id on screen, so every title
306
+ // starts at the same column regardless of digit count (#1 vs #10 vs #100).
307
+ const idColWidth = tasks.reduce((max, t) => Math.max(max, `#${t.id}`.length), 0);
286
308
  const lines = [gutter + formatHeader(tasks, inner, state, totalSecs)];
287
309
  for (const task of tasks) {
288
- lines.push(gutter + formatTaskLine(task, inner, this.frame));
310
+ lines.push(gutter + formatTaskLine(task, inner, this.frame, idColWidth));
289
311
  }
290
312
  return lines;
291
313
  }
@@ -1 +1 @@
1
- {"version":3,"file":"task-panel.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/task-panel.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component, TUI } from \"@kolisachint/hoocode-tui\";\nimport { truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { Task, TaskStatus } from \"../../../core/task-store.js\";\nimport { taskStore } from \"../../../core/task-store.js\";\nimport { theme } from \"../theme/theme.js\";\n\nconst TASK_STATUS_ICON: Record<TaskStatus, string> = {\n\tpending: \"●\",\n\tin_progress: \"◐\",\n\tdone: \"✓\",\n\tfailed: \"✗\",\n};\n\n/** Braille spinner frames + cadence, matched to the TUI Loader so the active row animates in step. */\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst SPINNER_INTERVAL_MS = 80;\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/** Cells in the deterministic progress bar (matches the design's 14-cell track). */\nconst PROGRESS_CELLS = 14;\n\n/** Overall pane state, derived from the task statuses. Drives the rail color + header stamp. */\ntype PanelState = \"working\" | \"reviewed\" | \"stopped\";\n\ninterface StatePresentation {\n\treadonly icon: string;\n\treadonly label: string;\n\treadonly color: \"warning\" | \"success\" | \"error\";\n}\n\nconst STATE_PRESENTATION: Record<PanelState, StatePresentation> = {\n\tworking: { icon: \"◐\", label: \"working\", color: \"warning\" },\n\treviewed: { icon: \"✓\", label: \"reviewed\", color: \"success\" },\n\tstopped: { icon: \"✗\", label: \"stopped\", color: \"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\treturn \"dim\";\n\t}\n}\n\n/** Format a duration in seconds into a compact, terminal-friendly string. */\nfunction formatDuration(secs: number): string {\n\tconst s = Math.max(0, secs);\n\tif (s < 10) return `${s.toFixed(1)}s`;\n\tif (s < 60) return `${Math.round(s)}s`;\n\tconst mins = Math.floor(s / 60);\n\tconst rem = Math.round(s % 60);\n\treturn `${mins}m${rem.toString().padStart(2, \"0\")}s`;\n}\n\n/** Wall-clock time a task occupied, derived from its create/update stamps. */\nfunction taskElapsedSecs(task: Task): number {\n\treturn Math.max(0, (task.updatedAt - task.createdAt) / 1000);\n}\n\n/** Sum the token + cost usage reported by the tasks shown this turn. */\nfunction sumTurnUsage(tasks: readonly Task[]): { input: number; output: number; cost: number } | null {\n\tlet input = 0;\n\tlet output = 0;\n\tlet cost = 0;\n\tfor (const task of tasks) {\n\t\tif (!task.usage) continue;\n\t\tinput += task.usage.input;\n\t\toutput += task.usage.output;\n\t\tcost += task.usage.cost;\n\t}\n\tif (input === 0 && output === 0 && cost === 0) return null;\n\treturn { input, output, cost };\n}\n\n/**\n * Deterministic block-glyph progress bar: a heavy run (━) for the completed\n * fraction over a dim track. In-progress tasks count as half, so the bar moves\n * the moment work starts. Fraction is the only input — no animation, no guess.\n */\nfunction progressBar(done: number, active: number, total: number): { plain: string; styled: string } {\n\tconst ratio = total > 0 ? Math.max(0, Math.min(1, (done + active * 0.5) / total)) : 0;\n\tconst filled = Math.round(ratio * PROGRESS_CELLS);\n\tconst fill = \"━\".repeat(filled);\n\tconst track = \"━\".repeat(PROGRESS_CELLS - filled);\n\treturn {\n\t\tplain: fill + track,\n\t\tstyled: theme.fg(\"success\", fill) + theme.fg(\"dim\", track),\n\t};\n}\n\n/**\n * Ledger header: a state stamp (◐ working / ✓ reviewed / ✗ stopped) + a\n * deterministic progress bar and done/total count on the left, and the per-turn\n * token + elapsed + cost delta (summed across the tasks below) on the right.\n */\nfunction formatHeader(tasks: readonly Task[], width: number, state: PanelState, totalSecs: number): string {\n\tconst total = tasks.length;\n\tconst done = tasks.filter((t) => t.status === \"done\").length;\n\tconst active = tasks.filter((t) => t.status === \"in_progress\").length;\n\n\tconst { icon, label, color } = STATE_PRESENTATION[state];\n\tconst stampPlain = `${icon} ${label.toUpperCase()}`;\n\tconst stamp = `${theme.fg(color, icon)} ${theme.bold(theme.fg(color, label.toUpperCase()))}`;\n\n\tconst bar = progressBar(done, active, total);\n\tconst countPlain = `${done}/${total}`;\n\tconst count = theme.fg(\"muted\", `${done}`) + theme.fg(\"dim\", \"/\") + theme.fg(\"muted\", `${total}`);\n\n\t// Left cluster has a full form (stamp · bar · count) and a compact fallback\n\t// (stamp · count) that drops the bar when the terminal is too narrow.\n\tconst leftFullPlain = `${stampPlain} ${bar.plain} ${countPlain}`;\n\tconst leftFull = `${stamp} ${bar.styled} ${count}`;\n\tconst leftMinPlain = `${stampPlain} ${countPlain}`;\n\tconst leftMin = `${stamp} ${count}`;\n\n\tconst turn = sumTurnUsage(tasks);\n\tlet turnPlain = \"\";\n\tlet turnText = \"\";\n\tif (turn) {\n\t\tconst inTok = formatTokens(turn.input);\n\t\tconst outTok = formatTokens(turn.output);\n\t\tconst elapsed = formatDuration(totalSecs);\n\t\tconst showCost = turn.cost > 0;\n\t\tconst costStr = showCost ? `$${turn.cost.toFixed(3)}` : \"\";\n\t\tturnPlain = `turn ↑${inTok} ↓${outTok} · ${elapsed}${showCost ? ` · ${costStr}` : \"\"}`;\n\t\t// Turn delta: muted framing, numbers one step brighter (bold), separators dim.\n\t\tturnText =\n\t\t\ttheme.fg(\"muted\", \"turn ↑\") +\n\t\t\ttheme.bold(inTok) +\n\t\t\ttheme.fg(\"muted\", \" ↓\") +\n\t\t\ttheme.bold(outTok) +\n\t\t\ttheme.fg(\"dim\", \" · \") +\n\t\t\ttheme.fg(\"muted\", elapsed) +\n\t\t\t(showCost ? theme.fg(\"dim\", \" · \") + theme.bold(costStr) : \"\");\n\t}\n\n\tif (turnPlain) {\n\t\tif (visibleWidth(leftFullPlain) + 2 + visibleWidth(turnPlain) <= width) {\n\t\t\tconst pad = Math.max(2, width - visibleWidth(leftFullPlain) - visibleWidth(turnPlain));\n\t\t\treturn leftFull + \" \".repeat(pad) + turnText;\n\t\t}\n\t\tif (visibleWidth(leftMinPlain) + 2 + visibleWidth(turnPlain) <= width) {\n\t\t\tconst pad = Math.max(2, width - visibleWidth(leftMinPlain) - visibleWidth(turnPlain));\n\t\t\treturn leftMin + \" \".repeat(pad) + turnText;\n\t\t}\n\t}\n\tif (visibleWidth(leftFullPlain) <= width) return leftFull;\n\treturn truncateToWidth(leftMin, width, \"…\");\n}\n\nfunction formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\treturn `${(count / 1000000).toFixed(1)}M`;\n}\n\nfunction formatTaskLine(task: Task, width: number, frame: number): string {\n\tconst isProgress = task.status === \"in_progress\";\n\tconst iconGlyph = isProgress\n\t\t? (SPINNER_FRAMES[frame] ?? TASK_STATUS_ICON.in_progress)\n\t\t: TASK_STATUS_ICON[task.status];\n\tconst icon = theme.fg(taskStatusColor(task.status), iconGlyph);\n\n\tconst idLabel = `#${task.id}`;\n\tconst title = task.title;\n\t// The id recedes (dim); 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\tconst styledId = theme.fg(\"dim\", idLabel);\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 \"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 audit stamp (tokens + elapsed); the\n\t// active row reads `running…`, pending rows read `queued`.\n\tlet rightPlain = \"\";\n\tlet rightStyled = \"\";\n\tif (task.status === \"done\" || task.status === \"failed\") {\n\t\tconst parts: string[] = [];\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\tconst elapsed = formatDuration(taskElapsedSecs(task));\n\t\tif (tokenText) {\n\t\t\tparts.push(tokenText, elapsed);\n\t\t\trightStyled = theme.fg(\"muted\", tokenText) + theme.fg(\"dim\", ` · ${elapsed}`);\n\t\t} else {\n\t\t\tparts.push(elapsed);\n\t\t\trightStyled = theme.fg(\"dim\", elapsed);\n\t\t}\n\t\trightPlain = parts.join(\" · \");\n\t} else if (task.status === \"in_progress\") {\n\t\trightPlain = \"running…\";\n\t\trightStyled = theme.fg(\"warning\", rightPlain);\n\t} else if (task.status === \"pending\") {\n\t\trightPlain = \"queued\";\n\t\trightStyled = theme.fg(\"dim\", rightPlain);\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 = `⚠ ${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\tconst left = truncateToWidth(`${icon} ${styledId} ${styledTitle}`, leftWidth, \"…\");\n\n\tif (!rightPlain) return left;\n\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 ledger header tops the list: a state stamp + deterministic progress bar +\n * done/total count on the left, the per-turn token/elapsed/cost delta on the right.\n * - Shows all tasks with all statuses (pending / in_progress / done / failed).\n * The active row animates a braille spinner; pending rows read `queued`.\n * - Subagent mode is intentionally NOT shown here (e.g. no \"[explore]\" tag).\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.\n */\nexport class TaskPanelComponent implements Component {\n\tprivate readonly ui: TUI | null;\n\tprivate frame = 0;\n\tprivate animationTimer: ReturnType<typeof setInterval> | null = null;\n\n\tconstructor(ui?: TUI) {\n\t\tthis.ui = ui ?? null;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached rendering state.\n\t}\n\n\t/** Run the spinner timer only while a task is active, ticking re-renders. */\n\tprivate ensureAnimation(active: boolean): void {\n\t\tif (active && this.ui && !this.animationTimer) {\n\t\t\tthis.animationTimer = setInterval(() => {\n\t\t\t\tthis.frame = (this.frame + 1) % SPINNER_FRAMES.length;\n\t\t\t\tthis.ui?.requestRender();\n\t\t\t}, SPINNER_INTERVAL_MS);\n\t\t\tthis.animationTimer.unref?.();\n\t\t} else if (!active && this.animationTimer) {\n\t\t\tclearInterval(this.animationTimer);\n\t\t\tthis.animationTimer = null;\n\t\t\tthis.frame = 0;\n\t\t}\n\t}\n\n\t/** Stop the spinner timer. Call on teardown. */\n\tdispose(): void {\n\t\tif (this.animationTimer) {\n\t\t\tclearInterval(this.animationTimer);\n\t\t\tthis.animationTimer = null;\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tconst tasks = taskStore.list();\n\t\tif (tasks.length === 0) {\n\t\t\tthis.ensureAnimation(false);\n\t\t\treturn [];\n\t\t}\n\n\t\tconst hasActive = tasks.some((t) => t.status === \"in_progress\");\n\t\tthis.ensureAnimation(hasActive);\n\n\t\tconst state = panelState(tasks);\n\t\tconst totalSecs = tasks.reduce((sum, t) => sum + taskElapsedSecs(t), 0);\n\t\tconst railColor = STATE_PRESENTATION[state].color;\n\t\tconst gutter = `${theme.fg(railColor, RAIL)} `;\n\t\tconst inner = Math.max(0, width - visibleWidth(RAIL) - 1);\n\n\t\tconst lines: string[] = [gutter + formatHeader(tasks, inner, state, totalSecs)];\n\t\tfor (const task of tasks) {\n\t\t\tlines.push(gutter + formatTaskLine(task, inner, this.frame));\n\t\t}\n\t\treturn lines;\n\t}\n}\n"]}
1
+ {"version":3,"file":"task-panel.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/task-panel.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component, TUI } from \"@kolisachint/hoocode-tui\";\nimport { truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { Task, TaskSource, TaskStatus } from \"../../../core/task-store.js\";\nimport { taskStore } from \"../../../core/task-store.js\";\nimport { theme } from \"../theme/theme.js\";\n\nconst TASK_STATUS_ICON: Record<TaskStatus, string> = {\n\tpending: \"●\",\n\tin_progress: \"◐\",\n\tdone: \"✓\",\n\tfailed: \"✗\",\n};\n\n/**\n * A single-cell source marker placed before the id so a subagent row and an MCP\n * row are distinguishable at a glance. Plain tasks reserve the cell (blank) to keep\n * the id column aligned. Deliberately a glyph, not a text tag — the pane stays\n * tag-free (no `[explore]`); the glyph just says *where the work came from*.\n */\nconst TASK_SOURCE_GLYPH: Record<TaskSource, string> = {\n\tsubagent: \"⚙\",\n\tmcp: \"⧉\",\n};\n\n/** Braille spinner frames + cadence, matched to the TUI Loader so the active row animates in step. */\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst SPINNER_INTERVAL_MS = 80;\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/** Cells in the deterministic progress bar (matches the design's 14-cell track). */\nconst PROGRESS_CELLS = 14;\n\n/** Overall pane state, derived from the task statuses. Drives the rail color + header stamp. */\ntype PanelState = \"working\" | \"reviewed\" | \"stopped\";\n\ninterface StatePresentation {\n\treadonly icon: string;\n\treadonly label: string;\n\treadonly color: \"warning\" | \"success\" | \"error\";\n}\n\nconst STATE_PRESENTATION: Record<PanelState, StatePresentation> = {\n\tworking: { icon: \"◐\", label: \"working\", color: \"warning\" },\n\treviewed: { icon: \"✓\", label: \"reviewed\", color: \"success\" },\n\tstopped: { icon: \"✗\", label: \"stopped\", color: \"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\treturn \"dim\";\n\t}\n}\n\n/** Format a duration in seconds into a compact, terminal-friendly string. */\nfunction formatDuration(secs: number): string {\n\tconst s = Math.max(0, secs);\n\tif (s < 10) return `${s.toFixed(1)}s`;\n\tif (s < 60) return `${Math.round(s)}s`;\n\tconst mins = Math.floor(s / 60);\n\tconst rem = Math.round(s % 60);\n\treturn `${mins}m${rem.toString().padStart(2, \"0\")}s`;\n}\n\n/** Wall-clock time a task occupied, derived from its create/update stamps. */\nfunction taskElapsedSecs(task: Task): number {\n\treturn Math.max(0, (task.updatedAt - task.createdAt) / 1000);\n}\n\n/** Sum the token + cost usage reported by the tasks shown this turn. */\nfunction sumTurnUsage(tasks: readonly Task[]): { input: number; output: number; cost: number } | null {\n\tlet input = 0;\n\tlet output = 0;\n\tlet cost = 0;\n\tfor (const task of tasks) {\n\t\tif (!task.usage) continue;\n\t\tinput += task.usage.input;\n\t\toutput += task.usage.output;\n\t\tcost += task.usage.cost;\n\t}\n\tif (input === 0 && output === 0 && cost === 0) return null;\n\treturn { input, output, cost };\n}\n\n/**\n * Deterministic block-glyph progress bar: a heavy run (━) for the completed\n * fraction over a dim track. In-progress tasks count as half, so the bar moves\n * the moment work starts. Fraction is the only input — no animation, no guess.\n */\nfunction progressBar(done: number, active: number, total: number): { plain: string; styled: string } {\n\tconst ratio = total > 0 ? Math.max(0, Math.min(1, (done + active * 0.5) / total)) : 0;\n\tconst filled = Math.round(ratio * PROGRESS_CELLS);\n\tconst fill = \"━\".repeat(filled);\n\tconst track = \"━\".repeat(PROGRESS_CELLS - filled);\n\treturn {\n\t\tplain: fill + track,\n\t\tstyled: theme.fg(\"success\", fill) + theme.fg(\"dim\", track),\n\t};\n}\n\n/**\n * Ledger header: a state stamp (◐ working / ✓ reviewed / ✗ stopped) + a\n * deterministic progress bar and done/total count on the left, and the per-turn\n * token + elapsed + cost delta (summed across the tasks below) on the right.\n */\nfunction formatHeader(tasks: readonly Task[], width: number, state: PanelState, totalSecs: number): string {\n\tconst total = tasks.length;\n\tconst done = tasks.filter((t) => t.status === \"done\").length;\n\tconst active = tasks.filter((t) => t.status === \"in_progress\").length;\n\n\tconst { icon, label, color } = STATE_PRESENTATION[state];\n\tconst stampPlain = `${icon} ${label.toUpperCase()}`;\n\tconst stamp = `${theme.fg(color, icon)} ${theme.bold(theme.fg(color, label.toUpperCase()))}`;\n\n\tconst bar = progressBar(done, active, total);\n\tconst countPlain = `${done}/${total}`;\n\tconst count = theme.fg(\"muted\", `${done}`) + theme.fg(\"dim\", \"/\") + theme.fg(\"muted\", `${total}`);\n\n\t// Left cluster has a full form (stamp · bar · count) and a compact fallback\n\t// (stamp · count) that drops the bar when the terminal is too narrow.\n\tconst leftFullPlain = `${stampPlain} ${bar.plain} ${countPlain}`;\n\tconst leftFull = `${stamp} ${bar.styled} ${count}`;\n\tconst leftMinPlain = `${stampPlain} ${countPlain}`;\n\tconst leftMin = `${stamp} ${count}`;\n\n\tconst turn = sumTurnUsage(tasks);\n\tlet turnPlain = \"\";\n\tlet turnText = \"\";\n\tif (turn) {\n\t\tconst inTok = formatTokens(turn.input);\n\t\tconst outTok = formatTokens(turn.output);\n\t\tconst elapsed = formatDuration(totalSecs);\n\t\tconst showCost = turn.cost > 0;\n\t\tconst costStr = showCost ? `$${turn.cost.toFixed(3)}` : \"\";\n\t\tturnPlain = `turn ↑${inTok} ↓${outTok} · ${elapsed}${showCost ? ` · ${costStr}` : \"\"}`;\n\t\t// Turn delta: muted framing, numbers one step brighter (bold), separators dim.\n\t\tturnText =\n\t\t\ttheme.fg(\"muted\", \"turn ↑\") +\n\t\t\ttheme.bold(inTok) +\n\t\t\ttheme.fg(\"muted\", \" ↓\") +\n\t\t\ttheme.bold(outTok) +\n\t\t\ttheme.fg(\"dim\", \" · \") +\n\t\t\ttheme.fg(\"muted\", elapsed) +\n\t\t\t(showCost ? theme.fg(\"dim\", \" · \") + theme.bold(costStr) : \"\");\n\t}\n\n\tif (turnPlain) {\n\t\tif (visibleWidth(leftFullPlain) + 2 + visibleWidth(turnPlain) <= width) {\n\t\t\tconst pad = Math.max(2, width - visibleWidth(leftFullPlain) - visibleWidth(turnPlain));\n\t\t\treturn leftFull + \" \".repeat(pad) + turnText;\n\t\t}\n\t\tif (visibleWidth(leftMinPlain) + 2 + visibleWidth(turnPlain) <= width) {\n\t\t\tconst pad = Math.max(2, width - visibleWidth(leftMinPlain) - visibleWidth(turnPlain));\n\t\t\treturn leftMin + \" \".repeat(pad) + turnText;\n\t\t}\n\t}\n\tif (visibleWidth(leftFullPlain) <= width) return leftFull;\n\treturn truncateToWidth(leftMin, width, \"…\");\n}\n\nfunction formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\treturn `${(count / 1000000).toFixed(1)}M`;\n}\n\nfunction formatTaskLine(task: Task, width: number, frame: number, idColWidth: number): string {\n\tconst isProgress = task.status === \"in_progress\";\n\tconst iconGlyph = isProgress\n\t\t? (SPINNER_FRAMES[frame] ?? TASK_STATUS_ICON.in_progress)\n\t\t: TASK_STATUS_ICON[task.status];\n\tconst icon = theme.fg(taskStatusColor(task.status), iconGlyph);\n\n\t// Source marker between the status icon and the id. Reserve the cell (blank) for\n\t// plain tasks so ids stay column-aligned whether or not a glyph is present.\n\tconst sourceGlyph = task.source ? TASK_SOURCE_GLYPH[task.source] : \" \";\n\tconst styledSource = task.source ? theme.fg(\"dim\", sourceGlyph) : \" \";\n\n\t// Right-pad the id to the shared column width so titles line up across rows even\n\t// when ids differ in digit count (#1 vs #10). Padding is plain spaces inside the\n\t// dim styling, so it adds no visible color.\n\tconst idLabel = `#${task.id}`.padEnd(idColWidth);\n\tconst title = task.title;\n\t// The id recedes (dim); 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\tconst styledId = theme.fg(\"dim\", idLabel);\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 \"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 audit stamp (tokens + elapsed); the\n\t// active row reads `running…`, pending rows read `queued`.\n\tlet rightPlain = \"\";\n\tlet rightStyled = \"\";\n\tif (task.status === \"done\" || task.status === \"failed\") {\n\t\tconst parts: string[] = [];\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\tconst elapsed = formatDuration(taskElapsedSecs(task));\n\t\tif (tokenText) {\n\t\t\tparts.push(tokenText, elapsed);\n\t\t\trightStyled = theme.fg(\"muted\", tokenText) + theme.fg(\"dim\", ` · ${elapsed}`);\n\t\t} else {\n\t\t\tparts.push(elapsed);\n\t\t\trightStyled = theme.fg(\"dim\", elapsed);\n\t\t}\n\t\trightPlain = parts.join(\" · \");\n\t} else if (task.status === \"in_progress\") {\n\t\trightPlain = \"running…\";\n\t\trightStyled = theme.fg(\"warning\", rightPlain);\n\t} else if (task.status === \"pending\") {\n\t\trightPlain = \"queued\";\n\t\trightStyled = theme.fg(\"dim\", rightPlain);\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 = `⚠ ${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\tconst left = truncateToWidth(`${icon} ${styledSource} ${styledId} ${styledTitle}`, leftWidth, \"…\");\n\n\tif (!rightPlain) return left;\n\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 ledger header tops the list: a state stamp + deterministic progress bar +\n * done/total count on the left, the per-turn token/elapsed/cost delta on the right.\n * - Shows all tasks with all statuses (pending / in_progress / done / failed).\n * The active row animates a braille spinner; pending rows read `queued`.\n * - A single-cell source glyph (⚙ subagent / ⧉ MCP) sits before the id so the two\n * kinds of background work are distinguishable. The subagent *mode* tag (e.g.\n * \"[explore]\") is still intentionally NOT shown — the pane stays tag-free.\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.\n */\nexport class TaskPanelComponent implements Component {\n\tprivate readonly ui: TUI | null;\n\tprivate frame = 0;\n\tprivate animationTimer: ReturnType<typeof setInterval> | null = null;\n\n\tconstructor(ui?: TUI) {\n\t\tthis.ui = ui ?? null;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached rendering state.\n\t}\n\n\t/** Run the spinner timer only while a task is active, ticking re-renders. */\n\tprivate ensureAnimation(active: boolean): void {\n\t\tif (active && this.ui && !this.animationTimer) {\n\t\t\tthis.animationTimer = setInterval(() => {\n\t\t\t\tthis.frame = (this.frame + 1) % SPINNER_FRAMES.length;\n\t\t\t\tthis.ui?.requestRender();\n\t\t\t}, SPINNER_INTERVAL_MS);\n\t\t\tthis.animationTimer.unref?.();\n\t\t} else if (!active && this.animationTimer) {\n\t\t\tclearInterval(this.animationTimer);\n\t\t\tthis.animationTimer = null;\n\t\t\tthis.frame = 0;\n\t\t}\n\t}\n\n\t/** Stop the spinner timer. Call on teardown. */\n\tdispose(): void {\n\t\tif (this.animationTimer) {\n\t\t\tclearInterval(this.animationTimer);\n\t\t\tthis.animationTimer = null;\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tconst tasks = taskStore.list();\n\t\tif (tasks.length === 0) {\n\t\t\tthis.ensureAnimation(false);\n\t\t\treturn [];\n\t\t}\n\n\t\tconst hasActive = tasks.some((t) => t.status === \"in_progress\");\n\t\tthis.ensureAnimation(hasActive);\n\n\t\tconst state = panelState(tasks);\n\t\tconst totalSecs = tasks.reduce((sum, t) => sum + taskElapsedSecs(t), 0);\n\t\tconst railColor = STATE_PRESENTATION[state].color;\n\t\tconst gutter = `${theme.fg(railColor, RAIL)} `;\n\t\tconst inner = Math.max(0, width - visibleWidth(RAIL) - 1);\n\n\t\t// Width of the id column, sized to the widest id on screen, so every title\n\t\t// starts at the same column regardless of digit count (#1 vs #10 vs #100).\n\t\tconst idColWidth = tasks.reduce((max, t) => Math.max(max, `#${t.id}`.length), 0);\n\n\t\tconst lines: string[] = [gutter + formatHeader(tasks, inner, state, totalSecs)];\n\t\tfor (const task of tasks) {\n\t\t\tlines.push(gutter + formatTaskLine(task, inner, this.frame, idColWidth));\n\t\t}\n\t\treturn lines;\n\t}\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "0.2.34",
4
+ "version": "0.2.36",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "0.2.34",
4
+ "version": "0.2.36",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-sandbox",
3
3
  "private": true,
4
- "version": "0.2.34",
4
+ "version": "0.2.36",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-with-deps",
3
3
  "private": true,
4
- "version": "0.2.34",
4
+ "version": "0.2.36",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-agent",
3
- "version": "0.4.36",
3
+ "version": "0.4.38",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "hoocodeConfig": {
@@ -44,9 +44,9 @@
44
44
  "prepublishOnly": "npm run clean && npm run build"
45
45
  },
46
46
  "dependencies": {
47
- "@kolisachint/hoocode-agent-core": "^0.4.36",
48
- "@kolisachint/hoocode-ai": "^0.4.36",
49
- "@kolisachint/hoocode-tui": "^0.4.36",
47
+ "@kolisachint/hoocode-agent-core": "^0.4.38",
48
+ "@kolisachint/hoocode-ai": "^0.4.38",
49
+ "@kolisachint/hoocode-tui": "^0.4.38",
50
50
  "@silvia-odwyer/photon-node": "^0.3.4",
51
51
  "chalk": "^5.5.0",
52
52
  "cli-highlight": "^2.1.11",