@kolisachint/hoocode-agent 0.4.157 → 0.4.158

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 (41) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/core/agent-session-stats.d.ts +17 -1
  3. package/dist/core/agent-session-stats.d.ts.map +1 -1
  4. package/dist/core/agent-session-stats.js +20 -0
  5. package/dist/core/agent-session-stats.js.map +1 -1
  6. package/dist/core/settings-defaults.d.ts +1 -0
  7. package/dist/core/settings-defaults.d.ts.map +1 -1
  8. package/dist/core/settings-defaults.js +2 -1
  9. package/dist/core/settings-defaults.js.map +1 -1
  10. package/dist/core/settings-manager.d.ts +2 -0
  11. package/dist/core/settings-manager.d.ts.map +1 -1
  12. package/dist/core/settings-manager.js +8 -0
  13. package/dist/core/settings-manager.js.map +1 -1
  14. package/dist/core/settings-types.d.ts +1 -0
  15. package/dist/core/settings-types.d.ts.map +1 -1
  16. package/dist/core/settings-types.js.map +1 -1
  17. package/dist/core/tools/todo.d.ts +24 -0
  18. package/dist/core/tools/todo.d.ts.map +1 -1
  19. package/dist/core/tools/todo.js +50 -9
  20. package/dist/core/tools/todo.js.map +1 -1
  21. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  22. package/dist/modes/interactive/components/footer.js +6 -15
  23. package/dist/modes/interactive/components/footer.js.map +1 -1
  24. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  25. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  26. package/dist/modes/interactive/components/settings-selector.js +15 -2
  27. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  28. package/dist/modes/interactive/components/task-panel.d.ts +20 -12
  29. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
  30. package/dist/modes/interactive/components/task-panel.js +170 -224
  31. package/dist/modes/interactive/components/task-panel.js.map +1 -1
  32. package/dist/modes/interactive/interactive-mode.d.ts +42 -7
  33. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  34. package/dist/modes/interactive/interactive-mode.js +124 -19
  35. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  36. package/docs/settings.md +2 -1
  37. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  38. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  39. package/examples/extensions/sandbox/package.json +1 -1
  40. package/examples/extensions/with-deps/package.json +1 -1
  41. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"todo.d.ts","sourceRoot":"","sources":["../../../src/core/tools/todo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAoCzE,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CAClB;AAoCD,qFAAqF;AACrF,wBAAgB,6BAA6B,IAAI,cAAc,CAuG9D","sourcesContent":["/**\n * TodoWrite tool: let the main agent maintain a visible todo list for the\n * current task.\n *\n * hoocode already has all the infrastructure this needs — the task store models\n * `{ title, status }` items with a per-turn lifecycle, and the TUI task panel\n * renders them. The only missing piece was a tool the model can call; this is\n * that thin adapter over `taskStore`.\n *\n * Semantics mirror Claude Code's TodoWrite: each call sends the FULL list and\n * REPLACES the previous one. Because the store is incremental (numeric ids), we\n * reconcile the incoming list against the existing main-agent tasks by position:\n * update items that are still there, create new ones, and drop the tail that was\n * removed. Reconciling (rather than clear-and-recreate) keeps ids stable so the\n * panel does not flicker and in-progress rows stay put.\n *\n * It is an optional, opt-in tool (enabled via the `enableTodoWrite` setting) and\n * is never registered inside a spawned subagent, so a subagent's todos cannot\n * leak into the parent's \"main\" task group.\n */\n\nimport { type Static, Type } from \"typebox\";\nimport { TODO_WRITE_TOOL_NAME } from \"../agent-frontmatter.js\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport { type Task, type TaskStatus, taskStore } from \"../task-store.js\";\n\nconst todoStatusSchema = Type.Union([Type.Literal(\"pending\"), Type.Literal(\"in_progress\"), Type.Literal(\"completed\")], {\n\tdescription: \"pending = not started, in_progress = actively being worked on, completed = finished.\",\n});\n\nconst todoItemSchema = Type.Object(\n\t{\n\t\tcontent: Type.String({\n\t\t\tdescription: \"The task, in imperative form (e.g. 'Add tests for the parser').\",\n\t\t}),\n\t\tstatus: todoStatusSchema,\n\t\tactiveForm: Type.Optional(\n\t\t\tType.String({\n\t\t\t\tdescription:\n\t\t\t\t\t\"Optional present-tense form shown while the item is in_progress (e.g. 'Adding tests for the parser').\",\n\t\t\t}),\n\t\t),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst todoWriteParams = Type.Object(\n\t{\n\t\ttodos: Type.Array(todoItemSchema, {\n\t\t\tdescription:\n\t\t\t\t\"The complete todo list. This REPLACES the previous list on every call, so always send every item with its current status — omitting an item removes it.\",\n\t\t}),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype TodoWriteParams = Static<typeof todoWriteParams>;\ntype IncomingStatus = TodoWriteParams[\"todos\"][number][\"status\"];\n\nexport interface TodoWriteDetails {\n\ttotal: number;\n\tpending: number;\n\tinProgress: number;\n\tcompleted: number;\n}\n\n/** Map the model-facing status vocabulary onto the task store's. */\nfunction toTaskStatus(status: IncomingStatus): TaskStatus {\n\treturn status === \"completed\" ? \"done\" : status;\n}\n\nconst STATUS_GLYPH: Record<TaskStatus, string> = {\n\tpending: \"[ ]\",\n\tin_progress: \"[~]\",\n\tdone: \"[x]\",\n\tfailed: \"[!]\",\n\t// TodoWrite never produces cancelled items; present for Record exhaustiveness.\n\tcancelled: \"[-]\",\n};\n\n/** Title to display: the active-form while in progress, otherwise the content. */\nfunction displayTitle(item: TodoWriteParams[\"todos\"][number]): string {\n\tif (item.status === \"in_progress\" && item.activeForm?.trim()) return item.activeForm.trim();\n\treturn item.content.trim();\n}\n\n/**\n * Current main-agent tasks, in stable creation order. Filters to root tasks the\n * main agent itself owns: no `source` (excludes \"subagent\"/MCP rows), no `agent`\n * (excludes delegated rows), and no `parentTaskId` (excludes merged child trees).\n * `taskOwnerId()` would fold MCP-sourced and delegated rows under \"main\", so\n * reconciling against it could overwrite or drop those rows when the TodoWrite\n * list is shorter than the combined count.\n */\nfunction mainTasks(): Task[] {\n\treturn taskStore\n\t\t.list()\n\t\t.filter((t) => t.source === undefined && t.agent === undefined && t.parentTaskId === undefined);\n}\n\n/** Create the TodoWrite tool definition. Registered as a customTool when enabled. */\nexport function createTodoWriteToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof todoWriteParams, TodoWriteDetails>({\n\t\tname: TODO_WRITE_TOOL_NAME,\n\t\tlabel: TODO_WRITE_TOOL_NAME,\n\t\tdescription: [\n\t\t\t\"Maintain a structured todo list for the current task, shown live in the task panel.\",\n\t\t\t\"Use it PROACTIVELY: at the start of any multi-step or non-trivial task, write the full plan as todos before you begin, then keep it current as you work.\",\n\t\t\t\"Mark exactly ONE item in_progress at a time, and flip an item to completed immediately after finishing it — do not batch completions or leave finished work marked in_progress.\",\n\t\t\t\"Each call sends the FULL list and REPLACES the previous one — always include every item with its current status; omitting an item removes it.\",\n\t\t\t\"Skip it only for trivial, single-step tasks where a list adds no value. When in doubt on multi-step work, use it — it keeps you from losing track of steps.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet:\n\t\t\t\"Plan and track multi-step work as a live todo list (use proactively; replaces the whole list each call)\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use TodoWrite proactively for any multi-step or non-trivial task: write the plan as todos up front, keep exactly one item in_progress, and mark items completed immediately as you finish them.\",\n\t\t\t\"TodoWrite replaces the entire list each call — always send all items with their current status.\",\n\t\t\t\"Skip TodoWrite for trivial single-step tasks where a checklist adds no value.\",\n\t\t],\n\t\tparameters: todoWriteParams,\n\t\tasync execute(_toolCallId, params: TodoWriteParams) {\n\t\t\tconst todos = params.todos ?? [];\n\t\t\tconst existing = mainTasks();\n\n\t\t\t// Reconcile by item identity first, position second, batched so the panel\n\t\t\t// renders once. Each task stores its item's canonical `content`\n\t\t\t// (todoContent) — the display title flips between content and activeForm\n\t\t\t// with status, so it can't identify an item. Matching by content keeps a\n\t\t\t// task's id pinned to the same plan item when the list is reordered or\n\t\t\t// shrunk; a purely positional reconcile re-labeled the surviving slots,\n\t\t\t// which silently re-pointed the subagent runs linked to those ids\n\t\t\t// (linkedTaskId) at the wrong plan items. Unmatched incoming items then\n\t\t\t// consume the leftover slots in order (a rename keeps its id and its\n\t\t\t// linked runs); any remaining leftovers were removed from the plan.\n\t\t\ttaskStore.batch(() => {\n\t\t\t\tconst content = (item: TodoWriteParams[\"todos\"][number]) => item.content.trim();\n\t\t\t\tconst matchedExisting = new Set<number>();\n\t\t\t\tconst assigned = new Array<Task | undefined>(todos.length);\n\t\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\t\tconst idx = existing.findIndex(\n\t\t\t\t\t\t(t, j) => !matchedExisting.has(j) && (t.todoContent ?? t.title) === content(todos[i]!),\n\t\t\t\t\t);\n\t\t\t\t\tif (idx !== -1) {\n\t\t\t\t\t\tmatchedExisting.add(idx);\n\t\t\t\t\t\tassigned[i] = existing[idx];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst leftovers = existing.filter((_, j) => !matchedExisting.has(j));\n\t\t\t\tlet nextLeftover = 0;\n\t\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\t\tif (!assigned[i]) assigned[i] = leftovers[nextLeftover++];\n\t\t\t\t}\n\n\t\t\t\tconst finalIds: number[] = [];\n\t\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\t\tconst item = todos[i]!;\n\t\t\t\t\tconst status = toTaskStatus(item.status);\n\t\t\t\t\tconst title = displayTitle(item);\n\t\t\t\t\tconst current = assigned[i];\n\t\t\t\t\tif (current) {\n\t\t\t\t\t\ttaskStore.update(current.id, { title, status, todoContent: content(item) });\n\t\t\t\t\t\tfinalIds.push(current.id);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst created = taskStore.create(title);\n\t\t\t\t\t\ttaskStore.update(created.id, { status, todoContent: content(item) });\n\t\t\t\t\t\tfinalIds.push(created.id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (let j = nextLeftover; j < leftovers.length; j++) {\n\t\t\t\t\ttaskStore.remove(leftovers[j]!.id);\n\t\t\t\t}\n\t\t\t\t// Identity matching keeps ids, but the panel must still show the plan\n\t\t\t\t// in the list's order — permute the plan tasks into it.\n\t\t\t\ttaskStore.arrange(finalIds);\n\t\t\t});\n\n\t\t\tconst counts = todos.reduce(\n\t\t\t\t(acc, t) => {\n\t\t\t\t\tif (t.status === \"in_progress\") acc.inProgress++;\n\t\t\t\t\telse if (t.status === \"completed\") acc.completed++;\n\t\t\t\t\telse acc.pending++;\n\t\t\t\t\treturn acc;\n\t\t\t\t},\n\t\t\t\t{ pending: 0, inProgress: 0, completed: 0 },\n\t\t\t);\n\n\t\t\tconst lines = todos.map((t) => `${STATUS_GLYPH[toTaskStatus(t.status)]} ${displayTitle(t)}`);\n\t\t\tconst header =\n\t\t\t\ttodos.length === 0\n\t\t\t\t\t? \"Todo list cleared.\"\n\t\t\t\t\t: `Todos updated (${counts.inProgress} in progress, ${counts.pending} pending, ${counts.completed} completed):`;\n\t\t\tconst text = todos.length === 0 ? header : `${header}\\n${lines.join(\"\\n\")}`;\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\tdetails: {\n\t\t\t\t\ttotal: todos.length,\n\t\t\t\t\tpending: counts.pending,\n\t\t\t\t\tinProgress: counts.inProgress,\n\t\t\t\t\tcompleted: counts.completed,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"todo.d.ts","sourceRoot":"","sources":["../../../src/core/tools/todo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAa,KAAK,UAAU,EAAa,MAAM,kBAAkB,CAAC;AAmCzE,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CAClB;AA2CD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,WAAW,CAAC,GAAG,MAAM,CAWlG;AAED,qFAAqF;AACrF,wBAAgB,6BAA6B,IAAI,cAAc,CAuG9D","sourcesContent":["/**\n * TodoWrite tool: let the main agent maintain a visible todo list for the\n * current task.\n *\n * hoocode already has all the infrastructure this needs — the task store models\n * `{ title, status }` items with a per-turn lifecycle, and the TUI task panel\n * renders them. The only missing piece was a tool the model can call; this is\n * that thin adapter over `taskStore`.\n *\n * Semantics mirror Claude Code's TodoWrite: each call sends the FULL list and\n * REPLACES the previous one. Because the store is incremental (numeric ids), we\n * reconcile the incoming list against the existing main-agent tasks by position:\n * update items that are still there, create new ones, and drop the tail that was\n * removed. Reconciling (rather than clear-and-recreate) keeps ids stable so the\n * panel does not flicker and in-progress rows stay put.\n *\n * It is an optional, opt-in tool (enabled via the `enableTodoWrite` setting) and\n * is never registered inside a spawned subagent, so a subagent's todos cannot\n * leak into the parent's \"main\" task group.\n */\n\nimport { type Static, Type } from \"typebox\";\nimport { TODO_WRITE_TOOL_NAME } from \"../agent-frontmatter.js\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport { type Task, type TaskStatus, taskStore } from \"../task-store.js\";\n\nconst todoStatusSchema = Type.Union([Type.Literal(\"pending\"), Type.Literal(\"in_progress\"), Type.Literal(\"completed\")], {\n\tdescription: \"pending = not started, in_progress = actively being worked on, completed = finished.\",\n});\n\nconst todoItemSchema = Type.Object(\n\t{\n\t\tcontent: Type.String({\n\t\t\tdescription: \"The task, in imperative form (e.g. 'Add tests for the parser').\",\n\t\t}),\n\t\tstatus: todoStatusSchema,\n\t\tactiveForm: Type.Optional(\n\t\t\tType.String({\n\t\t\t\tdescription:\n\t\t\t\t\t\"Optional present-tense form shown while the item is in_progress (e.g. 'Adding tests for the parser').\",\n\t\t\t}),\n\t\t),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst todoWriteParams = Type.Object(\n\t{\n\t\ttodos: Type.Array(todoItemSchema, {\n\t\t\tdescription:\n\t\t\t\t\"The complete todo list. This REPLACES the previous list on every call, so always send every item with its current status — omitting an item removes it.\",\n\t\t}),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype TodoWriteParams = Static<typeof todoWriteParams>;\ntype IncomingStatus = TodoWriteParams[\"todos\"][number][\"status\"];\n\nexport interface TodoWriteDetails {\n\ttotal: number;\n\tpending: number;\n\tinProgress: number;\n\tcompleted: number;\n}\n\n/** Map the model-facing status vocabulary onto the task store's. */\nfunction toTaskStatus(status: IncomingStatus): TaskStatus {\n\treturn status === \"completed\" ? \"done\" : status;\n}\n\nconst STATUS_GLYPH: Record<TaskStatus, string> = {\n\tpending: \"[ ]\",\n\tin_progress: \"[~]\",\n\tdone: \"[x]\",\n\tfailed: \"[!]\",\n\t// TodoWrite never produces cancelled items; present for Record exhaustiveness.\n\tcancelled: \"[-]\",\n};\n\n/** Title to display: the active-form while in progress, otherwise the content. */\nfunction displayTitle(item: TodoWriteParams[\"todos\"][number]): string {\n\tif (item.status === \"in_progress\" && item.activeForm?.trim()) return item.activeForm.trim();\n\treturn item.content.trim();\n}\n\n/**\n * A root task the main agent itself owns, i.e. a TodoWrite plan item: no\n * `source` (excludes \"subagent\"/MCP rows), no `agent` (excludes delegated rows),\n * and no `parentTaskId` (excludes merged child trees). `taskOwnerId()` would\n * fold MCP-sourced and delegated rows under \"main\", so reconciling against it\n * could overwrite or drop those rows when the TodoWrite list is shorter than the\n * combined count.\n */\nfunction isMainPlanTask(task: Task): boolean {\n\treturn task.source === undefined && task.agent === undefined && task.parentTaskId === undefined;\n}\n\n/** Current main-agent plan tasks, in stable creation order. */\nfunction mainTasks(): Task[] {\n\treturn taskStore.list().filter(isMainPlanTask);\n}\n\nfunction isActive(task: Task): boolean {\n\treturn task.status === \"pending\" || task.status === \"in_progress\";\n}\n\n/**\n * Settle plan items the model left pinned at `in_progress` when a request ends.\n *\n * TodoWrite is bookkeeping the model performs by hand, and even strong models\n * routinely drop the final call that flips the last item to completed. Nothing\n * else writes main-plan rows, so without this the panel would keep claiming the\n * work is in flight until the next user message triggers `taskStore.reset()`.\n * The request is over, so the row is wrong either way — settle it to the honest\n * outcome instead of leaving it lying.\n *\n * Scope is deliberately narrow:\n * - Only `in_progress` main-plan items. `pending` rows are left alone: \"never\n * started\" is already an accurate reading of an item the model skipped.\n * - Only main-plan items. Subagent- and MCP-sourced rows settle through their\n * own lifecycles (subagent.ts, mcp-loader.ts) and must not be second-guessed\n * here.\n * - Nothing settles while any delegated task is still pending/in_progress: a\n * subagent outliving the parent's agent_end is still working the plan, so its\n * plan item is genuinely in progress.\n *\n * Returns the number of tasks settled.\n */\nexport function settleDanglingMainTasks(outcome: Extract<TaskStatus, \"done\" | \"cancelled\">): number {\n\tconst all = taskStore.list();\n\tif (all.some((t) => !isMainPlanTask(t) && isActive(t))) return 0;\n\tconst dangling = all.filter((t) => isMainPlanTask(t) && t.status === \"in_progress\");\n\tif (dangling.length === 0) return 0;\n\ttaskStore.batch(() => {\n\t\tfor (const task of dangling) {\n\t\t\ttaskStore.update(task.id, { status: outcome });\n\t\t}\n\t});\n\treturn dangling.length;\n}\n\n/** Create the TodoWrite tool definition. Registered as a customTool when enabled. */\nexport function createTodoWriteToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof todoWriteParams, TodoWriteDetails>({\n\t\tname: TODO_WRITE_TOOL_NAME,\n\t\tlabel: TODO_WRITE_TOOL_NAME,\n\t\tdescription: [\n\t\t\t\"Maintain a structured todo list for the current task, shown live in the task panel.\",\n\t\t\t\"Use it PROACTIVELY: at the start of any multi-step or non-trivial task, write the full plan as todos before you begin, then keep it current as you work.\",\n\t\t\t\"Mark exactly ONE item in_progress at a time, and flip an item to completed immediately after finishing it — do not batch completions or leave finished work marked in_progress.\",\n\t\t\t\"Each call sends the FULL list and REPLACES the previous one — always include every item with its current status; omitting an item removes it.\",\n\t\t\t\"Skip it only for trivial, single-step tasks where a list adds no value. When in doubt on multi-step work, use it — it keeps you from losing track of steps.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet:\n\t\t\t\"Plan and track multi-step work as a live todo list (use proactively; replaces the whole list each call)\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use TodoWrite proactively for any multi-step or non-trivial task: write the plan as todos up front, keep exactly one item in_progress, and mark items completed immediately as you finish them.\",\n\t\t\t\"TodoWrite replaces the entire list each call — always send all items with their current status.\",\n\t\t\t\"Skip TodoWrite for trivial single-step tasks where a checklist adds no value.\",\n\t\t],\n\t\tparameters: todoWriteParams,\n\t\tasync execute(_toolCallId, params: TodoWriteParams) {\n\t\t\tconst todos = params.todos ?? [];\n\t\t\tconst existing = mainTasks();\n\n\t\t\t// Reconcile by item identity first, position second, batched so the panel\n\t\t\t// renders once. Each task stores its item's canonical `content`\n\t\t\t// (todoContent) — the display title flips between content and activeForm\n\t\t\t// with status, so it can't identify an item. Matching by content keeps a\n\t\t\t// task's id pinned to the same plan item when the list is reordered or\n\t\t\t// shrunk; a purely positional reconcile re-labeled the surviving slots,\n\t\t\t// which silently re-pointed the subagent runs linked to those ids\n\t\t\t// (linkedTaskId) at the wrong plan items. Unmatched incoming items then\n\t\t\t// consume the leftover slots in order (a rename keeps its id and its\n\t\t\t// linked runs); any remaining leftovers were removed from the plan.\n\t\t\ttaskStore.batch(() => {\n\t\t\t\tconst content = (item: TodoWriteParams[\"todos\"][number]) => item.content.trim();\n\t\t\t\tconst matchedExisting = new Set<number>();\n\t\t\t\tconst assigned = new Array<Task | undefined>(todos.length);\n\t\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\t\tconst idx = existing.findIndex(\n\t\t\t\t\t\t(t, j) => !matchedExisting.has(j) && (t.todoContent ?? t.title) === content(todos[i]!),\n\t\t\t\t\t);\n\t\t\t\t\tif (idx !== -1) {\n\t\t\t\t\t\tmatchedExisting.add(idx);\n\t\t\t\t\t\tassigned[i] = existing[idx];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst leftovers = existing.filter((_, j) => !matchedExisting.has(j));\n\t\t\t\tlet nextLeftover = 0;\n\t\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\t\tif (!assigned[i]) assigned[i] = leftovers[nextLeftover++];\n\t\t\t\t}\n\n\t\t\t\tconst finalIds: number[] = [];\n\t\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\t\tconst item = todos[i]!;\n\t\t\t\t\tconst status = toTaskStatus(item.status);\n\t\t\t\t\tconst title = displayTitle(item);\n\t\t\t\t\tconst current = assigned[i];\n\t\t\t\t\tif (current) {\n\t\t\t\t\t\ttaskStore.update(current.id, { title, status, todoContent: content(item) });\n\t\t\t\t\t\tfinalIds.push(current.id);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst created = taskStore.create(title);\n\t\t\t\t\t\ttaskStore.update(created.id, { status, todoContent: content(item) });\n\t\t\t\t\t\tfinalIds.push(created.id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (let j = nextLeftover; j < leftovers.length; j++) {\n\t\t\t\t\ttaskStore.remove(leftovers[j]!.id);\n\t\t\t\t}\n\t\t\t\t// Identity matching keeps ids, but the panel must still show the plan\n\t\t\t\t// in the list's order — permute the plan tasks into it.\n\t\t\t\ttaskStore.arrange(finalIds);\n\t\t\t});\n\n\t\t\tconst counts = todos.reduce(\n\t\t\t\t(acc, t) => {\n\t\t\t\t\tif (t.status === \"in_progress\") acc.inProgress++;\n\t\t\t\t\telse if (t.status === \"completed\") acc.completed++;\n\t\t\t\t\telse acc.pending++;\n\t\t\t\t\treturn acc;\n\t\t\t\t},\n\t\t\t\t{ pending: 0, inProgress: 0, completed: 0 },\n\t\t\t);\n\n\t\t\tconst lines = todos.map((t) => `${STATUS_GLYPH[toTaskStatus(t.status)]} ${displayTitle(t)}`);\n\t\t\tconst header =\n\t\t\t\ttodos.length === 0\n\t\t\t\t\t? \"Todo list cleared.\"\n\t\t\t\t\t: `Todos updated (${counts.inProgress} in progress, ${counts.pending} pending, ${counts.completed} completed):`;\n\t\t\tconst text = todos.length === 0 ? header : `${header}\\n${lines.join(\"\\n\")}`;\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\tdetails: {\n\t\t\t\t\ttotal: todos.length,\n\t\t\t\t\tpending: counts.pending,\n\t\t\t\t\tinProgress: counts.inProgress,\n\t\t\t\t\tcompleted: counts.completed,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
@@ -58,17 +58,58 @@ function displayTitle(item) {
58
58
  return item.content.trim();
59
59
  }
60
60
  /**
61
- * Current main-agent tasks, in stable creation order. Filters to root tasks the
62
- * main agent itself owns: no `source` (excludes "subagent"/MCP rows), no `agent`
63
- * (excludes delegated rows), and no `parentTaskId` (excludes merged child trees).
64
- * `taskOwnerId()` would fold MCP-sourced and delegated rows under "main", so
65
- * reconciling against it could overwrite or drop those rows when the TodoWrite
66
- * list is shorter than the combined count.
61
+ * A root task the main agent itself owns, i.e. a TodoWrite plan item: no
62
+ * `source` (excludes "subagent"/MCP rows), no `agent` (excludes delegated rows),
63
+ * and no `parentTaskId` (excludes merged child trees). `taskOwnerId()` would
64
+ * fold MCP-sourced and delegated rows under "main", so reconciling against it
65
+ * could overwrite or drop those rows when the TodoWrite list is shorter than the
66
+ * combined count.
67
67
  */
68
+ function isMainPlanTask(task) {
69
+ return task.source === undefined && task.agent === undefined && task.parentTaskId === undefined;
70
+ }
71
+ /** Current main-agent plan tasks, in stable creation order. */
68
72
  function mainTasks() {
69
- return taskStore
70
- .list()
71
- .filter((t) => t.source === undefined && t.agent === undefined && t.parentTaskId === undefined);
73
+ return taskStore.list().filter(isMainPlanTask);
74
+ }
75
+ function isActive(task) {
76
+ return task.status === "pending" || task.status === "in_progress";
77
+ }
78
+ /**
79
+ * Settle plan items the model left pinned at `in_progress` when a request ends.
80
+ *
81
+ * TodoWrite is bookkeeping the model performs by hand, and even strong models
82
+ * routinely drop the final call that flips the last item to completed. Nothing
83
+ * else writes main-plan rows, so without this the panel would keep claiming the
84
+ * work is in flight until the next user message triggers `taskStore.reset()`.
85
+ * The request is over, so the row is wrong either way — settle it to the honest
86
+ * outcome instead of leaving it lying.
87
+ *
88
+ * Scope is deliberately narrow:
89
+ * - Only `in_progress` main-plan items. `pending` rows are left alone: "never
90
+ * started" is already an accurate reading of an item the model skipped.
91
+ * - Only main-plan items. Subagent- and MCP-sourced rows settle through their
92
+ * own lifecycles (subagent.ts, mcp-loader.ts) and must not be second-guessed
93
+ * here.
94
+ * - Nothing settles while any delegated task is still pending/in_progress: a
95
+ * subagent outliving the parent's agent_end is still working the plan, so its
96
+ * plan item is genuinely in progress.
97
+ *
98
+ * Returns the number of tasks settled.
99
+ */
100
+ export function settleDanglingMainTasks(outcome) {
101
+ const all = taskStore.list();
102
+ if (all.some((t) => !isMainPlanTask(t) && isActive(t)))
103
+ return 0;
104
+ const dangling = all.filter((t) => isMainPlanTask(t) && t.status === "in_progress");
105
+ if (dangling.length === 0)
106
+ return 0;
107
+ taskStore.batch(() => {
108
+ for (const task of dangling) {
109
+ taskStore.update(task.id, { status: outcome });
110
+ }
111
+ });
112
+ return dangling.length;
72
113
  }
73
114
  /** Create the TodoWrite tool definition. Registered as a customTool when enabled. */
74
115
  export function createTodoWriteToolDefinition() {
@@ -1 +1 @@
1
- {"version":3,"file":"todo.js","sourceRoot":"","sources":["../../../src/core/tools/todo.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * TodoWrite tool: let the main agent maintain a visible todo list for the\n * current task.\n *\n * hoocode already has all the infrastructure this needs — the task store models\n * `{ title, status }` items with a per-turn lifecycle, and the TUI task panel\n * renders them. The only missing piece was a tool the model can call; this is\n * that thin adapter over `taskStore`.\n *\n * Semantics mirror Claude Code's TodoWrite: each call sends the FULL list and\n * REPLACES the previous one. Because the store is incremental (numeric ids), we\n * reconcile the incoming list against the existing main-agent tasks by position:\n * update items that are still there, create new ones, and drop the tail that was\n * removed. Reconciling (rather than clear-and-recreate) keeps ids stable so the\n * panel does not flicker and in-progress rows stay put.\n *\n * It is an optional, opt-in tool (enabled via the `enableTodoWrite` setting) and\n * is never registered inside a spawned subagent, so a subagent's todos cannot\n * leak into the parent's \"main\" task group.\n */\n\nimport { type Static, Type } from \"typebox\";\nimport { TODO_WRITE_TOOL_NAME } from \"../agent-frontmatter.js\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport { type Task, type TaskStatus, taskStore } from \"../task-store.js\";\n\nconst todoStatusSchema = Type.Union([Type.Literal(\"pending\"), Type.Literal(\"in_progress\"), Type.Literal(\"completed\")], {\n\tdescription: \"pending = not started, in_progress = actively being worked on, completed = finished.\",\n});\n\nconst todoItemSchema = Type.Object(\n\t{\n\t\tcontent: Type.String({\n\t\t\tdescription: \"The task, in imperative form (e.g. 'Add tests for the parser').\",\n\t\t}),\n\t\tstatus: todoStatusSchema,\n\t\tactiveForm: Type.Optional(\n\t\t\tType.String({\n\t\t\t\tdescription:\n\t\t\t\t\t\"Optional present-tense form shown while the item is in_progress (e.g. 'Adding tests for the parser').\",\n\t\t\t}),\n\t\t),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst todoWriteParams = Type.Object(\n\t{\n\t\ttodos: Type.Array(todoItemSchema, {\n\t\t\tdescription:\n\t\t\t\t\"The complete todo list. This REPLACES the previous list on every call, so always send every item with its current status — omitting an item removes it.\",\n\t\t}),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype TodoWriteParams = Static<typeof todoWriteParams>;\ntype IncomingStatus = TodoWriteParams[\"todos\"][number][\"status\"];\n\nexport interface TodoWriteDetails {\n\ttotal: number;\n\tpending: number;\n\tinProgress: number;\n\tcompleted: number;\n}\n\n/** Map the model-facing status vocabulary onto the task store's. */\nfunction toTaskStatus(status: IncomingStatus): TaskStatus {\n\treturn status === \"completed\" ? \"done\" : status;\n}\n\nconst STATUS_GLYPH: Record<TaskStatus, string> = {\n\tpending: \"[ ]\",\n\tin_progress: \"[~]\",\n\tdone: \"[x]\",\n\tfailed: \"[!]\",\n\t// TodoWrite never produces cancelled items; present for Record exhaustiveness.\n\tcancelled: \"[-]\",\n};\n\n/** Title to display: the active-form while in progress, otherwise the content. */\nfunction displayTitle(item: TodoWriteParams[\"todos\"][number]): string {\n\tif (item.status === \"in_progress\" && item.activeForm?.trim()) return item.activeForm.trim();\n\treturn item.content.trim();\n}\n\n/**\n * Current main-agent tasks, in stable creation order. Filters to root tasks the\n * main agent itself owns: no `source` (excludes \"subagent\"/MCP rows), no `agent`\n * (excludes delegated rows), and no `parentTaskId` (excludes merged child trees).\n * `taskOwnerId()` would fold MCP-sourced and delegated rows under \"main\", so\n * reconciling against it could overwrite or drop those rows when the TodoWrite\n * list is shorter than the combined count.\n */\nfunction mainTasks(): Task[] {\n\treturn taskStore\n\t\t.list()\n\t\t.filter((t) => t.source === undefined && t.agent === undefined && t.parentTaskId === undefined);\n}\n\n/** Create the TodoWrite tool definition. Registered as a customTool when enabled. */\nexport function createTodoWriteToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof todoWriteParams, TodoWriteDetails>({\n\t\tname: TODO_WRITE_TOOL_NAME,\n\t\tlabel: TODO_WRITE_TOOL_NAME,\n\t\tdescription: [\n\t\t\t\"Maintain a structured todo list for the current task, shown live in the task panel.\",\n\t\t\t\"Use it PROACTIVELY: at the start of any multi-step or non-trivial task, write the full plan as todos before you begin, then keep it current as you work.\",\n\t\t\t\"Mark exactly ONE item in_progress at a time, and flip an item to completed immediately after finishing it — do not batch completions or leave finished work marked in_progress.\",\n\t\t\t\"Each call sends the FULL list and REPLACES the previous one — always include every item with its current status; omitting an item removes it.\",\n\t\t\t\"Skip it only for trivial, single-step tasks where a list adds no value. When in doubt on multi-step work, use it — it keeps you from losing track of steps.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet:\n\t\t\t\"Plan and track multi-step work as a live todo list (use proactively; replaces the whole list each call)\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use TodoWrite proactively for any multi-step or non-trivial task: write the plan as todos up front, keep exactly one item in_progress, and mark items completed immediately as you finish them.\",\n\t\t\t\"TodoWrite replaces the entire list each call — always send all items with their current status.\",\n\t\t\t\"Skip TodoWrite for trivial single-step tasks where a checklist adds no value.\",\n\t\t],\n\t\tparameters: todoWriteParams,\n\t\tasync execute(_toolCallId, params: TodoWriteParams) {\n\t\t\tconst todos = params.todos ?? [];\n\t\t\tconst existing = mainTasks();\n\n\t\t\t// Reconcile by item identity first, position second, batched so the panel\n\t\t\t// renders once. Each task stores its item's canonical `content`\n\t\t\t// (todoContent) — the display title flips between content and activeForm\n\t\t\t// with status, so it can't identify an item. Matching by content keeps a\n\t\t\t// task's id pinned to the same plan item when the list is reordered or\n\t\t\t// shrunk; a purely positional reconcile re-labeled the surviving slots,\n\t\t\t// which silently re-pointed the subagent runs linked to those ids\n\t\t\t// (linkedTaskId) at the wrong plan items. Unmatched incoming items then\n\t\t\t// consume the leftover slots in order (a rename keeps its id and its\n\t\t\t// linked runs); any remaining leftovers were removed from the plan.\n\t\t\ttaskStore.batch(() => {\n\t\t\t\tconst content = (item: TodoWriteParams[\"todos\"][number]) => item.content.trim();\n\t\t\t\tconst matchedExisting = new Set<number>();\n\t\t\t\tconst assigned = new Array<Task | undefined>(todos.length);\n\t\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\t\tconst idx = existing.findIndex(\n\t\t\t\t\t\t(t, j) => !matchedExisting.has(j) && (t.todoContent ?? t.title) === content(todos[i]!),\n\t\t\t\t\t);\n\t\t\t\t\tif (idx !== -1) {\n\t\t\t\t\t\tmatchedExisting.add(idx);\n\t\t\t\t\t\tassigned[i] = existing[idx];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst leftovers = existing.filter((_, j) => !matchedExisting.has(j));\n\t\t\t\tlet nextLeftover = 0;\n\t\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\t\tif (!assigned[i]) assigned[i] = leftovers[nextLeftover++];\n\t\t\t\t}\n\n\t\t\t\tconst finalIds: number[] = [];\n\t\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\t\tconst item = todos[i]!;\n\t\t\t\t\tconst status = toTaskStatus(item.status);\n\t\t\t\t\tconst title = displayTitle(item);\n\t\t\t\t\tconst current = assigned[i];\n\t\t\t\t\tif (current) {\n\t\t\t\t\t\ttaskStore.update(current.id, { title, status, todoContent: content(item) });\n\t\t\t\t\t\tfinalIds.push(current.id);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst created = taskStore.create(title);\n\t\t\t\t\t\ttaskStore.update(created.id, { status, todoContent: content(item) });\n\t\t\t\t\t\tfinalIds.push(created.id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (let j = nextLeftover; j < leftovers.length; j++) {\n\t\t\t\t\ttaskStore.remove(leftovers[j]!.id);\n\t\t\t\t}\n\t\t\t\t// Identity matching keeps ids, but the panel must still show the plan\n\t\t\t\t// in the list's order — permute the plan tasks into it.\n\t\t\t\ttaskStore.arrange(finalIds);\n\t\t\t});\n\n\t\t\tconst counts = todos.reduce(\n\t\t\t\t(acc, t) => {\n\t\t\t\t\tif (t.status === \"in_progress\") acc.inProgress++;\n\t\t\t\t\telse if (t.status === \"completed\") acc.completed++;\n\t\t\t\t\telse acc.pending++;\n\t\t\t\t\treturn acc;\n\t\t\t\t},\n\t\t\t\t{ pending: 0, inProgress: 0, completed: 0 },\n\t\t\t);\n\n\t\t\tconst lines = todos.map((t) => `${STATUS_GLYPH[toTaskStatus(t.status)]} ${displayTitle(t)}`);\n\t\t\tconst header =\n\t\t\t\ttodos.length === 0\n\t\t\t\t\t? \"Todo list cleared.\"\n\t\t\t\t\t: `Todos updated (${counts.inProgress} in progress, ${counts.pending} pending, ${counts.completed} completed):`;\n\t\t\tconst text = todos.length === 0 ? header : `${header}\\n${lines.join(\"\\n\")}`;\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\tdetails: {\n\t\t\t\t\ttotal: todos.length,\n\t\t\t\t\tpending: counts.pending,\n\t\t\t\t\tinProgress: counts.inProgress,\n\t\t\t\t\tcompleted: counts.completed,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"todo.js","sourceRoot":"","sources":["../../../src/core/tools/todo.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * TodoWrite tool: let the main agent maintain a visible todo list for the\n * current task.\n *\n * hoocode already has all the infrastructure this needs — the task store models\n * `{ title, status }` items with a per-turn lifecycle, and the TUI task panel\n * renders them. The only missing piece was a tool the model can call; this is\n * that thin adapter over `taskStore`.\n *\n * Semantics mirror Claude Code's TodoWrite: each call sends the FULL list and\n * REPLACES the previous one. Because the store is incremental (numeric ids), we\n * reconcile the incoming list against the existing main-agent tasks by position:\n * update items that are still there, create new ones, and drop the tail that was\n * removed. Reconciling (rather than clear-and-recreate) keeps ids stable so the\n * panel does not flicker and in-progress rows stay put.\n *\n * It is an optional, opt-in tool (enabled via the `enableTodoWrite` setting) and\n * is never registered inside a spawned subagent, so a subagent's todos cannot\n * leak into the parent's \"main\" task group.\n */\n\nimport { type Static, Type } from \"typebox\";\nimport { TODO_WRITE_TOOL_NAME } from \"../agent-frontmatter.js\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport { type Task, type TaskStatus, taskStore } from \"../task-store.js\";\n\nconst todoStatusSchema = Type.Union([Type.Literal(\"pending\"), Type.Literal(\"in_progress\"), Type.Literal(\"completed\")], {\n\tdescription: \"pending = not started, in_progress = actively being worked on, completed = finished.\",\n});\n\nconst todoItemSchema = Type.Object(\n\t{\n\t\tcontent: Type.String({\n\t\t\tdescription: \"The task, in imperative form (e.g. 'Add tests for the parser').\",\n\t\t}),\n\t\tstatus: todoStatusSchema,\n\t\tactiveForm: Type.Optional(\n\t\t\tType.String({\n\t\t\t\tdescription:\n\t\t\t\t\t\"Optional present-tense form shown while the item is in_progress (e.g. 'Adding tests for the parser').\",\n\t\t\t}),\n\t\t),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst todoWriteParams = Type.Object(\n\t{\n\t\ttodos: Type.Array(todoItemSchema, {\n\t\t\tdescription:\n\t\t\t\t\"The complete todo list. This REPLACES the previous list on every call, so always send every item with its current status — omitting an item removes it.\",\n\t\t}),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype TodoWriteParams = Static<typeof todoWriteParams>;\ntype IncomingStatus = TodoWriteParams[\"todos\"][number][\"status\"];\n\nexport interface TodoWriteDetails {\n\ttotal: number;\n\tpending: number;\n\tinProgress: number;\n\tcompleted: number;\n}\n\n/** Map the model-facing status vocabulary onto the task store's. */\nfunction toTaskStatus(status: IncomingStatus): TaskStatus {\n\treturn status === \"completed\" ? \"done\" : status;\n}\n\nconst STATUS_GLYPH: Record<TaskStatus, string> = {\n\tpending: \"[ ]\",\n\tin_progress: \"[~]\",\n\tdone: \"[x]\",\n\tfailed: \"[!]\",\n\t// TodoWrite never produces cancelled items; present for Record exhaustiveness.\n\tcancelled: \"[-]\",\n};\n\n/** Title to display: the active-form while in progress, otherwise the content. */\nfunction displayTitle(item: TodoWriteParams[\"todos\"][number]): string {\n\tif (item.status === \"in_progress\" && item.activeForm?.trim()) return item.activeForm.trim();\n\treturn item.content.trim();\n}\n\n/**\n * A root task the main agent itself owns, i.e. a TodoWrite plan item: no\n * `source` (excludes \"subagent\"/MCP rows), no `agent` (excludes delegated rows),\n * and no `parentTaskId` (excludes merged child trees). `taskOwnerId()` would\n * fold MCP-sourced and delegated rows under \"main\", so reconciling against it\n * could overwrite or drop those rows when the TodoWrite list is shorter than the\n * combined count.\n */\nfunction isMainPlanTask(task: Task): boolean {\n\treturn task.source === undefined && task.agent === undefined && task.parentTaskId === undefined;\n}\n\n/** Current main-agent plan tasks, in stable creation order. */\nfunction mainTasks(): Task[] {\n\treturn taskStore.list().filter(isMainPlanTask);\n}\n\nfunction isActive(task: Task): boolean {\n\treturn task.status === \"pending\" || task.status === \"in_progress\";\n}\n\n/**\n * Settle plan items the model left pinned at `in_progress` when a request ends.\n *\n * TodoWrite is bookkeeping the model performs by hand, and even strong models\n * routinely drop the final call that flips the last item to completed. Nothing\n * else writes main-plan rows, so without this the panel would keep claiming the\n * work is in flight until the next user message triggers `taskStore.reset()`.\n * The request is over, so the row is wrong either way — settle it to the honest\n * outcome instead of leaving it lying.\n *\n * Scope is deliberately narrow:\n * - Only `in_progress` main-plan items. `pending` rows are left alone: \"never\n * started\" is already an accurate reading of an item the model skipped.\n * - Only main-plan items. Subagent- and MCP-sourced rows settle through their\n * own lifecycles (subagent.ts, mcp-loader.ts) and must not be second-guessed\n * here.\n * - Nothing settles while any delegated task is still pending/in_progress: a\n * subagent outliving the parent's agent_end is still working the plan, so its\n * plan item is genuinely in progress.\n *\n * Returns the number of tasks settled.\n */\nexport function settleDanglingMainTasks(outcome: Extract<TaskStatus, \"done\" | \"cancelled\">): number {\n\tconst all = taskStore.list();\n\tif (all.some((t) => !isMainPlanTask(t) && isActive(t))) return 0;\n\tconst dangling = all.filter((t) => isMainPlanTask(t) && t.status === \"in_progress\");\n\tif (dangling.length === 0) return 0;\n\ttaskStore.batch(() => {\n\t\tfor (const task of dangling) {\n\t\t\ttaskStore.update(task.id, { status: outcome });\n\t\t}\n\t});\n\treturn dangling.length;\n}\n\n/** Create the TodoWrite tool definition. Registered as a customTool when enabled. */\nexport function createTodoWriteToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof todoWriteParams, TodoWriteDetails>({\n\t\tname: TODO_WRITE_TOOL_NAME,\n\t\tlabel: TODO_WRITE_TOOL_NAME,\n\t\tdescription: [\n\t\t\t\"Maintain a structured todo list for the current task, shown live in the task panel.\",\n\t\t\t\"Use it PROACTIVELY: at the start of any multi-step or non-trivial task, write the full plan as todos before you begin, then keep it current as you work.\",\n\t\t\t\"Mark exactly ONE item in_progress at a time, and flip an item to completed immediately after finishing it — do not batch completions or leave finished work marked in_progress.\",\n\t\t\t\"Each call sends the FULL list and REPLACES the previous one — always include every item with its current status; omitting an item removes it.\",\n\t\t\t\"Skip it only for trivial, single-step tasks where a list adds no value. When in doubt on multi-step work, use it — it keeps you from losing track of steps.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet:\n\t\t\t\"Plan and track multi-step work as a live todo list (use proactively; replaces the whole list each call)\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use TodoWrite proactively for any multi-step or non-trivial task: write the plan as todos up front, keep exactly one item in_progress, and mark items completed immediately as you finish them.\",\n\t\t\t\"TodoWrite replaces the entire list each call — always send all items with their current status.\",\n\t\t\t\"Skip TodoWrite for trivial single-step tasks where a checklist adds no value.\",\n\t\t],\n\t\tparameters: todoWriteParams,\n\t\tasync execute(_toolCallId, params: TodoWriteParams) {\n\t\t\tconst todos = params.todos ?? [];\n\t\t\tconst existing = mainTasks();\n\n\t\t\t// Reconcile by item identity first, position second, batched so the panel\n\t\t\t// renders once. Each task stores its item's canonical `content`\n\t\t\t// (todoContent) — the display title flips between content and activeForm\n\t\t\t// with status, so it can't identify an item. Matching by content keeps a\n\t\t\t// task's id pinned to the same plan item when the list is reordered or\n\t\t\t// shrunk; a purely positional reconcile re-labeled the surviving slots,\n\t\t\t// which silently re-pointed the subagent runs linked to those ids\n\t\t\t// (linkedTaskId) at the wrong plan items. Unmatched incoming items then\n\t\t\t// consume the leftover slots in order (a rename keeps its id and its\n\t\t\t// linked runs); any remaining leftovers were removed from the plan.\n\t\t\ttaskStore.batch(() => {\n\t\t\t\tconst content = (item: TodoWriteParams[\"todos\"][number]) => item.content.trim();\n\t\t\t\tconst matchedExisting = new Set<number>();\n\t\t\t\tconst assigned = new Array<Task | undefined>(todos.length);\n\t\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\t\tconst idx = existing.findIndex(\n\t\t\t\t\t\t(t, j) => !matchedExisting.has(j) && (t.todoContent ?? t.title) === content(todos[i]!),\n\t\t\t\t\t);\n\t\t\t\t\tif (idx !== -1) {\n\t\t\t\t\t\tmatchedExisting.add(idx);\n\t\t\t\t\t\tassigned[i] = existing[idx];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst leftovers = existing.filter((_, j) => !matchedExisting.has(j));\n\t\t\t\tlet nextLeftover = 0;\n\t\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\t\tif (!assigned[i]) assigned[i] = leftovers[nextLeftover++];\n\t\t\t\t}\n\n\t\t\t\tconst finalIds: number[] = [];\n\t\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\t\tconst item = todos[i]!;\n\t\t\t\t\tconst status = toTaskStatus(item.status);\n\t\t\t\t\tconst title = displayTitle(item);\n\t\t\t\t\tconst current = assigned[i];\n\t\t\t\t\tif (current) {\n\t\t\t\t\t\ttaskStore.update(current.id, { title, status, todoContent: content(item) });\n\t\t\t\t\t\tfinalIds.push(current.id);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst created = taskStore.create(title);\n\t\t\t\t\t\ttaskStore.update(created.id, { status, todoContent: content(item) });\n\t\t\t\t\t\tfinalIds.push(created.id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (let j = nextLeftover; j < leftovers.length; j++) {\n\t\t\t\t\ttaskStore.remove(leftovers[j]!.id);\n\t\t\t\t}\n\t\t\t\t// Identity matching keeps ids, but the panel must still show the plan\n\t\t\t\t// in the list's order — permute the plan tasks into it.\n\t\t\t\ttaskStore.arrange(finalIds);\n\t\t\t});\n\n\t\t\tconst counts = todos.reduce(\n\t\t\t\t(acc, t) => {\n\t\t\t\t\tif (t.status === \"in_progress\") acc.inProgress++;\n\t\t\t\t\telse if (t.status === \"completed\") acc.completed++;\n\t\t\t\t\telse acc.pending++;\n\t\t\t\t\treturn acc;\n\t\t\t\t},\n\t\t\t\t{ pending: 0, inProgress: 0, completed: 0 },\n\t\t\t);\n\n\t\t\tconst lines = todos.map((t) => `${STATUS_GLYPH[toTaskStatus(t.status)]} ${displayTitle(t)}`);\n\t\t\tconst header =\n\t\t\t\ttodos.length === 0\n\t\t\t\t\t? \"Todo list cleared.\"\n\t\t\t\t\t: `Todos updated (${counts.inProgress} in progress, ${counts.pending} pending, ${counts.completed} completed):`;\n\t\t\tconst text = todos.length === 0 ? header : `${header}\\n${lines.join(\"\\n\")}`;\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\tdetails: {\n\t\t\t\t\ttotal: todos.length,\n\t\t\t\t\tpending: counts.pending,\n\t\t\t\t\tinProgress: counts.inProgress,\n\t\t\t\t\tcompleted: counts.completed,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,0BAA0B,CAAC;AACzF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AA2FxF;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAI/C,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,UAAU;IAJnB,OAAO,CAAC,kBAAkB,CAAQ;IAElC,YACS,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B,EAC3C;IAEJ,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAoJ9B;CACD","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { formatTokens } from \"../../../core/format-tokens.js\";\nimport { type StartupProgress, startupProgress } from \"../../../core/startup-progress.js\";\nimport { taskStore } from \"../../../core/task-store.js\";\nimport { BRAND_MARK, GIT_BRANCH_GLYPH } from \"../brand.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Assemble one footer line: `left` flush left, `right` flush right when it fits\n * (≥2 cols between), padded to the full width. When it doesn't fit, drop `right`\n * and pad; when even `left` overflows, truncate it. Width math runs on the plain\n * strings; the styled strings carry the colour. Every returned line is exactly\n * `width` cells or fewer — the invariant the footer-width tests hold us to.\n */\nfunction assembleLine(width: number, leftPlain: string, leftStyled: string, rightPlain = \"\", rightStyled = \"\"): string {\n\tconst lw = visibleWidth(leftPlain);\n\tif (rightPlain && lw + 2 + visibleWidth(rightPlain) <= width) {\n\t\treturn leftStyled + \" \".repeat(width - lw - visibleWidth(rightPlain)) + rightStyled;\n\t}\n\tif (lw <= width) return leftStyled + \" \".repeat(width - lw);\n\treturn truncateToWidth(leftStyled, width, theme.fg(\"dim\", \"…\"));\n}\n\n/** A compact context-fill gauge, coloured by proximity to the auto-compact trip point. */\nfunction contextGauge(percent: number, errorLevel: number, warnLevel: number): { plain: string; styled: string } {\n\tconst CELLS = 8;\n\tconst filled = Math.max(0, Math.min(CELLS, Math.round((percent / 100) * CELLS)));\n\tconst fill = \"▰\".repeat(filled);\n\tconst track = \"▱\".repeat(CELLS - filled);\n\tconst color = percent >= errorLevel ? \"error\" : percent >= warnLevel ? \"warning\" : \"accent\";\n\treturn { plain: fill + track, styled: theme.fg(color, fill) + theme.fg(\"dim\", track) };\n}\n\n/** Count subagent runs currently in flight, for the footer's live delegation cue. */\nfunction activeSubagentCount(): number {\n\treturn taskStore.list().filter((t) => t.source === \"subagent\" && t.status === \"in_progress\").length;\n}\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/** Cells in a startup-progress bar; compact so several tools fit the footer. */\nconst STARTUP_BAR_CELLS = 12;\n\nfunction formatMb(bytes: number): string {\n\treturn `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\n/**\n * One footer line for a transient startup-progress entry (tool download or\n * index build), styled like the voice download bar: a `·` fill over a dim\n * track with percent and a `received / total` (or `done/total`) detail. An\n * indeterminate download (no Content-Length) drops the bar for a running byte\n * count; an error entry renders as a dim message. Returns a styled string; the\n * caller width-clamps it.\n */\nfunction renderStartupLine(entry: StartupProgress): string {\n\tif (entry.kind === \"error\") {\n\t\treturn theme.fg(\"dim\", `${entry.label}: ${entry.message}`);\n\t}\n\tconst label = theme.fg(\"text\", entry.label);\n\tif (entry.kind === \"download\") {\n\t\tif (entry.totalBytes === null || entry.totalBytes <= 0) {\n\t\t\treturn `${label} ${theme.fg(\"dim\", `${formatMb(entry.receivedBytes)}…`)}`;\n\t\t}\n\t\tconst detail = `${formatMb(entry.receivedBytes)} / ${formatMb(entry.totalBytes)}`;\n\t\treturn `${label} ${determinateBar(entry.receivedBytes / entry.totalBytes, detail)}`;\n\t}\n\tconst detail = `${entry.done}/${entry.total} ${entry.unit}`;\n\tconst ratio = entry.total > 0 ? entry.done / entry.total : 0;\n\treturn `${label} ${determinateBar(ratio, detail)}`;\n}\n\n/** `·`-fill bar + percent + trailing detail, matching the voice download bar. */\nfunction determinateBar(ratio: number, detail: string): string {\n\tconst clamped = Math.max(0, Math.min(1, ratio));\n\tconst filled = Math.round(clamped * STARTUP_BAR_CELLS);\n\tconst bar = theme.fg(\"accent\", \"·\".repeat(filled)) + theme.fg(\"dim\", \"·\".repeat(STARTUP_BAR_CELLS - filled));\n\tconst pct = `${Math.round(clamped * 100)}%`;\n\treturn `${bar} ${theme.fg(\"muted\", pct)} ${theme.fg(\"dim\", `· ${detail}`)}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = this.session.sessionManager.getCwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tconst modeLabel = this.footerData.getActiveMode();\n\n\t\t// ── Line 1 — identity & location ────────────────────────────────────────\n\t\t// Lead with the brand mark + MODE (the agent's guardrail: Ask/Plan/Build/\n\t\t// Debug) in bold accent so it is the first thing the eye lands on, then the\n\t\t// path, git branch, and session name in descending emphasis. The live\n\t\t// subagent count sits flush right — present only while work is delegated.\n\t\tconst modeUp = modeLabel.toUpperCase();\n\t\tconst brand = `${BRAND_MARK} ${modeUp}`;\n\t\tlet l1Plain = `${brand} ${pwd}`;\n\t\tlet l1Styled = `${theme.bold(theme.fg(\"accent\", brand))} ${theme.fg(\"muted\", pwd)}`;\n\t\tif (branch) {\n\t\t\tl1Plain += ` ${GIT_BRANCH_GLYPH} ${branch}`;\n\t\t\tl1Styled += ` ${theme.fg(\"dim\", GIT_BRANCH_GLYPH)} ${theme.fg(\"muted\", branch)}`;\n\t\t}\n\t\tif (sessionName) {\n\t\t\tl1Plain += ` • ${sessionName}`;\n\t\t\tl1Styled += theme.fg(\"dim\", ` • ${sessionName}`);\n\t\t}\n\t\tconst nSub = activeSubagentCount();\n\t\tconst l1RightPlain = nSub > 0 ? `◇${nSub} running` : \"\";\n\t\tconst l1RightStyled = nSub > 0 ? theme.fg(\"accent\", `◇${nSub}`) + theme.fg(\"dim\", \" running\") : \"\";\n\t\tconst line1 = assembleLine(width, l1Plain, l1Styled, l1RightPlain, l1RightStyled);\n\n\t\t// ── Line 2 — session vitals ─────────────────────────────────────────────\n\t\t// A context-fill gauge (coloured by proximity to the auto-compact trip\n\t\t// point) leads, then token/cost deltas, with the model + thinking level\n\t\t// flush right. Numbers read in muted, labels/arrows in dim — a legible\n\t\t// hierarchy in place of the old uniform grey.\n\t\tlet thresholdPercent: number | undefined;\n\t\tif (this.autoCompactEnabled && contextWindow > 0) {\n\t\t\tconst reserveTokens = this.session.settingsManager.getCompactionSettings().reserveTokens;\n\t\t\tconst effective = contextWindow - reserveTokens;\n\t\t\tif (effective > 0) thresholdPercent = (effective / contextWindow) * 100;\n\t\t}\n\t\tconst errorLevel = thresholdPercent !== undefined ? thresholdPercent - 3 : 90;\n\t\tconst warnLevel = thresholdPercent !== undefined ? thresholdPercent - 10 : 70;\n\t\tconst autoIndicator =\n\t\t\tthresholdPercent !== undefined\n\t\t\t\t? ` auto@${thresholdPercent.toFixed(0)}%`\n\t\t\t\t: this.autoCompactEnabled\n\t\t\t\t\t? \" auto\"\n\t\t\t\t\t: \"\";\n\n\t\tconst gauge = contextGauge(contextPercentValue, errorLevel, warnLevel);\n\t\tconst pctText = contextPercent === \"?\" ? \"?\" : `${contextPercent}%`;\n\t\tconst pctColor =\n\t\t\tcontextPercentValue >= errorLevel ? \"error\" : contextPercentValue >= warnLevel ? \"warning\" : \"muted\";\n\t\tconst winText = `${formatTokens(contextWindow)}${autoIndicator}`;\n\n\t\tconst segs: Array<{ plain: string; styled: string }> = [\n\t\t\t{\n\t\t\t\tplain: `${gauge.plain} ${pctText} ${winText}`,\n\t\t\t\tstyled: `${gauge.styled} ${theme.fg(pctColor, pctText)} ${theme.fg(\"dim\", winText)}`,\n\t\t\t},\n\t\t];\n\t\tconst arrow = (a: string, n: number) => ({\n\t\t\tplain: `${a}${formatTokens(n)}`,\n\t\t\tstyled: theme.fg(\"dim\", a) + theme.fg(\"muted\", formatTokens(n)),\n\t\t});\n\t\tif (totalInput) segs.push(arrow(\"↑\", totalInput));\n\t\tif (totalOutput) segs.push(arrow(\"↓\", totalOutput));\n\t\tif (totalCacheRead) segs.push(arrow(\"R\", totalCacheRead));\n\t\tif (totalCacheWrite) segs.push(arrow(\"W\", totalCacheWrite));\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tsegs.push({ plain: costStr, styled: theme.fg(\"muted\", costStr) });\n\t\t}\n\t\tconst l2Plain = segs.map((s) => s.plain).join(\" \");\n\t\tconst l2Styled = segs.map((s) => s.styled).join(\" \");\n\n\t\t// Right: model, thinking level, and provider (when several are configured).\n\t\tconst modelName = state.model?.id || \"no-model\";\n\t\tlet r2Plain = modelName;\n\t\tlet r2Styled = theme.fg(\"muted\", modelName);\n\t\tif (state.model?.reasoning) {\n\t\t\tconst tl = state.thinkingLevel || \"off\";\n\t\t\tconst tstr = tl === \"off\" ? \"thinking off\" : tl;\n\t\t\tr2Plain += ` • ${tstr}`;\n\t\t\tr2Styled += theme.fg(\"dim\", ` • ${tstr}`);\n\t\t}\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t// Prepend the provider only when the whole right cluster still fits.\n\t\t\tconst withProv = `(${state.model.provider}) ${r2Plain}`;\n\t\t\tif (visibleWidth(l2Plain) + 2 + visibleWidth(withProv) <= width) {\n\t\t\t\tr2Plain = withProv;\n\t\t\t\tr2Styled = theme.fg(\"dim\", `(${state.model.provider}) `) + r2Styled;\n\t\t\t}\n\t\t}\n\t\tconst line2 = assembleLine(width, l2Plain, l2Styled, r2Plain, r2Styled);\n\n\t\tconst lines = [line1, line2];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\t// Transient startup progress (first-run tool downloads, index build): one\n\t\t// determinate bar per entry, cleared as each settles. Width-clamped like the\n\t\t// status line so the footer never overflows.\n\t\tfor (const entry of startupProgress.list()) {\n\t\t\tlines.push(truncateToWidth(renderStartupLine(entry), width, theme.fg(\"dim\", \"…\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,0BAA0B,CAAC;AACzF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AA2FxF;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAI/C,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,UAAU;IAJnB,OAAO,CAAC,kBAAkB,CAAQ;IAElC,YACS,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B,EAC3C;IAEJ,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA+I9B;CACD","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport { sumAssistantUsage } from \"../../../core/agent-session-stats.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { formatTokens } from \"../../../core/format-tokens.js\";\nimport { type StartupProgress, startupProgress } from \"../../../core/startup-progress.js\";\nimport { taskStore } from \"../../../core/task-store.js\";\nimport { BRAND_MARK, GIT_BRANCH_GLYPH } from \"../brand.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Assemble one footer line: `left` flush left, `right` flush right when it fits\n * (≥2 cols between), padded to the full width. When it doesn't fit, drop `right`\n * and pad; when even `left` overflows, truncate it. Width math runs on the plain\n * strings; the styled strings carry the colour. Every returned line is exactly\n * `width` cells or fewer — the invariant the footer-width tests hold us to.\n */\nfunction assembleLine(width: number, leftPlain: string, leftStyled: string, rightPlain = \"\", rightStyled = \"\"): string {\n\tconst lw = visibleWidth(leftPlain);\n\tif (rightPlain && lw + 2 + visibleWidth(rightPlain) <= width) {\n\t\treturn leftStyled + \" \".repeat(width - lw - visibleWidth(rightPlain)) + rightStyled;\n\t}\n\tif (lw <= width) return leftStyled + \" \".repeat(width - lw);\n\treturn truncateToWidth(leftStyled, width, theme.fg(\"dim\", \"…\"));\n}\n\n/** A compact context-fill gauge, coloured by proximity to the auto-compact trip point. */\nfunction contextGauge(percent: number, errorLevel: number, warnLevel: number): { plain: string; styled: string } {\n\tconst CELLS = 8;\n\tconst filled = Math.max(0, Math.min(CELLS, Math.round((percent / 100) * CELLS)));\n\tconst fill = \"▰\".repeat(filled);\n\tconst track = \"▱\".repeat(CELLS - filled);\n\tconst color = percent >= errorLevel ? \"error\" : percent >= warnLevel ? \"warning\" : \"accent\";\n\treturn { plain: fill + track, styled: theme.fg(color, fill) + theme.fg(\"dim\", track) };\n}\n\n/** Count subagent runs currently in flight, for the footer's live delegation cue. */\nfunction activeSubagentCount(): number {\n\treturn taskStore.list().filter((t) => t.source === \"subagent\" && t.status === \"in_progress\").length;\n}\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/** Cells in a startup-progress bar; compact so several tools fit the footer. */\nconst STARTUP_BAR_CELLS = 12;\n\nfunction formatMb(bytes: number): string {\n\treturn `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\n/**\n * One footer line for a transient startup-progress entry (tool download or\n * index build), styled like the voice download bar: a `·` fill over a dim\n * track with percent and a `received / total` (or `done/total`) detail. An\n * indeterminate download (no Content-Length) drops the bar for a running byte\n * count; an error entry renders as a dim message. Returns a styled string; the\n * caller width-clamps it.\n */\nfunction renderStartupLine(entry: StartupProgress): string {\n\tif (entry.kind === \"error\") {\n\t\treturn theme.fg(\"dim\", `${entry.label}: ${entry.message}`);\n\t}\n\tconst label = theme.fg(\"text\", entry.label);\n\tif (entry.kind === \"download\") {\n\t\tif (entry.totalBytes === null || entry.totalBytes <= 0) {\n\t\t\treturn `${label} ${theme.fg(\"dim\", `${formatMb(entry.receivedBytes)}…`)}`;\n\t\t}\n\t\tconst detail = `${formatMb(entry.receivedBytes)} / ${formatMb(entry.totalBytes)}`;\n\t\treturn `${label} ${determinateBar(entry.receivedBytes / entry.totalBytes, detail)}`;\n\t}\n\tconst detail = `${entry.done}/${entry.total} ${entry.unit}`;\n\tconst ratio = entry.total > 0 ? entry.done / entry.total : 0;\n\treturn `${label} ${determinateBar(ratio, detail)}`;\n}\n\n/** `·`-fill bar + percent + trailing detail, matching the voice download bar. */\nfunction determinateBar(ratio: number, detail: string): string {\n\tconst clamped = Math.max(0, Math.min(1, ratio));\n\tconst filled = Math.round(clamped * STARTUP_BAR_CELLS);\n\tconst bar = theme.fg(\"accent\", \"·\".repeat(filled)) + theme.fg(\"dim\", \"·\".repeat(STARTUP_BAR_CELLS - filled));\n\tconst pct = `${Math.round(clamped * 100)}%`;\n\treturn `${bar} ${theme.fg(\"muted\", pct)} ${theme.fg(\"dim\", `· ${detail}`)}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Cumulative usage across ALL session entries (not just post-compaction\n\t\t// messages). Shares sumAssistantUsage with the per-request cost line the\n\t\t// transcript prints at agent_end, so the footer total and that line are always\n\t\t// derived from the same accounting.\n\t\tconst {\n\t\t\tinput: totalInput,\n\t\t\toutput: totalOutput,\n\t\t\tcacheRead: totalCacheRead,\n\t\t\tcacheWrite: totalCacheWrite,\n\t\t\tcost: totalCost,\n\t\t} = sumAssistantUsage(this.session.sessionManager.getEntries());\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = this.session.sessionManager.getCwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tconst modeLabel = this.footerData.getActiveMode();\n\n\t\t// ── Line 1 — identity & location ────────────────────────────────────────\n\t\t// Lead with the brand mark + MODE (the agent's guardrail: Ask/Plan/Build/\n\t\t// Debug) in bold accent so it is the first thing the eye lands on, then the\n\t\t// path, git branch, and session name in descending emphasis. The live\n\t\t// subagent count sits flush right — present only while work is delegated.\n\t\tconst modeUp = modeLabel.toUpperCase();\n\t\tconst brand = `${BRAND_MARK} ${modeUp}`;\n\t\tlet l1Plain = `${brand} ${pwd}`;\n\t\tlet l1Styled = `${theme.bold(theme.fg(\"accent\", brand))} ${theme.fg(\"muted\", pwd)}`;\n\t\tif (branch) {\n\t\t\tl1Plain += ` ${GIT_BRANCH_GLYPH} ${branch}`;\n\t\t\tl1Styled += ` ${theme.fg(\"dim\", GIT_BRANCH_GLYPH)} ${theme.fg(\"muted\", branch)}`;\n\t\t}\n\t\tif (sessionName) {\n\t\t\tl1Plain += ` • ${sessionName}`;\n\t\t\tl1Styled += theme.fg(\"dim\", ` • ${sessionName}`);\n\t\t}\n\t\tconst nSub = activeSubagentCount();\n\t\tconst l1RightPlain = nSub > 0 ? `◇${nSub} running` : \"\";\n\t\tconst l1RightStyled = nSub > 0 ? theme.fg(\"accent\", `◇${nSub}`) + theme.fg(\"dim\", \" running\") : \"\";\n\t\tconst line1 = assembleLine(width, l1Plain, l1Styled, l1RightPlain, l1RightStyled);\n\n\t\t// ── Line 2 — session vitals ─────────────────────────────────────────────\n\t\t// A context-fill gauge (coloured by proximity to the auto-compact trip\n\t\t// point) leads, then token/cost deltas, with the model + thinking level\n\t\t// flush right. Numbers read in muted, labels/arrows in dim — a legible\n\t\t// hierarchy in place of the old uniform grey.\n\t\tlet thresholdPercent: number | undefined;\n\t\tif (this.autoCompactEnabled && contextWindow > 0) {\n\t\t\tconst reserveTokens = this.session.settingsManager.getCompactionSettings().reserveTokens;\n\t\t\tconst effective = contextWindow - reserveTokens;\n\t\t\tif (effective > 0) thresholdPercent = (effective / contextWindow) * 100;\n\t\t}\n\t\tconst errorLevel = thresholdPercent !== undefined ? thresholdPercent - 3 : 90;\n\t\tconst warnLevel = thresholdPercent !== undefined ? thresholdPercent - 10 : 70;\n\t\tconst autoIndicator =\n\t\t\tthresholdPercent !== undefined\n\t\t\t\t? ` auto@${thresholdPercent.toFixed(0)}%`\n\t\t\t\t: this.autoCompactEnabled\n\t\t\t\t\t? \" auto\"\n\t\t\t\t\t: \"\";\n\n\t\tconst gauge = contextGauge(contextPercentValue, errorLevel, warnLevel);\n\t\tconst pctText = contextPercent === \"?\" ? \"?\" : `${contextPercent}%`;\n\t\tconst pctColor =\n\t\t\tcontextPercentValue >= errorLevel ? \"error\" : contextPercentValue >= warnLevel ? \"warning\" : \"muted\";\n\t\tconst winText = `${formatTokens(contextWindow)}${autoIndicator}`;\n\n\t\tconst segs: Array<{ plain: string; styled: string }> = [\n\t\t\t{\n\t\t\t\tplain: `${gauge.plain} ${pctText} ${winText}`,\n\t\t\t\tstyled: `${gauge.styled} ${theme.fg(pctColor, pctText)} ${theme.fg(\"dim\", winText)}`,\n\t\t\t},\n\t\t];\n\t\tconst arrow = (a: string, n: number) => ({\n\t\t\tplain: `${a}${formatTokens(n)}`,\n\t\t\tstyled: theme.fg(\"dim\", a) + theme.fg(\"muted\", formatTokens(n)),\n\t\t});\n\t\tif (totalInput) segs.push(arrow(\"↑\", totalInput));\n\t\tif (totalOutput) segs.push(arrow(\"↓\", totalOutput));\n\t\tif (totalCacheRead) segs.push(arrow(\"R\", totalCacheRead));\n\t\tif (totalCacheWrite) segs.push(arrow(\"W\", totalCacheWrite));\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tsegs.push({ plain: costStr, styled: theme.fg(\"muted\", costStr) });\n\t\t}\n\t\tconst l2Plain = segs.map((s) => s.plain).join(\" \");\n\t\tconst l2Styled = segs.map((s) => s.styled).join(\" \");\n\n\t\t// Right: model, thinking level, and provider (when several are configured).\n\t\tconst modelName = state.model?.id || \"no-model\";\n\t\tlet r2Plain = modelName;\n\t\tlet r2Styled = theme.fg(\"muted\", modelName);\n\t\tif (state.model?.reasoning) {\n\t\t\tconst tl = state.thinkingLevel || \"off\";\n\t\t\tconst tstr = tl === \"off\" ? \"thinking off\" : tl;\n\t\t\tr2Plain += ` • ${tstr}`;\n\t\t\tr2Styled += theme.fg(\"dim\", ` • ${tstr}`);\n\t\t}\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t// Prepend the provider only when the whole right cluster still fits.\n\t\t\tconst withProv = `(${state.model.provider}) ${r2Plain}`;\n\t\t\tif (visibleWidth(l2Plain) + 2 + visibleWidth(withProv) <= width) {\n\t\t\t\tr2Plain = withProv;\n\t\t\t\tr2Styled = theme.fg(\"dim\", `(${state.model.provider}) `) + r2Styled;\n\t\t\t}\n\t\t}\n\t\tconst line2 = assembleLine(width, l2Plain, l2Styled, r2Plain, r2Styled);\n\n\t\tconst lines = [line1, line2];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\t// Transient startup progress (first-run tool downloads, index build): one\n\t\t// determinate bar per entry, cleared as each settles. Width-clamped like the\n\t\t// status line so the footer never overflows.\n\t\tfor (const entry of startupProgress.list()) {\n\t\t\tlines.push(truncateToWidth(renderStartupLine(entry), width, theme.fg(\"dim\", \"…\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { truncateToWidth, visibleWidth } from "@kolisachint/hoocode-tui";
2
+ import { sumAssistantUsage } from "../../../core/agent-session-stats.js";
2
3
  import { formatTokens } from "../../../core/format-tokens.js";
3
4
  import { startupProgress } from "../../../core/startup-progress.js";
4
5
  import { taskStore } from "../../../core/task-store.js";
@@ -115,21 +116,11 @@ export class FooterComponent {
115
116
  }
116
117
  render(width) {
117
118
  const state = this.session.state;
118
- // Calculate cumulative usage from ALL session entries (not just post-compaction messages)
119
- let totalInput = 0;
120
- let totalOutput = 0;
121
- let totalCacheRead = 0;
122
- let totalCacheWrite = 0;
123
- let totalCost = 0;
124
- for (const entry of this.session.sessionManager.getEntries()) {
125
- if (entry.type === "message" && entry.message.role === "assistant") {
126
- totalInput += entry.message.usage.input;
127
- totalOutput += entry.message.usage.output;
128
- totalCacheRead += entry.message.usage.cacheRead;
129
- totalCacheWrite += entry.message.usage.cacheWrite;
130
- totalCost += entry.message.usage.cost.total;
131
- }
132
- }
119
+ // Cumulative usage across ALL session entries (not just post-compaction
120
+ // messages). Shares sumAssistantUsage with the per-request cost line the
121
+ // transcript prints at agent_end, so the footer total and that line are always
122
+ // derived from the same accounting.
123
+ const { input: totalInput, output: totalOutput, cacheRead: totalCacheRead, cacheWrite: totalCacheWrite, cost: totalCost, } = sumAssistantUsage(this.session.sessionManager.getEntries());
133
124
  // Calculate context usage from session (handles compaction correctly).
134
125
  // After compaction, tokens are unknown until the next LLM response.
135
126
  const contextUsage = this.session.getContextUsage();
@@ -1 +1 @@
1
- {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { formatTokens } from \"../../../core/format-tokens.js\";\nimport { type StartupProgress, startupProgress } from \"../../../core/startup-progress.js\";\nimport { taskStore } from \"../../../core/task-store.js\";\nimport { BRAND_MARK, GIT_BRANCH_GLYPH } from \"../brand.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Assemble one footer line: `left` flush left, `right` flush right when it fits\n * (≥2 cols between), padded to the full width. When it doesn't fit, drop `right`\n * and pad; when even `left` overflows, truncate it. Width math runs on the plain\n * strings; the styled strings carry the colour. Every returned line is exactly\n * `width` cells or fewer — the invariant the footer-width tests hold us to.\n */\nfunction assembleLine(width: number, leftPlain: string, leftStyled: string, rightPlain = \"\", rightStyled = \"\"): string {\n\tconst lw = visibleWidth(leftPlain);\n\tif (rightPlain && lw + 2 + visibleWidth(rightPlain) <= width) {\n\t\treturn leftStyled + \" \".repeat(width - lw - visibleWidth(rightPlain)) + rightStyled;\n\t}\n\tif (lw <= width) return leftStyled + \" \".repeat(width - lw);\n\treturn truncateToWidth(leftStyled, width, theme.fg(\"dim\", \"…\"));\n}\n\n/** A compact context-fill gauge, coloured by proximity to the auto-compact trip point. */\nfunction contextGauge(percent: number, errorLevel: number, warnLevel: number): { plain: string; styled: string } {\n\tconst CELLS = 8;\n\tconst filled = Math.max(0, Math.min(CELLS, Math.round((percent / 100) * CELLS)));\n\tconst fill = \"▰\".repeat(filled);\n\tconst track = \"▱\".repeat(CELLS - filled);\n\tconst color = percent >= errorLevel ? \"error\" : percent >= warnLevel ? \"warning\" : \"accent\";\n\treturn { plain: fill + track, styled: theme.fg(color, fill) + theme.fg(\"dim\", track) };\n}\n\n/** Count subagent runs currently in flight, for the footer's live delegation cue. */\nfunction activeSubagentCount(): number {\n\treturn taskStore.list().filter((t) => t.source === \"subagent\" && t.status === \"in_progress\").length;\n}\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/** Cells in a startup-progress bar; compact so several tools fit the footer. */\nconst STARTUP_BAR_CELLS = 12;\n\nfunction formatMb(bytes: number): string {\n\treturn `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\n/**\n * One footer line for a transient startup-progress entry (tool download or\n * index build), styled like the voice download bar: a `·` fill over a dim\n * track with percent and a `received / total` (or `done/total`) detail. An\n * indeterminate download (no Content-Length) drops the bar for a running byte\n * count; an error entry renders as a dim message. Returns a styled string; the\n * caller width-clamps it.\n */\nfunction renderStartupLine(entry: StartupProgress): string {\n\tif (entry.kind === \"error\") {\n\t\treturn theme.fg(\"dim\", `${entry.label}: ${entry.message}`);\n\t}\n\tconst label = theme.fg(\"text\", entry.label);\n\tif (entry.kind === \"download\") {\n\t\tif (entry.totalBytes === null || entry.totalBytes <= 0) {\n\t\t\treturn `${label} ${theme.fg(\"dim\", `${formatMb(entry.receivedBytes)}…`)}`;\n\t\t}\n\t\tconst detail = `${formatMb(entry.receivedBytes)} / ${formatMb(entry.totalBytes)}`;\n\t\treturn `${label} ${determinateBar(entry.receivedBytes / entry.totalBytes, detail)}`;\n\t}\n\tconst detail = `${entry.done}/${entry.total} ${entry.unit}`;\n\tconst ratio = entry.total > 0 ? entry.done / entry.total : 0;\n\treturn `${label} ${determinateBar(ratio, detail)}`;\n}\n\n/** `·`-fill bar + percent + trailing detail, matching the voice download bar. */\nfunction determinateBar(ratio: number, detail: string): string {\n\tconst clamped = Math.max(0, Math.min(1, ratio));\n\tconst filled = Math.round(clamped * STARTUP_BAR_CELLS);\n\tconst bar = theme.fg(\"accent\", \"·\".repeat(filled)) + theme.fg(\"dim\", \"·\".repeat(STARTUP_BAR_CELLS - filled));\n\tconst pct = `${Math.round(clamped * 100)}%`;\n\treturn `${bar} ${theme.fg(\"muted\", pct)} ${theme.fg(\"dim\", `· ${detail}`)}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = this.session.sessionManager.getCwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tconst modeLabel = this.footerData.getActiveMode();\n\n\t\t// ── Line 1 — identity & location ────────────────────────────────────────\n\t\t// Lead with the brand mark + MODE (the agent's guardrail: Ask/Plan/Build/\n\t\t// Debug) in bold accent so it is the first thing the eye lands on, then the\n\t\t// path, git branch, and session name in descending emphasis. The live\n\t\t// subagent count sits flush right — present only while work is delegated.\n\t\tconst modeUp = modeLabel.toUpperCase();\n\t\tconst brand = `${BRAND_MARK} ${modeUp}`;\n\t\tlet l1Plain = `${brand} ${pwd}`;\n\t\tlet l1Styled = `${theme.bold(theme.fg(\"accent\", brand))} ${theme.fg(\"muted\", pwd)}`;\n\t\tif (branch) {\n\t\t\tl1Plain += ` ${GIT_BRANCH_GLYPH} ${branch}`;\n\t\t\tl1Styled += ` ${theme.fg(\"dim\", GIT_BRANCH_GLYPH)} ${theme.fg(\"muted\", branch)}`;\n\t\t}\n\t\tif (sessionName) {\n\t\t\tl1Plain += ` • ${sessionName}`;\n\t\t\tl1Styled += theme.fg(\"dim\", ` • ${sessionName}`);\n\t\t}\n\t\tconst nSub = activeSubagentCount();\n\t\tconst l1RightPlain = nSub > 0 ? `◇${nSub} running` : \"\";\n\t\tconst l1RightStyled = nSub > 0 ? theme.fg(\"accent\", `◇${nSub}`) + theme.fg(\"dim\", \" running\") : \"\";\n\t\tconst line1 = assembleLine(width, l1Plain, l1Styled, l1RightPlain, l1RightStyled);\n\n\t\t// ── Line 2 — session vitals ─────────────────────────────────────────────\n\t\t// A context-fill gauge (coloured by proximity to the auto-compact trip\n\t\t// point) leads, then token/cost deltas, with the model + thinking level\n\t\t// flush right. Numbers read in muted, labels/arrows in dim — a legible\n\t\t// hierarchy in place of the old uniform grey.\n\t\tlet thresholdPercent: number | undefined;\n\t\tif (this.autoCompactEnabled && contextWindow > 0) {\n\t\t\tconst reserveTokens = this.session.settingsManager.getCompactionSettings().reserveTokens;\n\t\t\tconst effective = contextWindow - reserveTokens;\n\t\t\tif (effective > 0) thresholdPercent = (effective / contextWindow) * 100;\n\t\t}\n\t\tconst errorLevel = thresholdPercent !== undefined ? thresholdPercent - 3 : 90;\n\t\tconst warnLevel = thresholdPercent !== undefined ? thresholdPercent - 10 : 70;\n\t\tconst autoIndicator =\n\t\t\tthresholdPercent !== undefined\n\t\t\t\t? ` auto@${thresholdPercent.toFixed(0)}%`\n\t\t\t\t: this.autoCompactEnabled\n\t\t\t\t\t? \" auto\"\n\t\t\t\t\t: \"\";\n\n\t\tconst gauge = contextGauge(contextPercentValue, errorLevel, warnLevel);\n\t\tconst pctText = contextPercent === \"?\" ? \"?\" : `${contextPercent}%`;\n\t\tconst pctColor =\n\t\t\tcontextPercentValue >= errorLevel ? \"error\" : contextPercentValue >= warnLevel ? \"warning\" : \"muted\";\n\t\tconst winText = `${formatTokens(contextWindow)}${autoIndicator}`;\n\n\t\tconst segs: Array<{ plain: string; styled: string }> = [\n\t\t\t{\n\t\t\t\tplain: `${gauge.plain} ${pctText} ${winText}`,\n\t\t\t\tstyled: `${gauge.styled} ${theme.fg(pctColor, pctText)} ${theme.fg(\"dim\", winText)}`,\n\t\t\t},\n\t\t];\n\t\tconst arrow = (a: string, n: number) => ({\n\t\t\tplain: `${a}${formatTokens(n)}`,\n\t\t\tstyled: theme.fg(\"dim\", a) + theme.fg(\"muted\", formatTokens(n)),\n\t\t});\n\t\tif (totalInput) segs.push(arrow(\"↑\", totalInput));\n\t\tif (totalOutput) segs.push(arrow(\"↓\", totalOutput));\n\t\tif (totalCacheRead) segs.push(arrow(\"R\", totalCacheRead));\n\t\tif (totalCacheWrite) segs.push(arrow(\"W\", totalCacheWrite));\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tsegs.push({ plain: costStr, styled: theme.fg(\"muted\", costStr) });\n\t\t}\n\t\tconst l2Plain = segs.map((s) => s.plain).join(\" \");\n\t\tconst l2Styled = segs.map((s) => s.styled).join(\" \");\n\n\t\t// Right: model, thinking level, and provider (when several are configured).\n\t\tconst modelName = state.model?.id || \"no-model\";\n\t\tlet r2Plain = modelName;\n\t\tlet r2Styled = theme.fg(\"muted\", modelName);\n\t\tif (state.model?.reasoning) {\n\t\t\tconst tl = state.thinkingLevel || \"off\";\n\t\t\tconst tstr = tl === \"off\" ? \"thinking off\" : tl;\n\t\t\tr2Plain += ` • ${tstr}`;\n\t\t\tr2Styled += theme.fg(\"dim\", ` • ${tstr}`);\n\t\t}\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t// Prepend the provider only when the whole right cluster still fits.\n\t\t\tconst withProv = `(${state.model.provider}) ${r2Plain}`;\n\t\t\tif (visibleWidth(l2Plain) + 2 + visibleWidth(withProv) <= width) {\n\t\t\t\tr2Plain = withProv;\n\t\t\t\tr2Styled = theme.fg(\"dim\", `(${state.model.provider}) `) + r2Styled;\n\t\t\t}\n\t\t}\n\t\tconst line2 = assembleLine(width, l2Plain, l2Styled, r2Plain, r2Styled);\n\n\t\tconst lines = [line1, line2];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\t// Transient startup progress (first-run tool downloads, index build): one\n\t\t// determinate bar per entry, cleared as each settles. Width-clamped like the\n\t\t// status line so the footer never overflows.\n\t\tfor (const entry of startupProgress.list()) {\n\t\t\tlines.push(truncateToWidth(renderStartupLine(entry), width, theme.fg(\"dim\", \"…\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport { sumAssistantUsage } from \"../../../core/agent-session-stats.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { formatTokens } from \"../../../core/format-tokens.js\";\nimport { type StartupProgress, startupProgress } from \"../../../core/startup-progress.js\";\nimport { taskStore } from \"../../../core/task-store.js\";\nimport { BRAND_MARK, GIT_BRANCH_GLYPH } from \"../brand.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Assemble one footer line: `left` flush left, `right` flush right when it fits\n * (≥2 cols between), padded to the full width. When it doesn't fit, drop `right`\n * and pad; when even `left` overflows, truncate it. Width math runs on the plain\n * strings; the styled strings carry the colour. Every returned line is exactly\n * `width` cells or fewer — the invariant the footer-width tests hold us to.\n */\nfunction assembleLine(width: number, leftPlain: string, leftStyled: string, rightPlain = \"\", rightStyled = \"\"): string {\n\tconst lw = visibleWidth(leftPlain);\n\tif (rightPlain && lw + 2 + visibleWidth(rightPlain) <= width) {\n\t\treturn leftStyled + \" \".repeat(width - lw - visibleWidth(rightPlain)) + rightStyled;\n\t}\n\tif (lw <= width) return leftStyled + \" \".repeat(width - lw);\n\treturn truncateToWidth(leftStyled, width, theme.fg(\"dim\", \"…\"));\n}\n\n/** A compact context-fill gauge, coloured by proximity to the auto-compact trip point. */\nfunction contextGauge(percent: number, errorLevel: number, warnLevel: number): { plain: string; styled: string } {\n\tconst CELLS = 8;\n\tconst filled = Math.max(0, Math.min(CELLS, Math.round((percent / 100) * CELLS)));\n\tconst fill = \"▰\".repeat(filled);\n\tconst track = \"▱\".repeat(CELLS - filled);\n\tconst color = percent >= errorLevel ? \"error\" : percent >= warnLevel ? \"warning\" : \"accent\";\n\treturn { plain: fill + track, styled: theme.fg(color, fill) + theme.fg(\"dim\", track) };\n}\n\n/** Count subagent runs currently in flight, for the footer's live delegation cue. */\nfunction activeSubagentCount(): number {\n\treturn taskStore.list().filter((t) => t.source === \"subagent\" && t.status === \"in_progress\").length;\n}\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/** Cells in a startup-progress bar; compact so several tools fit the footer. */\nconst STARTUP_BAR_CELLS = 12;\n\nfunction formatMb(bytes: number): string {\n\treturn `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\n/**\n * One footer line for a transient startup-progress entry (tool download or\n * index build), styled like the voice download bar: a `·` fill over a dim\n * track with percent and a `received / total` (or `done/total`) detail. An\n * indeterminate download (no Content-Length) drops the bar for a running byte\n * count; an error entry renders as a dim message. Returns a styled string; the\n * caller width-clamps it.\n */\nfunction renderStartupLine(entry: StartupProgress): string {\n\tif (entry.kind === \"error\") {\n\t\treturn theme.fg(\"dim\", `${entry.label}: ${entry.message}`);\n\t}\n\tconst label = theme.fg(\"text\", entry.label);\n\tif (entry.kind === \"download\") {\n\t\tif (entry.totalBytes === null || entry.totalBytes <= 0) {\n\t\t\treturn `${label} ${theme.fg(\"dim\", `${formatMb(entry.receivedBytes)}…`)}`;\n\t\t}\n\t\tconst detail = `${formatMb(entry.receivedBytes)} / ${formatMb(entry.totalBytes)}`;\n\t\treturn `${label} ${determinateBar(entry.receivedBytes / entry.totalBytes, detail)}`;\n\t}\n\tconst detail = `${entry.done}/${entry.total} ${entry.unit}`;\n\tconst ratio = entry.total > 0 ? entry.done / entry.total : 0;\n\treturn `${label} ${determinateBar(ratio, detail)}`;\n}\n\n/** `·`-fill bar + percent + trailing detail, matching the voice download bar. */\nfunction determinateBar(ratio: number, detail: string): string {\n\tconst clamped = Math.max(0, Math.min(1, ratio));\n\tconst filled = Math.round(clamped * STARTUP_BAR_CELLS);\n\tconst bar = theme.fg(\"accent\", \"·\".repeat(filled)) + theme.fg(\"dim\", \"·\".repeat(STARTUP_BAR_CELLS - filled));\n\tconst pct = `${Math.round(clamped * 100)}%`;\n\treturn `${bar} ${theme.fg(\"muted\", pct)} ${theme.fg(\"dim\", `· ${detail}`)}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Cumulative usage across ALL session entries (not just post-compaction\n\t\t// messages). Shares sumAssistantUsage with the per-request cost line the\n\t\t// transcript prints at agent_end, so the footer total and that line are always\n\t\t// derived from the same accounting.\n\t\tconst {\n\t\t\tinput: totalInput,\n\t\t\toutput: totalOutput,\n\t\t\tcacheRead: totalCacheRead,\n\t\t\tcacheWrite: totalCacheWrite,\n\t\t\tcost: totalCost,\n\t\t} = sumAssistantUsage(this.session.sessionManager.getEntries());\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = this.session.sessionManager.getCwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tconst modeLabel = this.footerData.getActiveMode();\n\n\t\t// ── Line 1 — identity & location ────────────────────────────────────────\n\t\t// Lead with the brand mark + MODE (the agent's guardrail: Ask/Plan/Build/\n\t\t// Debug) in bold accent so it is the first thing the eye lands on, then the\n\t\t// path, git branch, and session name in descending emphasis. The live\n\t\t// subagent count sits flush right — present only while work is delegated.\n\t\tconst modeUp = modeLabel.toUpperCase();\n\t\tconst brand = `${BRAND_MARK} ${modeUp}`;\n\t\tlet l1Plain = `${brand} ${pwd}`;\n\t\tlet l1Styled = `${theme.bold(theme.fg(\"accent\", brand))} ${theme.fg(\"muted\", pwd)}`;\n\t\tif (branch) {\n\t\t\tl1Plain += ` ${GIT_BRANCH_GLYPH} ${branch}`;\n\t\t\tl1Styled += ` ${theme.fg(\"dim\", GIT_BRANCH_GLYPH)} ${theme.fg(\"muted\", branch)}`;\n\t\t}\n\t\tif (sessionName) {\n\t\t\tl1Plain += ` • ${sessionName}`;\n\t\t\tl1Styled += theme.fg(\"dim\", ` • ${sessionName}`);\n\t\t}\n\t\tconst nSub = activeSubagentCount();\n\t\tconst l1RightPlain = nSub > 0 ? `◇${nSub} running` : \"\";\n\t\tconst l1RightStyled = nSub > 0 ? theme.fg(\"accent\", `◇${nSub}`) + theme.fg(\"dim\", \" running\") : \"\";\n\t\tconst line1 = assembleLine(width, l1Plain, l1Styled, l1RightPlain, l1RightStyled);\n\n\t\t// ── Line 2 — session vitals ─────────────────────────────────────────────\n\t\t// A context-fill gauge (coloured by proximity to the auto-compact trip\n\t\t// point) leads, then token/cost deltas, with the model + thinking level\n\t\t// flush right. Numbers read in muted, labels/arrows in dim — a legible\n\t\t// hierarchy in place of the old uniform grey.\n\t\tlet thresholdPercent: number | undefined;\n\t\tif (this.autoCompactEnabled && contextWindow > 0) {\n\t\t\tconst reserveTokens = this.session.settingsManager.getCompactionSettings().reserveTokens;\n\t\t\tconst effective = contextWindow - reserveTokens;\n\t\t\tif (effective > 0) thresholdPercent = (effective / contextWindow) * 100;\n\t\t}\n\t\tconst errorLevel = thresholdPercent !== undefined ? thresholdPercent - 3 : 90;\n\t\tconst warnLevel = thresholdPercent !== undefined ? thresholdPercent - 10 : 70;\n\t\tconst autoIndicator =\n\t\t\tthresholdPercent !== undefined\n\t\t\t\t? ` auto@${thresholdPercent.toFixed(0)}%`\n\t\t\t\t: this.autoCompactEnabled\n\t\t\t\t\t? \" auto\"\n\t\t\t\t\t: \"\";\n\n\t\tconst gauge = contextGauge(contextPercentValue, errorLevel, warnLevel);\n\t\tconst pctText = contextPercent === \"?\" ? \"?\" : `${contextPercent}%`;\n\t\tconst pctColor =\n\t\t\tcontextPercentValue >= errorLevel ? \"error\" : contextPercentValue >= warnLevel ? \"warning\" : \"muted\";\n\t\tconst winText = `${formatTokens(contextWindow)}${autoIndicator}`;\n\n\t\tconst segs: Array<{ plain: string; styled: string }> = [\n\t\t\t{\n\t\t\t\tplain: `${gauge.plain} ${pctText} ${winText}`,\n\t\t\t\tstyled: `${gauge.styled} ${theme.fg(pctColor, pctText)} ${theme.fg(\"dim\", winText)}`,\n\t\t\t},\n\t\t];\n\t\tconst arrow = (a: string, n: number) => ({\n\t\t\tplain: `${a}${formatTokens(n)}`,\n\t\t\tstyled: theme.fg(\"dim\", a) + theme.fg(\"muted\", formatTokens(n)),\n\t\t});\n\t\tif (totalInput) segs.push(arrow(\"↑\", totalInput));\n\t\tif (totalOutput) segs.push(arrow(\"↓\", totalOutput));\n\t\tif (totalCacheRead) segs.push(arrow(\"R\", totalCacheRead));\n\t\tif (totalCacheWrite) segs.push(arrow(\"W\", totalCacheWrite));\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tsegs.push({ plain: costStr, styled: theme.fg(\"muted\", costStr) });\n\t\t}\n\t\tconst l2Plain = segs.map((s) => s.plain).join(\" \");\n\t\tconst l2Styled = segs.map((s) => s.styled).join(\" \");\n\n\t\t// Right: model, thinking level, and provider (when several are configured).\n\t\tconst modelName = state.model?.id || \"no-model\";\n\t\tlet r2Plain = modelName;\n\t\tlet r2Styled = theme.fg(\"muted\", modelName);\n\t\tif (state.model?.reasoning) {\n\t\t\tconst tl = state.thinkingLevel || \"off\";\n\t\t\tconst tstr = tl === \"off\" ? \"thinking off\" : tl;\n\t\t\tr2Plain += ` • ${tstr}`;\n\t\t\tr2Styled += theme.fg(\"dim\", ` • ${tstr}`);\n\t\t}\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t// Prepend the provider only when the whole right cluster still fits.\n\t\t\tconst withProv = `(${state.model.provider}) ${r2Plain}`;\n\t\t\tif (visibleWidth(l2Plain) + 2 + visibleWidth(withProv) <= width) {\n\t\t\t\tr2Plain = withProv;\n\t\t\t\tr2Styled = theme.fg(\"dim\", `(${state.model.provider}) `) + r2Styled;\n\t\t\t}\n\t\t}\n\t\tconst line2 = assembleLine(width, l2Plain, l2Styled, r2Plain, r2Styled);\n\n\t\tconst lines = [line1, line2];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\t// Transient startup progress (first-run tool downloads, index build): one\n\t\t// determinate bar per entry, cleared as each settles. Width-clamped like the\n\t\t// status line so the footer never overflows.\n\t\tfor (const entry of startupProgress.list()) {\n\t\t\tlines.push(truncateToWidth(renderStartupLine(entry), width, theme.fg(\"dim\", \"…\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
@@ -51,6 +51,7 @@ export interface SettingsConfig {
51
51
  doubleEscapeAction: "fork" | "tree" | "none";
52
52
  treeFilterMode: "default" | "no-tools" | "user-only" | "labeled-only" | "all";
53
53
  showHardwareCursor: boolean;
54
+ editorBorder: "rule" | "box";
54
55
  editorPaddingX: number;
55
56
  autocompleteMaxVisible: number;
56
57
  quietStartup: boolean;
@@ -86,6 +87,7 @@ export interface SettingsCallbacks {
86
87
  onDoubleEscapeActionChange: (action: "fork" | "tree" | "none") => void;
87
88
  onTreeFilterModeChange: (mode: "default" | "no-tools" | "user-only" | "labeled-only" | "all") => void;
88
89
  onShowHardwareCursorChange: (enabled: boolean) => void;
90
+ onEditorBorderChange: (border: "rule" | "box") => void;
89
91
  onEditorPaddingXChange: (padding: number) => void;
90
92
  onAutocompleteMaxVisibleChange: (maxVisible: number) => void;
91
93
  onQuietStartupChange: (enabled: boolean) => void;