@kolisachint/hoocode-agent 0.4.49 → 0.4.51
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.
- package/CHANGELOG.md +43 -1
- package/dist/core/keybindings.d.ts +2 -2
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +5 -2
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/task-store.d.ts +7 -1
- package/dist/core/task-store.d.ts.map +1 -1
- package/dist/core/task-store.js +12 -7
- package/dist/core/task-store.js.map +1 -1
- package/dist/core/team-view.d.ts +10 -3
- package/dist/core/team-view.d.ts.map +1 -1
- package/dist/core/team-view.js +22 -6
- package/dist/core/team-view.js.map +1 -1
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +4 -2
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +12 -7
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +16 -7
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +181 -52
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.51] - 2026-06-11
|
|
4
|
+
### Fixed
|
|
5
|
+
|
|
6
|
+
- `--team` no longer corrupts the interactive TUI. Team-view warnings went to
|
|
7
|
+
raw stderr while the TUI owned the screen, scribbling over the render until
|
|
8
|
+
the editor stopped echoing input; an `/events` endpoint that answered 200
|
|
9
|
+
but closed without streaming repeated that warning every 5s retry. Warnings
|
|
10
|
+
now surface in the chat, and a reconnect only counts as recovered once the
|
|
11
|
+
stream actually delivers data.
|
|
12
|
+
- Idle team roles no longer pin the task pane at "◐ WORKING". The mirror gave
|
|
13
|
+
every role a `pending` task that never finished, so the pane never collapsed
|
|
14
|
+
and task numbering never reset. Roles now get a task row only while they are
|
|
15
|
+
actually doing something (or failed); an all-idle team leaves the pane
|
|
16
|
+
collapsed.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Default key for `app.tasks.cycleView` is now `alt+t` (with `shift+ctrl+t`
|
|
21
|
+
kept as an alias). Windows Terminal intercepts `ctrl+shift+t` as its own
|
|
22
|
+
"new tab" shortcut, so the old default never reached the app there.
|
|
23
|
+
|
|
24
|
+
## [0.4.50] - 2026-06-11
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Task panel: the `teams` view is now a real lens distinct from `subagents`
|
|
29
|
+
instead of an alias for the same grouped path. `subagents` filters out
|
|
30
|
+
`kind: "role"` agents and `teams` filters to only role agents; non-matching
|
|
31
|
+
agents and their tasks no longer leak between lenses. The `teams` view also
|
|
32
|
+
renders forward-handoff connectors (`└──→ next`) between successive role
|
|
33
|
+
agents and keeps a placeholder header for queued roles with no tasks yet.
|
|
34
|
+
- Task store: `update()` and `addAgentStats()` now log a `console.warn` when
|
|
35
|
+
called with an unknown task or agent id. Previously straggler completions
|
|
36
|
+
(late MCP/SSE events whose owner had been reaped) silently no-opped, hiding
|
|
37
|
+
observability data.
|
|
38
|
+
- Task store: `reset()` now preserves agents with non-zero accumulated stats
|
|
39
|
+
across user turns. Previously an agent that finished all of its tasks before
|
|
40
|
+
the next user message would be wiped along with its cross-turn cost
|
|
41
|
+
accounting; subsequent re-dispatches restarted from zero.
|
|
42
|
+
- Task panel: animation timer is now guarded against post-`dispose()` revival.
|
|
43
|
+
A `disposed` flag short-circuits both `render()` and `ensureAnimation()` so a
|
|
44
|
+
late render call cannot resurrect the spinner interval after teardown.
|
|
45
|
+
|
|
3
46
|
## [0.4.49] - 2026-06-11
|
|
4
47
|
|
|
5
48
|
## [0.4.48] - 2026-06-11
|
|
@@ -10,7 +53,6 @@
|
|
|
10
53
|
agents and the single `GET /events` SSE stream maps TeamEvents onto live
|
|
11
54
|
task/agent patches. Connection failures and drops log a warning and never
|
|
12
55
|
block the main agent.
|
|
13
|
-
=======
|
|
14
56
|
|
|
15
57
|
## [0.4.47] - 2026-06-11
|
|
16
58
|
|
|
@@ -214,8 +214,8 @@ export declare const KEYBINDINGS: {
|
|
|
214
214
|
readonly description: "Toggle thinking blocks";
|
|
215
215
|
};
|
|
216
216
|
readonly "app.tasks.cycleView": {
|
|
217
|
-
readonly defaultKeys: "
|
|
218
|
-
readonly description: "Cycle task panel view (tasks → subagents → teams)";
|
|
217
|
+
readonly defaultKeys: "ctrl+n";
|
|
218
|
+
readonly description: "Cycle task panel view (tasks → subagents → teams, skips empty lenses)";
|
|
219
219
|
};
|
|
220
220
|
readonly "app.session.toggleNamedFilter": {
|
|
221
221
|
readonly defaultKeys: "ctrl+n";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keybindings.d.ts","sourceRoot":"","sources":["../../src/core/keybindings.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype Keybinding,\n\ttype KeybindingDefinitions,\n\ttype KeybindingsConfig,\n\ttype KeyId,\n\tTUI_KEYBINDINGS,\n\tKeybindingsManager as TuiKeybindingsManager,\n} from \"@kolisachint/hoocode-tui\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\nexport interface AppKeybindings {\n\t\"app.interrupt\": true;\n\t\"app.clear\": true;\n\t\"app.exit\": true;\n\t\"app.suspend\": true;\n\t\"app.thinking.cycle\": true;\n\t\"app.model.cycleForward\": true;\n\t\"app.model.cycleBackward\": true;\n\t\"app.model.select\": true;\n\t\"app.tools.expand\": true;\n\t\"app.thinking.toggle\": true;\n\t\"app.tasks.cycleView\": true;\n\t\"app.session.toggleNamedFilter\": true;\n\t\"app.editor.external\": true;\n\t\"app.message.followUp\": true;\n\t\"app.message.dequeue\": true;\n\t\"app.clipboard.pasteImage\": true;\n\t\"app.session.new\": true;\n\t\"app.session.tree\": true;\n\t\"app.session.fork\": true;\n\t\"app.session.resume\": true;\n\t\"app.tree.foldOrUp\": true;\n\t\"app.tree.unfoldOrDown\": true;\n\t\"app.tree.editLabel\": true;\n\t\"app.tree.toggleLabelTimestamp\": true;\n\t\"app.session.togglePath\": true;\n\t\"app.session.toggleSort\": true;\n\t\"app.session.rename\": true;\n\t\"app.session.delete\": true;\n\t\"app.session.deleteNoninvasive\": true;\n\t\"app.models.save\": true;\n\t\"app.models.enableAll\": true;\n\t\"app.models.clearAll\": true;\n\t\"app.models.toggleProvider\": true;\n\t\"app.models.reorderUp\": true;\n\t\"app.models.reorderDown\": true;\n\t\"app.tree.filter.default\": true;\n\t\"app.tree.filter.noTools\": true;\n\t\"app.tree.filter.userOnly\": true;\n\t\"app.tree.filter.labeledOnly\": true;\n\t\"app.tree.filter.all\": true;\n\t\"app.tree.filter.cycleForward\": true;\n\t\"app.tree.filter.cycleBackward\": true;\n}\n\nexport type AppKeybinding = keyof AppKeybindings;\n\ndeclare module \"@kolisachint/hoocode-tui\" {\n\tinterface Keybindings extends AppKeybindings {}\n}\n\nexport const KEYBINDINGS = {\n\t...TUI_KEYBINDINGS,\n\t\"app.interrupt\": { defaultKeys: \"escape\", description: \"Cancel or abort\" },\n\t\"app.clear\": { defaultKeys: \"ctrl+c\", description: \"Clear editor\" },\n\t\"app.exit\": { defaultKeys: \"ctrl+d\", description: \"Exit when editor is empty\" },\n\t\"app.suspend\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? [] : \"ctrl+z\",\n\t\tdescription: \"Suspend to background\",\n\t},\n\t\"app.thinking.cycle\": {\n\t\tdefaultKeys: \"shift+tab\",\n\t\tdescription: \"Cycle thinking level\",\n\t},\n\t\"app.model.cycleForward\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Cycle to next model\",\n\t},\n\t\"app.model.cycleBackward\": {\n\t\tdefaultKeys: \"shift+ctrl+p\",\n\t\tdescription: \"Cycle to previous model\",\n\t},\n\t\"app.model.select\": { defaultKeys: \"ctrl+l\", description: \"Open model selector\" },\n\t\"app.tools.expand\": { defaultKeys: \"ctrl+o\", description: \"Toggle tool output\" },\n\t\"app.thinking.toggle\": {\n\t\tdefaultKeys: \"ctrl+t\",\n\t\tdescription: \"Toggle thinking blocks\",\n\t},\n\t\"app.tasks.cycleView\": {\n\t\tdefaultKeys: \"shift+ctrl+t\",\n\t\tdescription: \"Cycle task panel view (tasks → subagents → teams)\",\n\t},\n\t\"app.session.toggleNamedFilter\": {\n\t\tdefaultKeys: \"ctrl+n\",\n\t\tdescription: \"Toggle named session filter\",\n\t},\n\t\"app.editor.external\": {\n\t\tdefaultKeys: \"ctrl+g\",\n\t\tdescription: \"Open external editor\",\n\t},\n\t\"app.message.followUp\": {\n\t\tdefaultKeys: \"alt+enter\",\n\t\tdescription: \"Queue follow-up message\",\n\t},\n\t\"app.message.dequeue\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Restore queued messages\",\n\t},\n\t\"app.clipboard.pasteImage\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? \"alt+v\" : \"ctrl+v\",\n\t\tdescription: \"Paste image from clipboard\",\n\t},\n\t\"app.session.new\": { defaultKeys: [], description: \"Start a new session\" },\n\t\"app.session.tree\": { defaultKeys: [], description: \"Open session tree\" },\n\t\"app.session.fork\": { defaultKeys: [], description: \"Fork current session\" },\n\t\"app.session.resume\": { defaultKeys: [], description: \"Resume a session\" },\n\t\"app.tree.foldOrUp\": {\n\t\tdefaultKeys: [\"ctrl+left\", \"alt+left\"],\n\t\tdescription: \"Fold tree branch or move up\",\n\t},\n\t\"app.tree.unfoldOrDown\": {\n\t\tdefaultKeys: [\"ctrl+right\", \"alt+right\"],\n\t\tdescription: \"Unfold tree branch or move down\",\n\t},\n\t\"app.tree.editLabel\": {\n\t\tdefaultKeys: \"shift+l\",\n\t\tdescription: \"Edit tree label\",\n\t},\n\t\"app.tree.toggleLabelTimestamp\": {\n\t\tdefaultKeys: \"shift+t\",\n\t\tdescription: \"Toggle tree label timestamps\",\n\t},\n\t\"app.session.togglePath\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Toggle session path display\",\n\t},\n\t\"app.session.toggleSort\": {\n\t\tdefaultKeys: \"ctrl+s\",\n\t\tdescription: \"Toggle session sort mode\",\n\t},\n\t\"app.session.rename\": {\n\t\tdefaultKeys: \"ctrl+r\",\n\t\tdescription: \"Rename session\",\n\t},\n\t\"app.session.delete\": {\n\t\tdefaultKeys: \"ctrl+d\",\n\t\tdescription: \"Delete session\",\n\t},\n\t\"app.session.deleteNoninvasive\": {\n\t\tdefaultKeys: \"ctrl+backspace\",\n\t\tdescription: \"Delete session when query is empty\",\n\t},\n\t\"app.models.save\": {\n\t\tdefaultKeys: \"ctrl+s\",\n\t\tdescription: \"Save model selection\",\n\t},\n\t\"app.models.enableAll\": {\n\t\tdefaultKeys: \"ctrl+a\",\n\t\tdescription: \"Enable all models\",\n\t},\n\t\"app.models.clearAll\": {\n\t\tdefaultKeys: \"ctrl+x\",\n\t\tdescription: \"Clear all models\",\n\t},\n\t\"app.models.toggleProvider\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Toggle all models for provider\",\n\t},\n\t\"app.models.reorderUp\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Move model up in order\",\n\t},\n\t\"app.models.reorderDown\": {\n\t\tdefaultKeys: \"alt+down\",\n\t\tdescription: \"Move model down in order\",\n\t},\n\t\"app.options.next\": { defaultKeys: \"right\", description: \"Confirm and advance to the next question\" },\n\t\"app.options.back\": { defaultKeys: \"left\", description: \"Go back to the previous question\" },\n\t\"app.tree.filter.default\": {\n\t\tdefaultKeys: \"ctrl+d\",\n\t\tdescription: \"Tree filter: default view\",\n\t},\n\t\"app.tree.filter.noTools\": {\n\t\tdefaultKeys: \"ctrl+t\",\n\t\tdescription: \"Tree filter: hide tool results\",\n\t},\n\t\"app.tree.filter.userOnly\": {\n\t\tdefaultKeys: \"ctrl+u\",\n\t\tdescription: \"Tree filter: user messages only\",\n\t},\n\t\"app.tree.filter.labeledOnly\": {\n\t\tdefaultKeys: \"ctrl+l\",\n\t\tdescription: \"Tree filter: labeled entries only\",\n\t},\n\t\"app.tree.filter.all\": {\n\t\tdefaultKeys: \"ctrl+a\",\n\t\tdescription: \"Tree filter: show all entries\",\n\t},\n\t\"app.tree.filter.cycleForward\": {\n\t\tdefaultKeys: \"ctrl+o\",\n\t\tdescription: \"Tree filter: cycle forward\",\n\t},\n\t\"app.tree.filter.cycleBackward\": {\n\t\tdefaultKeys: \"shift+ctrl+o\",\n\t\tdescription: \"Tree filter: cycle backward\",\n\t},\n} as const satisfies KeybindingDefinitions;\n\nconst KEYBINDING_NAME_MIGRATIONS = {\n\tcursorUp: \"tui.editor.cursorUp\",\n\tcursorDown: \"tui.editor.cursorDown\",\n\tcursorLeft: \"tui.editor.cursorLeft\",\n\tcursorRight: \"tui.editor.cursorRight\",\n\tcursorWordLeft: \"tui.editor.cursorWordLeft\",\n\tcursorWordRight: \"tui.editor.cursorWordRight\",\n\tcursorLineStart: \"tui.editor.cursorLineStart\",\n\tcursorLineEnd: \"tui.editor.cursorLineEnd\",\n\tjumpForward: \"tui.editor.jumpForward\",\n\tjumpBackward: \"tui.editor.jumpBackward\",\n\tpageUp: \"tui.editor.pageUp\",\n\tpageDown: \"tui.editor.pageDown\",\n\tdeleteCharBackward: \"tui.editor.deleteCharBackward\",\n\tdeleteCharForward: \"tui.editor.deleteCharForward\",\n\tdeleteWordBackward: \"tui.editor.deleteWordBackward\",\n\tdeleteWordForward: \"tui.editor.deleteWordForward\",\n\tdeleteToLineStart: \"tui.editor.deleteToLineStart\",\n\tdeleteToLineEnd: \"tui.editor.deleteToLineEnd\",\n\tyank: \"tui.editor.yank\",\n\tyankPop: \"tui.editor.yankPop\",\n\tundo: \"tui.editor.undo\",\n\tnewLine: \"tui.input.newLine\",\n\tsubmit: \"tui.input.submit\",\n\ttab: \"tui.input.tab\",\n\tcopy: \"tui.input.copy\",\n\tselectUp: \"tui.select.up\",\n\tselectDown: \"tui.select.down\",\n\tselectPageUp: \"tui.select.pageUp\",\n\tselectPageDown: \"tui.select.pageDown\",\n\tselectConfirm: \"tui.select.confirm\",\n\tselectCancel: \"tui.select.cancel\",\n\tinterrupt: \"app.interrupt\",\n\tclear: \"app.clear\",\n\texit: \"app.exit\",\n\tsuspend: \"app.suspend\",\n\tcycleThinkingLevel: \"app.thinking.cycle\",\n\tcycleModelForward: \"app.model.cycleForward\",\n\tcycleModelBackward: \"app.model.cycleBackward\",\n\tselectModel: \"app.model.select\",\n\texpandTools: \"app.tools.expand\",\n\ttoggleThinking: \"app.thinking.toggle\",\n\ttoggleSessionNamedFilter: \"app.session.toggleNamedFilter\",\n\texternalEditor: \"app.editor.external\",\n\tfollowUp: \"app.message.followUp\",\n\tdequeue: \"app.message.dequeue\",\n\tpasteImage: \"app.clipboard.pasteImage\",\n\tnewSession: \"app.session.new\",\n\ttree: \"app.session.tree\",\n\tfork: \"app.session.fork\",\n\tresume: \"app.session.resume\",\n\ttreeFoldOrUp: \"app.tree.foldOrUp\",\n\ttreeUnfoldOrDown: \"app.tree.unfoldOrDown\",\n\ttreeEditLabel: \"app.tree.editLabel\",\n\ttreeToggleLabelTimestamp: \"app.tree.toggleLabelTimestamp\",\n\ttoggleSessionPath: \"app.session.togglePath\",\n\ttoggleSessionSort: \"app.session.toggleSort\",\n\trenameSession: \"app.session.rename\",\n\tdeleteSession: \"app.session.delete\",\n\tdeleteSessionNoninvasive: \"app.session.deleteNoninvasive\",\n} as const satisfies Record<string, Keybinding>;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isLegacyKeybindingName(key: string): key is keyof typeof KEYBINDING_NAME_MIGRATIONS {\n\treturn key in KEYBINDING_NAME_MIGRATIONS;\n}\n\nfunction toKeybindingsConfig(value: unknown): KeybindingsConfig {\n\tif (!isRecord(value)) return {};\n\n\tconst config: KeybindingsConfig = {};\n\tfor (const [key, binding] of Object.entries(value)) {\n\t\tif (typeof binding === \"string\") {\n\t\t\tconfig[key] = binding as KeyId;\n\t\t\tcontinue;\n\t\t}\n\t\tif (Array.isArray(binding) && binding.every((entry) => typeof entry === \"string\")) {\n\t\t\tconfig[key] = binding as KeyId[];\n\t\t}\n\t}\n\treturn config;\n}\n\nexport function migrateKeybindingsConfig(rawConfig: Record<string, unknown>): {\n\tconfig: Record<string, unknown>;\n\tmigrated: boolean;\n} {\n\tconst config: Record<string, unknown> = {};\n\tlet migrated = false;\n\n\tfor (const [key, value] of Object.entries(rawConfig)) {\n\t\tconst nextKey = isLegacyKeybindingName(key) ? KEYBINDING_NAME_MIGRATIONS[key] : key;\n\t\tif (nextKey !== key) {\n\t\t\tmigrated = true;\n\t\t}\n\t\tif (key !== nextKey && Object.hasOwn(rawConfig, nextKey)) {\n\t\t\tmigrated = true;\n\t\t\tcontinue;\n\t\t}\n\t\tconfig[nextKey] = value;\n\t}\n\n\treturn { config: orderKeybindingsConfig(config), migrated };\n}\n\nfunction orderKeybindingsConfig(config: Record<string, unknown>): Record<string, unknown> {\n\tconst ordered: Record<string, unknown> = {};\n\tfor (const keybinding of Object.keys(KEYBINDINGS)) {\n\t\tif (Object.hasOwn(config, keybinding)) {\n\t\t\tordered[keybinding] = config[keybinding];\n\t\t}\n\t}\n\n\tconst extras = Object.keys(config)\n\t\t.filter((key) => !Object.hasOwn(ordered, key))\n\t\t.sort();\n\tfor (const key of extras) {\n\t\tordered[key] = config[key];\n\t}\n\n\treturn ordered;\n}\n\nfunction loadRawConfig(path: string): Record<string, unknown> | undefined {\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as unknown;\n\t\treturn isRecord(parsed) ? parsed : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport class KeybindingsManager extends TuiKeybindingsManager {\n\tprivate configPath: string | undefined;\n\n\tconstructor(userBindings: KeybindingsConfig = {}, configPath?: string) {\n\t\tsuper(KEYBINDINGS, userBindings);\n\t\tthis.configPath = configPath;\n\t}\n\n\tstatic create(agentDir: string = getAgentDir()): KeybindingsManager {\n\t\tconst configPath = join(agentDir, \"keybindings.json\");\n\t\tconst userBindings = KeybindingsManager.loadFromFile(configPath);\n\t\treturn new KeybindingsManager(userBindings, configPath);\n\t}\n\n\treload(): void {\n\t\tif (!this.configPath) return;\n\t\tthis.setUserBindings(KeybindingsManager.loadFromFile(this.configPath));\n\t}\n\n\tgetEffectiveConfig(): KeybindingsConfig {\n\t\treturn this.getResolvedBindings();\n\t}\n\n\tprivate static loadFromFile(path: string): KeybindingsConfig {\n\t\tconst rawConfig = loadRawConfig(path);\n\t\tif (!rawConfig) return {};\n\t\treturn toKeybindingsConfig(migrateKeybindingsConfig(rawConfig).config);\n\t}\n}\n\nexport type { Keybinding, KeyId, KeybindingsConfig };\n"]}
|
|
1
|
+
{"version":3,"file":"keybindings.d.ts","sourceRoot":"","sources":["../../src/core/keybindings.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype Keybinding,\n\ttype KeybindingDefinitions,\n\ttype KeybindingsConfig,\n\ttype KeyId,\n\tTUI_KEYBINDINGS,\n\tKeybindingsManager as TuiKeybindingsManager,\n} from \"@kolisachint/hoocode-tui\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\nexport interface AppKeybindings {\n\t\"app.interrupt\": true;\n\t\"app.clear\": true;\n\t\"app.exit\": true;\n\t\"app.suspend\": true;\n\t\"app.thinking.cycle\": true;\n\t\"app.model.cycleForward\": true;\n\t\"app.model.cycleBackward\": true;\n\t\"app.model.select\": true;\n\t\"app.tools.expand\": true;\n\t\"app.thinking.toggle\": true;\n\t\"app.tasks.cycleView\": true;\n\t\"app.session.toggleNamedFilter\": true;\n\t\"app.editor.external\": true;\n\t\"app.message.followUp\": true;\n\t\"app.message.dequeue\": true;\n\t\"app.clipboard.pasteImage\": true;\n\t\"app.session.new\": true;\n\t\"app.session.tree\": true;\n\t\"app.session.fork\": true;\n\t\"app.session.resume\": true;\n\t\"app.tree.foldOrUp\": true;\n\t\"app.tree.unfoldOrDown\": true;\n\t\"app.tree.editLabel\": true;\n\t\"app.tree.toggleLabelTimestamp\": true;\n\t\"app.session.togglePath\": true;\n\t\"app.session.toggleSort\": true;\n\t\"app.session.rename\": true;\n\t\"app.session.delete\": true;\n\t\"app.session.deleteNoninvasive\": true;\n\t\"app.models.save\": true;\n\t\"app.models.enableAll\": true;\n\t\"app.models.clearAll\": true;\n\t\"app.models.toggleProvider\": true;\n\t\"app.models.reorderUp\": true;\n\t\"app.models.reorderDown\": true;\n\t\"app.tree.filter.default\": true;\n\t\"app.tree.filter.noTools\": true;\n\t\"app.tree.filter.userOnly\": true;\n\t\"app.tree.filter.labeledOnly\": true;\n\t\"app.tree.filter.all\": true;\n\t\"app.tree.filter.cycleForward\": true;\n\t\"app.tree.filter.cycleBackward\": true;\n}\n\nexport type AppKeybinding = keyof AppKeybindings;\n\ndeclare module \"@kolisachint/hoocode-tui\" {\n\tinterface Keybindings extends AppKeybindings {}\n}\n\nexport const KEYBINDINGS = {\n\t...TUI_KEYBINDINGS,\n\t\"app.interrupt\": { defaultKeys: \"escape\", description: \"Cancel or abort\" },\n\t\"app.clear\": { defaultKeys: \"ctrl+c\", description: \"Clear editor\" },\n\t\"app.exit\": { defaultKeys: \"ctrl+d\", description: \"Exit when editor is empty\" },\n\t\"app.suspend\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? [] : \"ctrl+z\",\n\t\tdescription: \"Suspend to background\",\n\t},\n\t\"app.thinking.cycle\": {\n\t\tdefaultKeys: \"shift+tab\",\n\t\tdescription: \"Cycle thinking level\",\n\t},\n\t\"app.model.cycleForward\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Cycle to next model\",\n\t},\n\t\"app.model.cycleBackward\": {\n\t\tdefaultKeys: \"shift+ctrl+p\",\n\t\tdescription: \"Cycle to previous model\",\n\t},\n\t\"app.model.select\": { defaultKeys: \"ctrl+l\", description: \"Open model selector\" },\n\t\"app.tools.expand\": { defaultKeys: \"ctrl+o\", description: \"Toggle tool output\" },\n\t\"app.thinking.toggle\": {\n\t\tdefaultKeys: \"ctrl+t\",\n\t\tdescription: \"Toggle thinking blocks\",\n\t},\n\t\"app.tasks.cycleView\": {\n\t\t// ctrl+n is free in the main editor (no emacs next-line binding here);\n\t\t// app.session.toggleNamedFilter also uses ctrl+n but only inside the\n\t\t// session-selector overlay, which captures keys while open.\n\t\tdefaultKeys: \"ctrl+n\",\n\t\tdescription: \"Cycle task panel view (tasks → subagents → teams, skips empty lenses)\",\n\t},\n\t\"app.session.toggleNamedFilter\": {\n\t\tdefaultKeys: \"ctrl+n\",\n\t\tdescription: \"Toggle named session filter\",\n\t},\n\t\"app.editor.external\": {\n\t\tdefaultKeys: \"ctrl+g\",\n\t\tdescription: \"Open external editor\",\n\t},\n\t\"app.message.followUp\": {\n\t\tdefaultKeys: \"alt+enter\",\n\t\tdescription: \"Queue follow-up message\",\n\t},\n\t\"app.message.dequeue\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Restore queued messages\",\n\t},\n\t\"app.clipboard.pasteImage\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? \"alt+v\" : \"ctrl+v\",\n\t\tdescription: \"Paste image from clipboard\",\n\t},\n\t\"app.session.new\": { defaultKeys: [], description: \"Start a new session\" },\n\t\"app.session.tree\": { defaultKeys: [], description: \"Open session tree\" },\n\t\"app.session.fork\": { defaultKeys: [], description: \"Fork current session\" },\n\t\"app.session.resume\": { defaultKeys: [], description: \"Resume a session\" },\n\t\"app.tree.foldOrUp\": {\n\t\tdefaultKeys: [\"ctrl+left\", \"alt+left\"],\n\t\tdescription: \"Fold tree branch or move up\",\n\t},\n\t\"app.tree.unfoldOrDown\": {\n\t\tdefaultKeys: [\"ctrl+right\", \"alt+right\"],\n\t\tdescription: \"Unfold tree branch or move down\",\n\t},\n\t\"app.tree.editLabel\": {\n\t\tdefaultKeys: \"shift+l\",\n\t\tdescription: \"Edit tree label\",\n\t},\n\t\"app.tree.toggleLabelTimestamp\": {\n\t\tdefaultKeys: \"shift+t\",\n\t\tdescription: \"Toggle tree label timestamps\",\n\t},\n\t\"app.session.togglePath\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Toggle session path display\",\n\t},\n\t\"app.session.toggleSort\": {\n\t\tdefaultKeys: \"ctrl+s\",\n\t\tdescription: \"Toggle session sort mode\",\n\t},\n\t\"app.session.rename\": {\n\t\tdefaultKeys: \"ctrl+r\",\n\t\tdescription: \"Rename session\",\n\t},\n\t\"app.session.delete\": {\n\t\tdefaultKeys: \"ctrl+d\",\n\t\tdescription: \"Delete session\",\n\t},\n\t\"app.session.deleteNoninvasive\": {\n\t\tdefaultKeys: \"ctrl+backspace\",\n\t\tdescription: \"Delete session when query is empty\",\n\t},\n\t\"app.models.save\": {\n\t\tdefaultKeys: \"ctrl+s\",\n\t\tdescription: \"Save model selection\",\n\t},\n\t\"app.models.enableAll\": {\n\t\tdefaultKeys: \"ctrl+a\",\n\t\tdescription: \"Enable all models\",\n\t},\n\t\"app.models.clearAll\": {\n\t\tdefaultKeys: \"ctrl+x\",\n\t\tdescription: \"Clear all models\",\n\t},\n\t\"app.models.toggleProvider\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Toggle all models for provider\",\n\t},\n\t\"app.models.reorderUp\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Move model up in order\",\n\t},\n\t\"app.models.reorderDown\": {\n\t\tdefaultKeys: \"alt+down\",\n\t\tdescription: \"Move model down in order\",\n\t},\n\t\"app.options.next\": { defaultKeys: \"right\", description: \"Confirm and advance to the next question\" },\n\t\"app.options.back\": { defaultKeys: \"left\", description: \"Go back to the previous question\" },\n\t\"app.tree.filter.default\": {\n\t\tdefaultKeys: \"ctrl+d\",\n\t\tdescription: \"Tree filter: default view\",\n\t},\n\t\"app.tree.filter.noTools\": {\n\t\tdefaultKeys: \"ctrl+t\",\n\t\tdescription: \"Tree filter: hide tool results\",\n\t},\n\t\"app.tree.filter.userOnly\": {\n\t\tdefaultKeys: \"ctrl+u\",\n\t\tdescription: \"Tree filter: user messages only\",\n\t},\n\t\"app.tree.filter.labeledOnly\": {\n\t\tdefaultKeys: \"ctrl+l\",\n\t\tdescription: \"Tree filter: labeled entries only\",\n\t},\n\t\"app.tree.filter.all\": {\n\t\tdefaultKeys: \"ctrl+a\",\n\t\tdescription: \"Tree filter: show all entries\",\n\t},\n\t\"app.tree.filter.cycleForward\": {\n\t\tdefaultKeys: \"ctrl+o\",\n\t\tdescription: \"Tree filter: cycle forward\",\n\t},\n\t\"app.tree.filter.cycleBackward\": {\n\t\tdefaultKeys: \"shift+ctrl+o\",\n\t\tdescription: \"Tree filter: cycle backward\",\n\t},\n} as const satisfies KeybindingDefinitions;\n\nconst KEYBINDING_NAME_MIGRATIONS = {\n\tcursorUp: \"tui.editor.cursorUp\",\n\tcursorDown: \"tui.editor.cursorDown\",\n\tcursorLeft: \"tui.editor.cursorLeft\",\n\tcursorRight: \"tui.editor.cursorRight\",\n\tcursorWordLeft: \"tui.editor.cursorWordLeft\",\n\tcursorWordRight: \"tui.editor.cursorWordRight\",\n\tcursorLineStart: \"tui.editor.cursorLineStart\",\n\tcursorLineEnd: \"tui.editor.cursorLineEnd\",\n\tjumpForward: \"tui.editor.jumpForward\",\n\tjumpBackward: \"tui.editor.jumpBackward\",\n\tpageUp: \"tui.editor.pageUp\",\n\tpageDown: \"tui.editor.pageDown\",\n\tdeleteCharBackward: \"tui.editor.deleteCharBackward\",\n\tdeleteCharForward: \"tui.editor.deleteCharForward\",\n\tdeleteWordBackward: \"tui.editor.deleteWordBackward\",\n\tdeleteWordForward: \"tui.editor.deleteWordForward\",\n\tdeleteToLineStart: \"tui.editor.deleteToLineStart\",\n\tdeleteToLineEnd: \"tui.editor.deleteToLineEnd\",\n\tyank: \"tui.editor.yank\",\n\tyankPop: \"tui.editor.yankPop\",\n\tundo: \"tui.editor.undo\",\n\tnewLine: \"tui.input.newLine\",\n\tsubmit: \"tui.input.submit\",\n\ttab: \"tui.input.tab\",\n\tcopy: \"tui.input.copy\",\n\tselectUp: \"tui.select.up\",\n\tselectDown: \"tui.select.down\",\n\tselectPageUp: \"tui.select.pageUp\",\n\tselectPageDown: \"tui.select.pageDown\",\n\tselectConfirm: \"tui.select.confirm\",\n\tselectCancel: \"tui.select.cancel\",\n\tinterrupt: \"app.interrupt\",\n\tclear: \"app.clear\",\n\texit: \"app.exit\",\n\tsuspend: \"app.suspend\",\n\tcycleThinkingLevel: \"app.thinking.cycle\",\n\tcycleModelForward: \"app.model.cycleForward\",\n\tcycleModelBackward: \"app.model.cycleBackward\",\n\tselectModel: \"app.model.select\",\n\texpandTools: \"app.tools.expand\",\n\ttoggleThinking: \"app.thinking.toggle\",\n\ttoggleSessionNamedFilter: \"app.session.toggleNamedFilter\",\n\texternalEditor: \"app.editor.external\",\n\tfollowUp: \"app.message.followUp\",\n\tdequeue: \"app.message.dequeue\",\n\tpasteImage: \"app.clipboard.pasteImage\",\n\tnewSession: \"app.session.new\",\n\ttree: \"app.session.tree\",\n\tfork: \"app.session.fork\",\n\tresume: \"app.session.resume\",\n\ttreeFoldOrUp: \"app.tree.foldOrUp\",\n\ttreeUnfoldOrDown: \"app.tree.unfoldOrDown\",\n\ttreeEditLabel: \"app.tree.editLabel\",\n\ttreeToggleLabelTimestamp: \"app.tree.toggleLabelTimestamp\",\n\ttoggleSessionPath: \"app.session.togglePath\",\n\ttoggleSessionSort: \"app.session.toggleSort\",\n\trenameSession: \"app.session.rename\",\n\tdeleteSession: \"app.session.delete\",\n\tdeleteSessionNoninvasive: \"app.session.deleteNoninvasive\",\n} as const satisfies Record<string, Keybinding>;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isLegacyKeybindingName(key: string): key is keyof typeof KEYBINDING_NAME_MIGRATIONS {\n\treturn key in KEYBINDING_NAME_MIGRATIONS;\n}\n\nfunction toKeybindingsConfig(value: unknown): KeybindingsConfig {\n\tif (!isRecord(value)) return {};\n\n\tconst config: KeybindingsConfig = {};\n\tfor (const [key, binding] of Object.entries(value)) {\n\t\tif (typeof binding === \"string\") {\n\t\t\tconfig[key] = binding as KeyId;\n\t\t\tcontinue;\n\t\t}\n\t\tif (Array.isArray(binding) && binding.every((entry) => typeof entry === \"string\")) {\n\t\t\tconfig[key] = binding as KeyId[];\n\t\t}\n\t}\n\treturn config;\n}\n\nexport function migrateKeybindingsConfig(rawConfig: Record<string, unknown>): {\n\tconfig: Record<string, unknown>;\n\tmigrated: boolean;\n} {\n\tconst config: Record<string, unknown> = {};\n\tlet migrated = false;\n\n\tfor (const [key, value] of Object.entries(rawConfig)) {\n\t\tconst nextKey = isLegacyKeybindingName(key) ? KEYBINDING_NAME_MIGRATIONS[key] : key;\n\t\tif (nextKey !== key) {\n\t\t\tmigrated = true;\n\t\t}\n\t\tif (key !== nextKey && Object.hasOwn(rawConfig, nextKey)) {\n\t\t\tmigrated = true;\n\t\t\tcontinue;\n\t\t}\n\t\tconfig[nextKey] = value;\n\t}\n\n\treturn { config: orderKeybindingsConfig(config), migrated };\n}\n\nfunction orderKeybindingsConfig(config: Record<string, unknown>): Record<string, unknown> {\n\tconst ordered: Record<string, unknown> = {};\n\tfor (const keybinding of Object.keys(KEYBINDINGS)) {\n\t\tif (Object.hasOwn(config, keybinding)) {\n\t\t\tordered[keybinding] = config[keybinding];\n\t\t}\n\t}\n\n\tconst extras = Object.keys(config)\n\t\t.filter((key) => !Object.hasOwn(ordered, key))\n\t\t.sort();\n\tfor (const key of extras) {\n\t\tordered[key] = config[key];\n\t}\n\n\treturn ordered;\n}\n\nfunction loadRawConfig(path: string): Record<string, unknown> | undefined {\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as unknown;\n\t\treturn isRecord(parsed) ? parsed : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport class KeybindingsManager extends TuiKeybindingsManager {\n\tprivate configPath: string | undefined;\n\n\tconstructor(userBindings: KeybindingsConfig = {}, configPath?: string) {\n\t\tsuper(KEYBINDINGS, userBindings);\n\t\tthis.configPath = configPath;\n\t}\n\n\tstatic create(agentDir: string = getAgentDir()): KeybindingsManager {\n\t\tconst configPath = join(agentDir, \"keybindings.json\");\n\t\tconst userBindings = KeybindingsManager.loadFromFile(configPath);\n\t\treturn new KeybindingsManager(userBindings, configPath);\n\t}\n\n\treload(): void {\n\t\tif (!this.configPath) return;\n\t\tthis.setUserBindings(KeybindingsManager.loadFromFile(this.configPath));\n\t}\n\n\tgetEffectiveConfig(): KeybindingsConfig {\n\t\treturn this.getResolvedBindings();\n\t}\n\n\tprivate static loadFromFile(path: string): KeybindingsConfig {\n\t\tconst rawConfig = loadRawConfig(path);\n\t\tif (!rawConfig) return {};\n\t\treturn toKeybindingsConfig(migrateKeybindingsConfig(rawConfig).config);\n\t}\n}\n\nexport type { Keybinding, KeyId, KeybindingsConfig };\n"]}
|
package/dist/core/keybindings.js
CHANGED
|
@@ -30,8 +30,11 @@ export const KEYBINDINGS = {
|
|
|
30
30
|
description: "Toggle thinking blocks",
|
|
31
31
|
},
|
|
32
32
|
"app.tasks.cycleView": {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// ctrl+n is free in the main editor (no emacs next-line binding here);
|
|
34
|
+
// app.session.toggleNamedFilter also uses ctrl+n but only inside the
|
|
35
|
+
// session-selector overlay, which captures keys while open.
|
|
36
|
+
defaultKeys: "ctrl+n",
|
|
37
|
+
description: "Cycle task panel view (tasks → subagents → teams, skips empty lenses)",
|
|
35
38
|
},
|
|
36
39
|
"app.session.toggleNamedFilter": {
|
|
37
40
|
defaultKeys: "ctrl+n",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keybindings.js","sourceRoot":"","sources":["../../src/core/keybindings.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype Keybinding,\n\ttype KeybindingDefinitions,\n\ttype KeybindingsConfig,\n\ttype KeyId,\n\tTUI_KEYBINDINGS,\n\tKeybindingsManager as TuiKeybindingsManager,\n} from \"@kolisachint/hoocode-tui\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\nexport interface AppKeybindings {\n\t\"app.interrupt\": true;\n\t\"app.clear\": true;\n\t\"app.exit\": true;\n\t\"app.suspend\": true;\n\t\"app.thinking.cycle\": true;\n\t\"app.model.cycleForward\": true;\n\t\"app.model.cycleBackward\": true;\n\t\"app.model.select\": true;\n\t\"app.tools.expand\": true;\n\t\"app.thinking.toggle\": true;\n\t\"app.tasks.cycleView\": true;\n\t\"app.session.toggleNamedFilter\": true;\n\t\"app.editor.external\": true;\n\t\"app.message.followUp\": true;\n\t\"app.message.dequeue\": true;\n\t\"app.clipboard.pasteImage\": true;\n\t\"app.session.new\": true;\n\t\"app.session.tree\": true;\n\t\"app.session.fork\": true;\n\t\"app.session.resume\": true;\n\t\"app.tree.foldOrUp\": true;\n\t\"app.tree.unfoldOrDown\": true;\n\t\"app.tree.editLabel\": true;\n\t\"app.tree.toggleLabelTimestamp\": true;\n\t\"app.session.togglePath\": true;\n\t\"app.session.toggleSort\": true;\n\t\"app.session.rename\": true;\n\t\"app.session.delete\": true;\n\t\"app.session.deleteNoninvasive\": true;\n\t\"app.models.save\": true;\n\t\"app.models.enableAll\": true;\n\t\"app.models.clearAll\": true;\n\t\"app.models.toggleProvider\": true;\n\t\"app.models.reorderUp\": true;\n\t\"app.models.reorderDown\": true;\n\t\"app.tree.filter.default\": true;\n\t\"app.tree.filter.noTools\": true;\n\t\"app.tree.filter.userOnly\": true;\n\t\"app.tree.filter.labeledOnly\": true;\n\t\"app.tree.filter.all\": true;\n\t\"app.tree.filter.cycleForward\": true;\n\t\"app.tree.filter.cycleBackward\": true;\n}\n\nexport type AppKeybinding = keyof AppKeybindings;\n\ndeclare module \"@kolisachint/hoocode-tui\" {\n\tinterface Keybindings extends AppKeybindings {}\n}\n\nexport const KEYBINDINGS = {\n\t...TUI_KEYBINDINGS,\n\t\"app.interrupt\": { defaultKeys: \"escape\", description: \"Cancel or abort\" },\n\t\"app.clear\": { defaultKeys: \"ctrl+c\", description: \"Clear editor\" },\n\t\"app.exit\": { defaultKeys: \"ctrl+d\", description: \"Exit when editor is empty\" },\n\t\"app.suspend\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? [] : \"ctrl+z\",\n\t\tdescription: \"Suspend to background\",\n\t},\n\t\"app.thinking.cycle\": {\n\t\tdefaultKeys: \"shift+tab\",\n\t\tdescription: \"Cycle thinking level\",\n\t},\n\t\"app.model.cycleForward\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Cycle to next model\",\n\t},\n\t\"app.model.cycleBackward\": {\n\t\tdefaultKeys: \"shift+ctrl+p\",\n\t\tdescription: \"Cycle to previous model\",\n\t},\n\t\"app.model.select\": { defaultKeys: \"ctrl+l\", description: \"Open model selector\" },\n\t\"app.tools.expand\": { defaultKeys: \"ctrl+o\", description: \"Toggle tool output\" },\n\t\"app.thinking.toggle\": {\n\t\tdefaultKeys: \"ctrl+t\",\n\t\tdescription: \"Toggle thinking blocks\",\n\t},\n\t\"app.tasks.cycleView\": {\n\t\tdefaultKeys: \"shift+ctrl+t\",\n\t\tdescription: \"Cycle task panel view (tasks → subagents → teams)\",\n\t},\n\t\"app.session.toggleNamedFilter\": {\n\t\tdefaultKeys: \"ctrl+n\",\n\t\tdescription: \"Toggle named session filter\",\n\t},\n\t\"app.editor.external\": {\n\t\tdefaultKeys: \"ctrl+g\",\n\t\tdescription: \"Open external editor\",\n\t},\n\t\"app.message.followUp\": {\n\t\tdefaultKeys: \"alt+enter\",\n\t\tdescription: \"Queue follow-up message\",\n\t},\n\t\"app.message.dequeue\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Restore queued messages\",\n\t},\n\t\"app.clipboard.pasteImage\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? \"alt+v\" : \"ctrl+v\",\n\t\tdescription: \"Paste image from clipboard\",\n\t},\n\t\"app.session.new\": { defaultKeys: [], description: \"Start a new session\" },\n\t\"app.session.tree\": { defaultKeys: [], description: \"Open session tree\" },\n\t\"app.session.fork\": { defaultKeys: [], description: \"Fork current session\" },\n\t\"app.session.resume\": { defaultKeys: [], description: \"Resume a session\" },\n\t\"app.tree.foldOrUp\": {\n\t\tdefaultKeys: [\"ctrl+left\", \"alt+left\"],\n\t\tdescription: \"Fold tree branch or move up\",\n\t},\n\t\"app.tree.unfoldOrDown\": {\n\t\tdefaultKeys: [\"ctrl+right\", \"alt+right\"],\n\t\tdescription: \"Unfold tree branch or move down\",\n\t},\n\t\"app.tree.editLabel\": {\n\t\tdefaultKeys: \"shift+l\",\n\t\tdescription: \"Edit tree label\",\n\t},\n\t\"app.tree.toggleLabelTimestamp\": {\n\t\tdefaultKeys: \"shift+t\",\n\t\tdescription: \"Toggle tree label timestamps\",\n\t},\n\t\"app.session.togglePath\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Toggle session path display\",\n\t},\n\t\"app.session.toggleSort\": {\n\t\tdefaultKeys: \"ctrl+s\",\n\t\tdescription: \"Toggle session sort mode\",\n\t},\n\t\"app.session.rename\": {\n\t\tdefaultKeys: \"ctrl+r\",\n\t\tdescription: \"Rename session\",\n\t},\n\t\"app.session.delete\": {\n\t\tdefaultKeys: \"ctrl+d\",\n\t\tdescription: \"Delete session\",\n\t},\n\t\"app.session.deleteNoninvasive\": {\n\t\tdefaultKeys: \"ctrl+backspace\",\n\t\tdescription: \"Delete session when query is empty\",\n\t},\n\t\"app.models.save\": {\n\t\tdefaultKeys: \"ctrl+s\",\n\t\tdescription: \"Save model selection\",\n\t},\n\t\"app.models.enableAll\": {\n\t\tdefaultKeys: \"ctrl+a\",\n\t\tdescription: \"Enable all models\",\n\t},\n\t\"app.models.clearAll\": {\n\t\tdefaultKeys: \"ctrl+x\",\n\t\tdescription: \"Clear all models\",\n\t},\n\t\"app.models.toggleProvider\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Toggle all models for provider\",\n\t},\n\t\"app.models.reorderUp\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Move model up in order\",\n\t},\n\t\"app.models.reorderDown\": {\n\t\tdefaultKeys: \"alt+down\",\n\t\tdescription: \"Move model down in order\",\n\t},\n\t\"app.options.next\": { defaultKeys: \"right\", description: \"Confirm and advance to the next question\" },\n\t\"app.options.back\": { defaultKeys: \"left\", description: \"Go back to the previous question\" },\n\t\"app.tree.filter.default\": {\n\t\tdefaultKeys: \"ctrl+d\",\n\t\tdescription: \"Tree filter: default view\",\n\t},\n\t\"app.tree.filter.noTools\": {\n\t\tdefaultKeys: \"ctrl+t\",\n\t\tdescription: \"Tree filter: hide tool results\",\n\t},\n\t\"app.tree.filter.userOnly\": {\n\t\tdefaultKeys: \"ctrl+u\",\n\t\tdescription: \"Tree filter: user messages only\",\n\t},\n\t\"app.tree.filter.labeledOnly\": {\n\t\tdefaultKeys: \"ctrl+l\",\n\t\tdescription: \"Tree filter: labeled entries only\",\n\t},\n\t\"app.tree.filter.all\": {\n\t\tdefaultKeys: \"ctrl+a\",\n\t\tdescription: \"Tree filter: show all entries\",\n\t},\n\t\"app.tree.filter.cycleForward\": {\n\t\tdefaultKeys: \"ctrl+o\",\n\t\tdescription: \"Tree filter: cycle forward\",\n\t},\n\t\"app.tree.filter.cycleBackward\": {\n\t\tdefaultKeys: \"shift+ctrl+o\",\n\t\tdescription: \"Tree filter: cycle backward\",\n\t},\n} as const satisfies KeybindingDefinitions;\n\nconst KEYBINDING_NAME_MIGRATIONS = {\n\tcursorUp: \"tui.editor.cursorUp\",\n\tcursorDown: \"tui.editor.cursorDown\",\n\tcursorLeft: \"tui.editor.cursorLeft\",\n\tcursorRight: \"tui.editor.cursorRight\",\n\tcursorWordLeft: \"tui.editor.cursorWordLeft\",\n\tcursorWordRight: \"tui.editor.cursorWordRight\",\n\tcursorLineStart: \"tui.editor.cursorLineStart\",\n\tcursorLineEnd: \"tui.editor.cursorLineEnd\",\n\tjumpForward: \"tui.editor.jumpForward\",\n\tjumpBackward: \"tui.editor.jumpBackward\",\n\tpageUp: \"tui.editor.pageUp\",\n\tpageDown: \"tui.editor.pageDown\",\n\tdeleteCharBackward: \"tui.editor.deleteCharBackward\",\n\tdeleteCharForward: \"tui.editor.deleteCharForward\",\n\tdeleteWordBackward: \"tui.editor.deleteWordBackward\",\n\tdeleteWordForward: \"tui.editor.deleteWordForward\",\n\tdeleteToLineStart: \"tui.editor.deleteToLineStart\",\n\tdeleteToLineEnd: \"tui.editor.deleteToLineEnd\",\n\tyank: \"tui.editor.yank\",\n\tyankPop: \"tui.editor.yankPop\",\n\tundo: \"tui.editor.undo\",\n\tnewLine: \"tui.input.newLine\",\n\tsubmit: \"tui.input.submit\",\n\ttab: \"tui.input.tab\",\n\tcopy: \"tui.input.copy\",\n\tselectUp: \"tui.select.up\",\n\tselectDown: \"tui.select.down\",\n\tselectPageUp: \"tui.select.pageUp\",\n\tselectPageDown: \"tui.select.pageDown\",\n\tselectConfirm: \"tui.select.confirm\",\n\tselectCancel: \"tui.select.cancel\",\n\tinterrupt: \"app.interrupt\",\n\tclear: \"app.clear\",\n\texit: \"app.exit\",\n\tsuspend: \"app.suspend\",\n\tcycleThinkingLevel: \"app.thinking.cycle\",\n\tcycleModelForward: \"app.model.cycleForward\",\n\tcycleModelBackward: \"app.model.cycleBackward\",\n\tselectModel: \"app.model.select\",\n\texpandTools: \"app.tools.expand\",\n\ttoggleThinking: \"app.thinking.toggle\",\n\ttoggleSessionNamedFilter: \"app.session.toggleNamedFilter\",\n\texternalEditor: \"app.editor.external\",\n\tfollowUp: \"app.message.followUp\",\n\tdequeue: \"app.message.dequeue\",\n\tpasteImage: \"app.clipboard.pasteImage\",\n\tnewSession: \"app.session.new\",\n\ttree: \"app.session.tree\",\n\tfork: \"app.session.fork\",\n\tresume: \"app.session.resume\",\n\ttreeFoldOrUp: \"app.tree.foldOrUp\",\n\ttreeUnfoldOrDown: \"app.tree.unfoldOrDown\",\n\ttreeEditLabel: \"app.tree.editLabel\",\n\ttreeToggleLabelTimestamp: \"app.tree.toggleLabelTimestamp\",\n\ttoggleSessionPath: \"app.session.togglePath\",\n\ttoggleSessionSort: \"app.session.toggleSort\",\n\trenameSession: \"app.session.rename\",\n\tdeleteSession: \"app.session.delete\",\n\tdeleteSessionNoninvasive: \"app.session.deleteNoninvasive\",\n} as const satisfies Record<string, Keybinding>;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isLegacyKeybindingName(key: string): key is keyof typeof KEYBINDING_NAME_MIGRATIONS {\n\treturn key in KEYBINDING_NAME_MIGRATIONS;\n}\n\nfunction toKeybindingsConfig(value: unknown): KeybindingsConfig {\n\tif (!isRecord(value)) return {};\n\n\tconst config: KeybindingsConfig = {};\n\tfor (const [key, binding] of Object.entries(value)) {\n\t\tif (typeof binding === \"string\") {\n\t\t\tconfig[key] = binding as KeyId;\n\t\t\tcontinue;\n\t\t}\n\t\tif (Array.isArray(binding) && binding.every((entry) => typeof entry === \"string\")) {\n\t\t\tconfig[key] = binding as KeyId[];\n\t\t}\n\t}\n\treturn config;\n}\n\nexport function migrateKeybindingsConfig(rawConfig: Record<string, unknown>): {\n\tconfig: Record<string, unknown>;\n\tmigrated: boolean;\n} {\n\tconst config: Record<string, unknown> = {};\n\tlet migrated = false;\n\n\tfor (const [key, value] of Object.entries(rawConfig)) {\n\t\tconst nextKey = isLegacyKeybindingName(key) ? KEYBINDING_NAME_MIGRATIONS[key] : key;\n\t\tif (nextKey !== key) {\n\t\t\tmigrated = true;\n\t\t}\n\t\tif (key !== nextKey && Object.hasOwn(rawConfig, nextKey)) {\n\t\t\tmigrated = true;\n\t\t\tcontinue;\n\t\t}\n\t\tconfig[nextKey] = value;\n\t}\n\n\treturn { config: orderKeybindingsConfig(config), migrated };\n}\n\nfunction orderKeybindingsConfig(config: Record<string, unknown>): Record<string, unknown> {\n\tconst ordered: Record<string, unknown> = {};\n\tfor (const keybinding of Object.keys(KEYBINDINGS)) {\n\t\tif (Object.hasOwn(config, keybinding)) {\n\t\t\tordered[keybinding] = config[keybinding];\n\t\t}\n\t}\n\n\tconst extras = Object.keys(config)\n\t\t.filter((key) => !Object.hasOwn(ordered, key))\n\t\t.sort();\n\tfor (const key of extras) {\n\t\tordered[key] = config[key];\n\t}\n\n\treturn ordered;\n}\n\nfunction loadRawConfig(path: string): Record<string, unknown> | undefined {\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as unknown;\n\t\treturn isRecord(parsed) ? parsed : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport class KeybindingsManager extends TuiKeybindingsManager {\n\tprivate configPath: string | undefined;\n\n\tconstructor(userBindings: KeybindingsConfig = {}, configPath?: string) {\n\t\tsuper(KEYBINDINGS, userBindings);\n\t\tthis.configPath = configPath;\n\t}\n\n\tstatic create(agentDir: string = getAgentDir()): KeybindingsManager {\n\t\tconst configPath = join(agentDir, \"keybindings.json\");\n\t\tconst userBindings = KeybindingsManager.loadFromFile(configPath);\n\t\treturn new KeybindingsManager(userBindings, configPath);\n\t}\n\n\treload(): void {\n\t\tif (!this.configPath) return;\n\t\tthis.setUserBindings(KeybindingsManager.loadFromFile(this.configPath));\n\t}\n\n\tgetEffectiveConfig(): KeybindingsConfig {\n\t\treturn this.getResolvedBindings();\n\t}\n\n\tprivate static loadFromFile(path: string): KeybindingsConfig {\n\t\tconst rawConfig = loadRawConfig(path);\n\t\tif (!rawConfig) return {};\n\t\treturn toKeybindingsConfig(migrateKeybindingsConfig(rawConfig).config);\n\t}\n}\n\nexport type { Keybinding, KeyId, KeybindingsConfig };\n"]}
|
|
1
|
+
{"version":3,"file":"keybindings.js","sourceRoot":"","sources":["../../src/core/keybindings.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype Keybinding,\n\ttype KeybindingDefinitions,\n\ttype KeybindingsConfig,\n\ttype KeyId,\n\tTUI_KEYBINDINGS,\n\tKeybindingsManager as TuiKeybindingsManager,\n} from \"@kolisachint/hoocode-tui\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\nexport interface AppKeybindings {\n\t\"app.interrupt\": true;\n\t\"app.clear\": true;\n\t\"app.exit\": true;\n\t\"app.suspend\": true;\n\t\"app.thinking.cycle\": true;\n\t\"app.model.cycleForward\": true;\n\t\"app.model.cycleBackward\": true;\n\t\"app.model.select\": true;\n\t\"app.tools.expand\": true;\n\t\"app.thinking.toggle\": true;\n\t\"app.tasks.cycleView\": true;\n\t\"app.session.toggleNamedFilter\": true;\n\t\"app.editor.external\": true;\n\t\"app.message.followUp\": true;\n\t\"app.message.dequeue\": true;\n\t\"app.clipboard.pasteImage\": true;\n\t\"app.session.new\": true;\n\t\"app.session.tree\": true;\n\t\"app.session.fork\": true;\n\t\"app.session.resume\": true;\n\t\"app.tree.foldOrUp\": true;\n\t\"app.tree.unfoldOrDown\": true;\n\t\"app.tree.editLabel\": true;\n\t\"app.tree.toggleLabelTimestamp\": true;\n\t\"app.session.togglePath\": true;\n\t\"app.session.toggleSort\": true;\n\t\"app.session.rename\": true;\n\t\"app.session.delete\": true;\n\t\"app.session.deleteNoninvasive\": true;\n\t\"app.models.save\": true;\n\t\"app.models.enableAll\": true;\n\t\"app.models.clearAll\": true;\n\t\"app.models.toggleProvider\": true;\n\t\"app.models.reorderUp\": true;\n\t\"app.models.reorderDown\": true;\n\t\"app.tree.filter.default\": true;\n\t\"app.tree.filter.noTools\": true;\n\t\"app.tree.filter.userOnly\": true;\n\t\"app.tree.filter.labeledOnly\": true;\n\t\"app.tree.filter.all\": true;\n\t\"app.tree.filter.cycleForward\": true;\n\t\"app.tree.filter.cycleBackward\": true;\n}\n\nexport type AppKeybinding = keyof AppKeybindings;\n\ndeclare module \"@kolisachint/hoocode-tui\" {\n\tinterface Keybindings extends AppKeybindings {}\n}\n\nexport const KEYBINDINGS = {\n\t...TUI_KEYBINDINGS,\n\t\"app.interrupt\": { defaultKeys: \"escape\", description: \"Cancel or abort\" },\n\t\"app.clear\": { defaultKeys: \"ctrl+c\", description: \"Clear editor\" },\n\t\"app.exit\": { defaultKeys: \"ctrl+d\", description: \"Exit when editor is empty\" },\n\t\"app.suspend\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? [] : \"ctrl+z\",\n\t\tdescription: \"Suspend to background\",\n\t},\n\t\"app.thinking.cycle\": {\n\t\tdefaultKeys: \"shift+tab\",\n\t\tdescription: \"Cycle thinking level\",\n\t},\n\t\"app.model.cycleForward\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Cycle to next model\",\n\t},\n\t\"app.model.cycleBackward\": {\n\t\tdefaultKeys: \"shift+ctrl+p\",\n\t\tdescription: \"Cycle to previous model\",\n\t},\n\t\"app.model.select\": { defaultKeys: \"ctrl+l\", description: \"Open model selector\" },\n\t\"app.tools.expand\": { defaultKeys: \"ctrl+o\", description: \"Toggle tool output\" },\n\t\"app.thinking.toggle\": {\n\t\tdefaultKeys: \"ctrl+t\",\n\t\tdescription: \"Toggle thinking blocks\",\n\t},\n\t\"app.tasks.cycleView\": {\n\t\t// ctrl+n is free in the main editor (no emacs next-line binding here);\n\t\t// app.session.toggleNamedFilter also uses ctrl+n but only inside the\n\t\t// session-selector overlay, which captures keys while open.\n\t\tdefaultKeys: \"ctrl+n\",\n\t\tdescription: \"Cycle task panel view (tasks → subagents → teams, skips empty lenses)\",\n\t},\n\t\"app.session.toggleNamedFilter\": {\n\t\tdefaultKeys: \"ctrl+n\",\n\t\tdescription: \"Toggle named session filter\",\n\t},\n\t\"app.editor.external\": {\n\t\tdefaultKeys: \"ctrl+g\",\n\t\tdescription: \"Open external editor\",\n\t},\n\t\"app.message.followUp\": {\n\t\tdefaultKeys: \"alt+enter\",\n\t\tdescription: \"Queue follow-up message\",\n\t},\n\t\"app.message.dequeue\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Restore queued messages\",\n\t},\n\t\"app.clipboard.pasteImage\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? \"alt+v\" : \"ctrl+v\",\n\t\tdescription: \"Paste image from clipboard\",\n\t},\n\t\"app.session.new\": { defaultKeys: [], description: \"Start a new session\" },\n\t\"app.session.tree\": { defaultKeys: [], description: \"Open session tree\" },\n\t\"app.session.fork\": { defaultKeys: [], description: \"Fork current session\" },\n\t\"app.session.resume\": { defaultKeys: [], description: \"Resume a session\" },\n\t\"app.tree.foldOrUp\": {\n\t\tdefaultKeys: [\"ctrl+left\", \"alt+left\"],\n\t\tdescription: \"Fold tree branch or move up\",\n\t},\n\t\"app.tree.unfoldOrDown\": {\n\t\tdefaultKeys: [\"ctrl+right\", \"alt+right\"],\n\t\tdescription: \"Unfold tree branch or move down\",\n\t},\n\t\"app.tree.editLabel\": {\n\t\tdefaultKeys: \"shift+l\",\n\t\tdescription: \"Edit tree label\",\n\t},\n\t\"app.tree.toggleLabelTimestamp\": {\n\t\tdefaultKeys: \"shift+t\",\n\t\tdescription: \"Toggle tree label timestamps\",\n\t},\n\t\"app.session.togglePath\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Toggle session path display\",\n\t},\n\t\"app.session.toggleSort\": {\n\t\tdefaultKeys: \"ctrl+s\",\n\t\tdescription: \"Toggle session sort mode\",\n\t},\n\t\"app.session.rename\": {\n\t\tdefaultKeys: \"ctrl+r\",\n\t\tdescription: \"Rename session\",\n\t},\n\t\"app.session.delete\": {\n\t\tdefaultKeys: \"ctrl+d\",\n\t\tdescription: \"Delete session\",\n\t},\n\t\"app.session.deleteNoninvasive\": {\n\t\tdefaultKeys: \"ctrl+backspace\",\n\t\tdescription: \"Delete session when query is empty\",\n\t},\n\t\"app.models.save\": {\n\t\tdefaultKeys: \"ctrl+s\",\n\t\tdescription: \"Save model selection\",\n\t},\n\t\"app.models.enableAll\": {\n\t\tdefaultKeys: \"ctrl+a\",\n\t\tdescription: \"Enable all models\",\n\t},\n\t\"app.models.clearAll\": {\n\t\tdefaultKeys: \"ctrl+x\",\n\t\tdescription: \"Clear all models\",\n\t},\n\t\"app.models.toggleProvider\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Toggle all models for provider\",\n\t},\n\t\"app.models.reorderUp\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Move model up in order\",\n\t},\n\t\"app.models.reorderDown\": {\n\t\tdefaultKeys: \"alt+down\",\n\t\tdescription: \"Move model down in order\",\n\t},\n\t\"app.options.next\": { defaultKeys: \"right\", description: \"Confirm and advance to the next question\" },\n\t\"app.options.back\": { defaultKeys: \"left\", description: \"Go back to the previous question\" },\n\t\"app.tree.filter.default\": {\n\t\tdefaultKeys: \"ctrl+d\",\n\t\tdescription: \"Tree filter: default view\",\n\t},\n\t\"app.tree.filter.noTools\": {\n\t\tdefaultKeys: \"ctrl+t\",\n\t\tdescription: \"Tree filter: hide tool results\",\n\t},\n\t\"app.tree.filter.userOnly\": {\n\t\tdefaultKeys: \"ctrl+u\",\n\t\tdescription: \"Tree filter: user messages only\",\n\t},\n\t\"app.tree.filter.labeledOnly\": {\n\t\tdefaultKeys: \"ctrl+l\",\n\t\tdescription: \"Tree filter: labeled entries only\",\n\t},\n\t\"app.tree.filter.all\": {\n\t\tdefaultKeys: \"ctrl+a\",\n\t\tdescription: \"Tree filter: show all entries\",\n\t},\n\t\"app.tree.filter.cycleForward\": {\n\t\tdefaultKeys: \"ctrl+o\",\n\t\tdescription: \"Tree filter: cycle forward\",\n\t},\n\t\"app.tree.filter.cycleBackward\": {\n\t\tdefaultKeys: \"shift+ctrl+o\",\n\t\tdescription: \"Tree filter: cycle backward\",\n\t},\n} as const satisfies KeybindingDefinitions;\n\nconst KEYBINDING_NAME_MIGRATIONS = {\n\tcursorUp: \"tui.editor.cursorUp\",\n\tcursorDown: \"tui.editor.cursorDown\",\n\tcursorLeft: \"tui.editor.cursorLeft\",\n\tcursorRight: \"tui.editor.cursorRight\",\n\tcursorWordLeft: \"tui.editor.cursorWordLeft\",\n\tcursorWordRight: \"tui.editor.cursorWordRight\",\n\tcursorLineStart: \"tui.editor.cursorLineStart\",\n\tcursorLineEnd: \"tui.editor.cursorLineEnd\",\n\tjumpForward: \"tui.editor.jumpForward\",\n\tjumpBackward: \"tui.editor.jumpBackward\",\n\tpageUp: \"tui.editor.pageUp\",\n\tpageDown: \"tui.editor.pageDown\",\n\tdeleteCharBackward: \"tui.editor.deleteCharBackward\",\n\tdeleteCharForward: \"tui.editor.deleteCharForward\",\n\tdeleteWordBackward: \"tui.editor.deleteWordBackward\",\n\tdeleteWordForward: \"tui.editor.deleteWordForward\",\n\tdeleteToLineStart: \"tui.editor.deleteToLineStart\",\n\tdeleteToLineEnd: \"tui.editor.deleteToLineEnd\",\n\tyank: \"tui.editor.yank\",\n\tyankPop: \"tui.editor.yankPop\",\n\tundo: \"tui.editor.undo\",\n\tnewLine: \"tui.input.newLine\",\n\tsubmit: \"tui.input.submit\",\n\ttab: \"tui.input.tab\",\n\tcopy: \"tui.input.copy\",\n\tselectUp: \"tui.select.up\",\n\tselectDown: \"tui.select.down\",\n\tselectPageUp: \"tui.select.pageUp\",\n\tselectPageDown: \"tui.select.pageDown\",\n\tselectConfirm: \"tui.select.confirm\",\n\tselectCancel: \"tui.select.cancel\",\n\tinterrupt: \"app.interrupt\",\n\tclear: \"app.clear\",\n\texit: \"app.exit\",\n\tsuspend: \"app.suspend\",\n\tcycleThinkingLevel: \"app.thinking.cycle\",\n\tcycleModelForward: \"app.model.cycleForward\",\n\tcycleModelBackward: \"app.model.cycleBackward\",\n\tselectModel: \"app.model.select\",\n\texpandTools: \"app.tools.expand\",\n\ttoggleThinking: \"app.thinking.toggle\",\n\ttoggleSessionNamedFilter: \"app.session.toggleNamedFilter\",\n\texternalEditor: \"app.editor.external\",\n\tfollowUp: \"app.message.followUp\",\n\tdequeue: \"app.message.dequeue\",\n\tpasteImage: \"app.clipboard.pasteImage\",\n\tnewSession: \"app.session.new\",\n\ttree: \"app.session.tree\",\n\tfork: \"app.session.fork\",\n\tresume: \"app.session.resume\",\n\ttreeFoldOrUp: \"app.tree.foldOrUp\",\n\ttreeUnfoldOrDown: \"app.tree.unfoldOrDown\",\n\ttreeEditLabel: \"app.tree.editLabel\",\n\ttreeToggleLabelTimestamp: \"app.tree.toggleLabelTimestamp\",\n\ttoggleSessionPath: \"app.session.togglePath\",\n\ttoggleSessionSort: \"app.session.toggleSort\",\n\trenameSession: \"app.session.rename\",\n\tdeleteSession: \"app.session.delete\",\n\tdeleteSessionNoninvasive: \"app.session.deleteNoninvasive\",\n} as const satisfies Record<string, Keybinding>;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isLegacyKeybindingName(key: string): key is keyof typeof KEYBINDING_NAME_MIGRATIONS {\n\treturn key in KEYBINDING_NAME_MIGRATIONS;\n}\n\nfunction toKeybindingsConfig(value: unknown): KeybindingsConfig {\n\tif (!isRecord(value)) return {};\n\n\tconst config: KeybindingsConfig = {};\n\tfor (const [key, binding] of Object.entries(value)) {\n\t\tif (typeof binding === \"string\") {\n\t\t\tconfig[key] = binding as KeyId;\n\t\t\tcontinue;\n\t\t}\n\t\tif (Array.isArray(binding) && binding.every((entry) => typeof entry === \"string\")) {\n\t\t\tconfig[key] = binding as KeyId[];\n\t\t}\n\t}\n\treturn config;\n}\n\nexport function migrateKeybindingsConfig(rawConfig: Record<string, unknown>): {\n\tconfig: Record<string, unknown>;\n\tmigrated: boolean;\n} {\n\tconst config: Record<string, unknown> = {};\n\tlet migrated = false;\n\n\tfor (const [key, value] of Object.entries(rawConfig)) {\n\t\tconst nextKey = isLegacyKeybindingName(key) ? KEYBINDING_NAME_MIGRATIONS[key] : key;\n\t\tif (nextKey !== key) {\n\t\t\tmigrated = true;\n\t\t}\n\t\tif (key !== nextKey && Object.hasOwn(rawConfig, nextKey)) {\n\t\t\tmigrated = true;\n\t\t\tcontinue;\n\t\t}\n\t\tconfig[nextKey] = value;\n\t}\n\n\treturn { config: orderKeybindingsConfig(config), migrated };\n}\n\nfunction orderKeybindingsConfig(config: Record<string, unknown>): Record<string, unknown> {\n\tconst ordered: Record<string, unknown> = {};\n\tfor (const keybinding of Object.keys(KEYBINDINGS)) {\n\t\tif (Object.hasOwn(config, keybinding)) {\n\t\t\tordered[keybinding] = config[keybinding];\n\t\t}\n\t}\n\n\tconst extras = Object.keys(config)\n\t\t.filter((key) => !Object.hasOwn(ordered, key))\n\t\t.sort();\n\tfor (const key of extras) {\n\t\tordered[key] = config[key];\n\t}\n\n\treturn ordered;\n}\n\nfunction loadRawConfig(path: string): Record<string, unknown> | undefined {\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as unknown;\n\t\treturn isRecord(parsed) ? parsed : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport class KeybindingsManager extends TuiKeybindingsManager {\n\tprivate configPath: string | undefined;\n\n\tconstructor(userBindings: KeybindingsConfig = {}, configPath?: string) {\n\t\tsuper(KEYBINDINGS, userBindings);\n\t\tthis.configPath = configPath;\n\t}\n\n\tstatic create(agentDir: string = getAgentDir()): KeybindingsManager {\n\t\tconst configPath = join(agentDir, \"keybindings.json\");\n\t\tconst userBindings = KeybindingsManager.loadFromFile(configPath);\n\t\treturn new KeybindingsManager(userBindings, configPath);\n\t}\n\n\treload(): void {\n\t\tif (!this.configPath) return;\n\t\tthis.setUserBindings(KeybindingsManager.loadFromFile(this.configPath));\n\t}\n\n\tgetEffectiveConfig(): KeybindingsConfig {\n\t\treturn this.getResolvedBindings();\n\t}\n\n\tprivate static loadFromFile(path: string): KeybindingsConfig {\n\t\tconst rawConfig = loadRawConfig(path);\n\t\tif (!rawConfig) return {};\n\t\treturn toKeybindingsConfig(migrateKeybindingsConfig(rawConfig).config);\n\t}\n}\n\nexport type { Keybinding, KeyId, KeybindingsConfig };\n"]}
|
|
@@ -48,7 +48,11 @@ export interface Task {
|
|
|
48
48
|
status: TaskStatus;
|
|
49
49
|
/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */
|
|
50
50
|
source?: TaskSource;
|
|
51
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Origin label shown as the row's `[tag]` in the task pane: the subagent
|
|
53
|
+
* type for delegations (e.g. "explore"), the MCP server name for MCP tasks
|
|
54
|
+
* (e.g. "github").
|
|
55
|
+
*/
|
|
52
56
|
subagentMode?: string;
|
|
53
57
|
/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */
|
|
54
58
|
agent?: string;
|
|
@@ -121,6 +125,8 @@ declare class TaskStore {
|
|
|
121
125
|
* dropping its task here would orphan the live work (its later status update
|
|
122
126
|
* would target a removed id and silently vanish). Numbering only restarts once
|
|
123
127
|
* no active task survives, so ids never collide with a kept task.
|
|
128
|
+
* Agents with accumulated stats are preserved across resets so cross-turn cost
|
|
129
|
+
* accounting survives.
|
|
124
130
|
*/
|
|
125
131
|
reset(): void;
|
|
126
132
|
list(): readonly Task[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/** Subagent mode when this task is owned by a subagent (e.g. \"explore\"). */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n}\n\nexport type TaskPatch = Partial<\n\tPick<Task, \"title\" | \"status\" | \"source\" | \"subagentMode\" | \"agent\" | \"usage\" | \"note\">\n>;\n\nexport type TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) return;\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\tif (patch.note !== undefined) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\t// The roster only matters while it has tasks to group; drop it with them\n\t\t\t// so a fresh turn opens with a clean pane. Agents owning surviving tasks\n\t\t\t// are kept (their group header must not vanish under live rows).\n\t\t\tthis.taskAgents = [];\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter((a) => liveOwners.has(a.id));\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
|
1
|
+
{"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/**\n\t * Origin label shown as the row's `[tag]` in the task pane: the subagent\n\t * type for delegations (e.g. \"explore\"), the MCP server name for MCP tasks\n\t * (e.g. \"github\").\n\t */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n}\n\nexport type TaskPatch = Partial<\n\tPick<Task, \"title\" | \"status\" | \"source\" | \"subagentMode\" | \"agent\" | \"usage\" | \"note\">\n>;\n\nexport type TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) {\n\t\t\tconsole.warn(`[task-store] update: unknown task id ${id}`);\n\t\t\treturn;\n\t\t}\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\tif (patch.note !== undefined) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) {\n\t\t\tconsole.warn(`[task-store] addAgentStats: unknown agent id \"${id}\"`);\n\t\t\treturn;\n\t\t}\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t * Agents with accumulated stats are preserved across resets so cross-turn cost\n\t * accounting survives.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\t// Keep agents with non-zero accumulated stats so cross-turn cost accounting\n\t\t\t// survives; drop everyone else (fresh turn opens with a clean roster).\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0),\n\t\t\t);\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => liveOwners.has(a.id) || (a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0)),\n\t\t\t);\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
package/dist/core/task-store.js
CHANGED
|
@@ -40,8 +40,10 @@ class TaskStore {
|
|
|
40
40
|
}
|
|
41
41
|
update(id, patch) {
|
|
42
42
|
const task = this.tasks.find((t) => t.id === id);
|
|
43
|
-
if (!task)
|
|
43
|
+
if (!task) {
|
|
44
|
+
console.warn(`[task-store] update: unknown task id ${id}`);
|
|
44
45
|
return;
|
|
46
|
+
}
|
|
45
47
|
if (patch.title !== undefined)
|
|
46
48
|
task.title = patch.title;
|
|
47
49
|
if (patch.status !== undefined)
|
|
@@ -95,8 +97,10 @@ class TaskStore {
|
|
|
95
97
|
/** Add a usage delta to an agent's running totals (creating them at zero). */
|
|
96
98
|
addAgentStats(id, delta) {
|
|
97
99
|
const agent = this.taskAgents.find((a) => a.id === id);
|
|
98
|
-
if (!agent)
|
|
100
|
+
if (!agent) {
|
|
101
|
+
console.warn(`[task-store] addAgentStats: unknown agent id "${id}"`);
|
|
99
102
|
return;
|
|
103
|
+
}
|
|
100
104
|
const stats = agent.stats ?? { input: 0, output: 0, cost: 0 };
|
|
101
105
|
stats.input += delta.input ?? 0;
|
|
102
106
|
stats.output += delta.output ?? 0;
|
|
@@ -139,6 +143,8 @@ class TaskStore {
|
|
|
139
143
|
* dropping its task here would orphan the live work (its later status update
|
|
140
144
|
* would target a removed id and silently vanish). Numbering only restarts once
|
|
141
145
|
* no active task survives, so ids never collide with a kept task.
|
|
146
|
+
* Agents with accumulated stats are preserved across resets so cross-turn cost
|
|
147
|
+
* accounting survives.
|
|
142
148
|
*/
|
|
143
149
|
reset() {
|
|
144
150
|
const active = this.tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
@@ -147,14 +153,13 @@ class TaskStore {
|
|
|
147
153
|
this.tasks = active;
|
|
148
154
|
if (active.length === 0) {
|
|
149
155
|
this.nextId = 1;
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
this.taskAgents = [];
|
|
156
|
+
// Keep agents with non-zero accumulated stats so cross-turn cost accounting
|
|
157
|
+
// survives; drop everyone else (fresh turn opens with a clean roster).
|
|
158
|
+
this.taskAgents = this.taskAgents.filter((a) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0));
|
|
154
159
|
}
|
|
155
160
|
else {
|
|
156
161
|
const liveOwners = new Set(active.map(taskOwnerId));
|
|
157
|
-
this.taskAgents = this.taskAgents.filter((a) => liveOwners.has(a.id));
|
|
162
|
+
this.taskAgents = this.taskAgents.filter((a) => liveOwners.has(a.id) || (a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0)));
|
|
158
163
|
}
|
|
159
164
|
this.emit();
|
|
160
165
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-store.js","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA+EH;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,IAAoC,EAAU;IACzE,IAAI,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC;IAClC,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC;AAAA,CACxD;AAID,MAAM,SAAS;IACN,KAAK,GAAW,EAAE,CAAC;IACnB,UAAU,GAAgB,EAAE,CAAC;IAC7B,MAAM,GAAG,CAAC,CAAC;IACF,SAAS,GAAG,IAAI,GAAG,EAAY,CAAC;IAEjD,MAAM,CAAC,KAAa,EAAE,OAAO,GAAsB,EAAE,EAAQ;QAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,IAAI,GAAS;YAClB,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE;YACjB,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,iBAAiB;YACxC,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;SACd,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IAAA,CACZ;IAED,MAAM,CAAC,EAAU,EAAE,KAAgB,EAAQ;QAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACxD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC3D,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC3D,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS;YAAE,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;QAC7E,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACxD,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACxD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACrD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED;;;;OAIG;IACH,WAAW,CAAC,KAAyE,EAAa;QACjG,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,EAAE,CAAC,CAAC;QAChE,IAAI,QAAQ,EAAE,CAAC;YACd,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YACtC,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,OAAO,QAAQ,CAAC;QACjB,CAAC;QACD,MAAM,OAAO,GAAc;YAC1B,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,KAAK,EAAE,KAAK,CAAC,KAAK;SAClB,CAAC;QACF,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,OAAO,OAAO,CAAC;IAAA,CACf;IAED,iFAA+E;IAC/E,UAAU,CAAC,EAAU,EAAE,KAAqB,EAAQ;QACnD,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACvD,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,8EAA8E;IAC9E,aAAa,CAAC,EAAU,EAAE,KAAyD,EAAQ;QAC1F,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACvD,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC9D,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;QAChC,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;QAClC,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;QAC9B,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,MAAM,GAAyB;QAC9B,OAAO,IAAI,CAAC,UAAU,CAAC;IAAA,CACvB;IAEO,eAAe,CAAC,KAAgB,EAAE,KAAqB,EAAQ;QACtE,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACtD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACtD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACtD,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACzD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;YAAE,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC/D,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAAA,CACzD;IAED,MAAM,CAAC,EAAU,EAAQ;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACrD,IAAI,GAAG,KAAK,CAAC,CAAC;YAAE,OAAO;QACvB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED;;;;;;;;;;;OAWG;IACH,KAAK,GAAS;QACb,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,aAAa,CAAC,CAAC;QAC9F,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACrE,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC;QACpB,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;YAChB,yEAAyE;YACzE,yEAAyE;YACzE,iEAAiE;YACjE,IAAI,CAAC,UAAU,GAAG,EAAE,CAAC;QACtB,CAAC;aAAM,CAAC;YACP,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;YACpD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACvE,CAAC;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,IAAI,GAAoB;QACvB,OAAO,IAAI,CAAC,KAAK,CAAC;IAAA,CAClB;IAED,8EAA8E;IAC9E,KAAK,GAAS;QACb,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAChB,IAAI,CAAC,UAAU,GAAG,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QAChB,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,SAAS,CAAC,QAAkB,EAAc;QACzC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE,CAAC;YACZ,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAAA,CAChC,CAAC;IAAA,CACF;IAEO,IAAI,GAAS;QACpB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACvC,QAAQ,EAAE,CAAC;QACZ,CAAC;IAAA,CACD;CACD;AAED,uCAAuC;AACvC,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/** Subagent mode when this task is owned by a subagent (e.g. \"explore\"). */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n}\n\nexport type TaskPatch = Partial<\n\tPick<Task, \"title\" | \"status\" | \"source\" | \"subagentMode\" | \"agent\" | \"usage\" | \"note\">\n>;\n\nexport type TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) return;\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\tif (patch.note !== undefined) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\t// The roster only matters while it has tasks to group; drop it with them\n\t\t\t// so a fresh turn opens with a clean pane. Agents owning surviving tasks\n\t\t\t// are kept (their group header must not vanish under live rows).\n\t\t\tthis.taskAgents = [];\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter((a) => liveOwners.has(a.id));\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
|
1
|
+
{"version":3,"file":"task-store.js","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/**\n\t * Origin label shown as the row's `[tag]` in the task pane: the subagent\n\t * type for delegations (e.g. \"explore\"), the MCP server name for MCP tasks\n\t * (e.g. \"github\").\n\t */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n}\n\nexport type TaskPatch = Partial<\n\tPick<Task, \"title\" | \"status\" | \"source\" | \"subagentMode\" | \"agent\" | \"usage\" | \"note\">\n>;\n\nexport type TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) {\n\t\t\tconsole.warn(`[task-store] update: unknown task id ${id}`);\n\t\t\treturn;\n\t\t}\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\tif (patch.note !== undefined) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) {\n\t\t\tconsole.warn(`[task-store] addAgentStats: unknown agent id \"${id}\"`);\n\t\t\treturn;\n\t\t}\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t * Agents with accumulated stats are preserved across resets so cross-turn cost\n\t * accounting survives.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\t// Keep agents with non-zero accumulated stats so cross-turn cost accounting\n\t\t\t// survives; drop everyone else (fresh turn opens with a clean roster).\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0),\n\t\t\t);\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => liveOwners.has(a.id) || (a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0)),\n\t\t\t);\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
package/dist/core/team-view.d.ts
CHANGED
|
@@ -31,8 +31,11 @@ export interface TeamViewEvent {
|
|
|
31
31
|
/**
|
|
32
32
|
* Maps team status snapshots and TeamEvents onto task-store patches.
|
|
33
33
|
*
|
|
34
|
-
* Each role owns one roster entry (id `team:<role>`) and one
|
|
35
|
-
*
|
|
34
|
+
* Each role owns one roster entry (id `team:<role>`) and at most one task whose
|
|
35
|
+
* title tracks the role's latest activity. Tasks exist only while a role is
|
|
36
|
+
* actually doing something (active/running, or failed so the error is visible);
|
|
37
|
+
* idle roles keep their roster entry but no task, so a quiet team leaves the
|
|
38
|
+
* pane collapsed instead of pinning it at "working". Entries are re-created on
|
|
36
39
|
* demand because taskStore.reset() wipes finished tasks between user turns.
|
|
37
40
|
*/
|
|
38
41
|
export declare class TeamViewMapper {
|
|
@@ -44,7 +47,11 @@ export declare class TeamViewMapper {
|
|
|
44
47
|
/** Map one TeamEvent from GET /events onto the store. */
|
|
45
48
|
applyEvent(event: TeamViewEvent): void;
|
|
46
49
|
private agentId;
|
|
47
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Make sure the role's roster entry exists, plus its task when the state
|
|
52
|
+
* warrants one (reset() may have dropped both). Idle/done states never
|
|
53
|
+
* create a task — only patch one that live activity already opened.
|
|
54
|
+
*/
|
|
48
55
|
private ensureRole;
|
|
49
56
|
private patchRole;
|
|
50
57
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"team-view.d.ts","sourceRoot":"","sources":["../../src/core/team-view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAwC,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAElF,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE7F,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAkCD;;;;;;GAMG;AACH,qBAAa,cAAc;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAErD,YAAY,KAAK,GAAE,OAAO,SAAqB,EAE9C;IAED,kDAAkD;IAClD,WAAW,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAM9C;IAED,yDAAyD;IACzD,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CA4CrC;IAED,OAAO,CAAC,OAAO;IAIf,6FAA6F;IAC7F,OAAO,CAAC,UAAU;IAYlB,OAAO,CAAC,SAAS;CAMjB;AAED,MAAM,WAAW,eAAe;IAC/B,+CAA+C;IAC/C,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,gCAAgC;IAChC,KAAK,CAAC,EAAE,OAAO,SAAS,CAAC;IACzB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IAClC,sDAAsD;IACtD,IAAI,IAAI,IAAI,CAAC;CACb;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,kBAAkB,CAyE9F","sourcesContent":["/**\n * Read-only hooteams team view (`--team <url>`).\n *\n * Connects to a running hooteams server, registers every role as a\n * kind=\"role\" agent in the task store, and maps the server's TeamEvent SSE\n * stream onto task-store patches so the task panel's existing \"teams\" view\n * shows live role state. Strictly observational: no steering, no attach.\n *\n * The connection is best-effort by design — a connect failure or a later\n * drop logs a warning and never blocks (or crashes) the main agent. At most\n * one SSE connection (to /events) is open at any time.\n */\n\nimport { type TaskAgentState, type TaskStatus, taskStore } from \"./task-store.js\";\n\n/** Shape of GET /status: coarse per-role status keyed by role name. */\nexport type TeamStatusSnapshot = Record<string, { status?: string; lastEventType?: string }>;\n\n/** One frame of GET /events: a hoocode AgentEvent tagged with its producer. */\nexport interface TeamViewEvent {\n\ttype: string;\n\trole: string;\n\tagentId?: string;\n\tts?: number;\n\ttoolName?: string;\n\tmessage?: { role?: string; errorMessage?: string };\n}\n\n/** hooteams AgentStatus word → task panel agent state. */\nfunction stateFromStatus(status: string | undefined): TaskAgentState {\n\tswitch (status) {\n\t\tcase \"idle\":\n\t\t\treturn \"idle\";\n\t\tcase \"thinking\":\n\t\tcase \"streaming\":\n\t\t\treturn \"active\";\n\t\tcase \"tool\":\n\t\t\treturn \"running\";\n\t\tcase \"done\":\n\t\t\treturn \"done\";\n\t\tcase \"error\":\n\t\t\treturn \"failed\";\n\t\tdefault:\n\t\t\treturn \"idle\";\n\t}\n}\n\nfunction taskStatusFromState(state: TaskAgentState): TaskStatus {\n\tswitch (state) {\n\t\tcase \"done\":\n\t\t\treturn \"done\";\n\t\tcase \"failed\":\n\t\t\treturn \"failed\";\n\t\tcase \"idle\":\n\t\t\treturn \"pending\";\n\t\tdefault:\n\t\t\treturn \"in_progress\";\n\t}\n}\n\n/**\n * Maps team status snapshots and TeamEvents onto task-store patches.\n *\n * Each role owns one roster entry (id `team:<role>`) and one live task; the\n * task's title tracks the role's latest activity. Both are re-created on\n * demand because taskStore.reset() wipes finished tasks between user turns.\n */\nexport class TeamViewMapper {\n\tprivate readonly store: typeof taskStore;\n\tprivate readonly taskIds = new Map<string, number>();\n\n\tconstructor(store: typeof taskStore = taskStore) {\n\t\tthis.store = store;\n\t}\n\n\t/** Register roles from a GET /status snapshot. */\n\tapplyStatus(snapshot: TeamStatusSnapshot): void {\n\t\tfor (const [role, info] of Object.entries(snapshot)) {\n\t\t\tconst state = stateFromStatus(info?.status);\n\t\t\tthis.ensureRole(role, state, info?.lastEventType ?? \"connected\");\n\t\t\tthis.patchRole(role, state);\n\t\t}\n\t}\n\n\t/** Map one TeamEvent from GET /events onto the store. */\n\tapplyEvent(event: TeamViewEvent): void {\n\t\tif (!event || typeof event.role !== \"string\" || event.role.length === 0) return;\n\t\tconst role = event.role;\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\tcase \"turn_start\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"thinking\");\n\t\t\t\tthis.patchRole(role, \"active\", \"thinking\");\n\t\t\t\tbreak;\n\t\t\tcase \"message_start\":\n\t\t\tcase \"message_update\":\n\t\t\tcase \"message_end\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"responding\");\n\t\t\t\tthis.patchRole(role, \"active\");\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_start\":\n\t\t\t\tthis.ensureRole(role, \"running\", `tool: ${event.toolName ?? \"?\"}`);\n\t\t\t\tthis.patchRole(role, \"running\", `tool: ${event.toolName ?? \"?\"}`);\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_end\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"thinking\");\n\t\t\t\tthis.patchRole(role, \"active\", \"thinking\");\n\t\t\t\tbreak;\n\t\t\tcase \"turn_end\":\n\t\t\t\tif (event.message?.role === \"assistant\" && event.message.errorMessage) {\n\t\t\t\t\tthis.ensureRole(role, \"failed\", \"error\");\n\t\t\t\t\tthis.patchRole(role, \"failed\", \"error\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"agent_end\": {\n\t\t\t\t// A failed run stays failed; agent_end only marks clean completions\n\t\t\t\t// (mirrors hooteams' own status tracking).\n\t\t\t\tconst failed = this.store.agents().find((a) => a.id === this.agentId(role))?.state === \"failed\";\n\t\t\t\tif (!failed) {\n\t\t\t\t\tthis.ensureRole(role, \"done\", \"idle\");\n\t\t\t\t\tthis.patchRole(role, \"done\", \"idle\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// Unknown event types still prove the role exists.\n\t\t\t\tthis.ensureRole(role, \"idle\", event.type);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate agentId(role: string): string {\n\t\treturn `team:${role}`;\n\t}\n\n\t/** Make sure the role's roster entry and live task exist (reset() may have dropped them). */\n\tprivate ensureRole(role: string, state: TaskAgentState, title: string): void {\n\t\tconst id = this.agentId(role);\n\t\tthis.store.upsertAgent({ id, name: role, kind: \"role\", state });\n\t\tconst taskId = this.taskIds.get(role);\n\t\tconst existing = taskId !== undefined ? this.store.list().find((task) => task.id === taskId) : undefined;\n\t\tif (!existing) {\n\t\t\tconst task = this.store.create(title, { agent: id });\n\t\t\tthis.store.update(task.id, { status: taskStatusFromState(state) });\n\t\t\tthis.taskIds.set(role, task.id);\n\t\t}\n\t}\n\n\tprivate patchRole(role: string, state: TaskAgentState, title?: string): void {\n\t\tthis.store.patchAgent(this.agentId(role), { state });\n\t\tconst taskId = this.taskIds.get(role);\n\t\tif (taskId === undefined) return;\n\t\tthis.store.update(taskId, { status: taskStatusFromState(state), ...(title !== undefined ? { title } : {}) });\n\t}\n}\n\nexport interface TeamViewOptions {\n\t/** Warning sink; defaults to console.error. */\n\twarn?: (message: string) => void;\n\t/** Store override for tests. */\n\tstore?: typeof taskStore;\n\t/** Delay between reconnect attempts in ms (default 5000). */\n\tretryDelayMs?: number;\n}\n\nexport interface TeamViewConnection {\n\t/** Close the SSE connection and stop reconnecting. */\n\tstop(): void;\n}\n\nconst STATUS_TIMEOUT_MS = 5000;\n\n/**\n * Start the read-only team view against a hooteams server base URL.\n *\n * Returns immediately; all network work happens in the background and any\n * failure is reported through `warn` without ever throwing.\n */\nexport function connectTeamView(url: string, options: TeamViewOptions = {}): TeamViewConnection {\n\tconst base = url.replace(/\\/+$/, \"\");\n\tconst warn = options.warn ?? ((message: string) => console.error(message));\n\tconst retryDelayMs = options.retryDelayMs ?? 5000;\n\tconst mapper = new TeamViewMapper(options.store);\n\tconst controller = new AbortController();\n\tlet stopped = false;\n\n\tconst run = async (): Promise<void> => {\n\t\t// 1. Status snapshot: register the current roles.\n\t\ttry {\n\t\t\tconst response = await fetch(`${base}/status`, {\n\t\t\t\tsignal: AbortSignal.any([controller.signal, AbortSignal.timeout(STATUS_TIMEOUT_MS)]),\n\t\t\t});\n\t\t\tif (!response.ok) throw new Error(`HTTP ${response.status}`);\n\t\t\tmapper.applyStatus((await response.json()) as TeamStatusSnapshot);\n\t\t} catch (error) {\n\t\t\tif (stopped) return;\n\t\t\twarn(`team view: failed to fetch ${base}/status (${String(error)}); continuing without the team view`);\n\t\t}\n\n\t\t// 2. Single SSE subscription, reconnecting on drops.\n\t\tlet announcedDrop = false;\n\t\twhile (!stopped) {\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(`${base}/events`, { signal: controller.signal });\n\t\t\t\tif (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);\n\t\t\t\tannouncedDrop = false;\n\t\t\t\tconst reader = response.body.getReader();\n\t\t\t\tconst decoder = new TextDecoder();\n\t\t\t\tlet buffer = \"\";\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst { value, done } = await reader.read();\n\t\t\t\t\tif (done) break;\n\t\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t\t\tlet index = buffer.indexOf(\"\\n\\n\");\n\t\t\t\t\twhile (index !== -1) {\n\t\t\t\t\t\tconst frame = buffer.slice(0, index);\n\t\t\t\t\t\tbuffer = buffer.slice(index + 2);\n\t\t\t\t\t\tfor (const line of frame.split(\"\\n\")) {\n\t\t\t\t\t\t\tif (!line.startsWith(\"data:\")) continue;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tmapper.applyEvent(JSON.parse(line.slice(5).trim()) as TeamViewEvent);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Malformed frames are dropped; the stream stays up.\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tindex = buffer.indexOf(\"\\n\\n\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (stopped) return;\n\t\t\t\tthrow new Error(\"stream ended\");\n\t\t\t} catch (error) {\n\t\t\t\tif (stopped || controller.signal.aborted) return;\n\t\t\t\tif (!announcedDrop) {\n\t\t\t\t\tannouncedDrop = true;\n\t\t\t\t\twarn(`team view: lost connection to ${base}/events (${String(error)}); retrying in background`);\n\t\t\t\t}\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, retryDelayMs));\n\t\t\t}\n\t\t}\n\t};\n\n\tvoid run().catch((error) => {\n\t\tif (!stopped) warn(`team view: unexpected error (${String(error)})`);\n\t});\n\n\treturn {\n\t\tstop() {\n\t\t\tstopped = true;\n\t\t\tcontroller.abort();\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"team-view.d.ts","sourceRoot":"","sources":["../../src/core/team-view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAwC,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAElF,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE7F,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAyCD;;;;;;;;;GASG;AACH,qBAAa,cAAc;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAErD,YAAY,KAAK,GAAE,OAAO,SAAqB,EAE9C;IAED,kDAAkD;IAClD,WAAW,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAM9C;IAED,yDAAyD;IACzD,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CA4CrC;IAED,OAAO,CAAC,OAAO;IAIf;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAYlB,OAAO,CAAC,SAAS;CAMjB;AAED,MAAM,WAAW,eAAe;IAC/B,+CAA+C;IAC/C,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,gCAAgC;IAChC,KAAK,CAAC,EAAE,OAAO,SAAS,CAAC;IACzB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IAClC,sDAAsD;IACtD,IAAI,IAAI,IAAI,CAAC;CACb;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,kBAAkB,CA4E9F","sourcesContent":["/**\n * Read-only hooteams team view (`--team <url>`).\n *\n * Connects to a running hooteams server, registers every role as a\n * kind=\"role\" agent in the task store, and maps the server's TeamEvent SSE\n * stream onto task-store patches so the task panel's existing \"teams\" view\n * shows live role state. Strictly observational: no steering, no attach.\n *\n * The connection is best-effort by design — a connect failure or a later\n * drop logs a warning and never blocks (or crashes) the main agent. At most\n * one SSE connection (to /events) is open at any time.\n */\n\nimport { type TaskAgentState, type TaskStatus, taskStore } from \"./task-store.js\";\n\n/** Shape of GET /status: coarse per-role status keyed by role name. */\nexport type TeamStatusSnapshot = Record<string, { status?: string; lastEventType?: string }>;\n\n/** One frame of GET /events: a hoocode AgentEvent tagged with its producer. */\nexport interface TeamViewEvent {\n\ttype: string;\n\trole: string;\n\tagentId?: string;\n\tts?: number;\n\ttoolName?: string;\n\tmessage?: { role?: string; errorMessage?: string };\n}\n\n/** hooteams AgentStatus word → task panel agent state. */\nfunction stateFromStatus(status: string | undefined): TaskAgentState {\n\tswitch (status) {\n\t\tcase \"idle\":\n\t\t\treturn \"idle\";\n\t\tcase \"thinking\":\n\t\tcase \"streaming\":\n\t\t\treturn \"active\";\n\t\tcase \"tool\":\n\t\t\treturn \"running\";\n\t\tcase \"done\":\n\t\t\treturn \"done\";\n\t\tcase \"error\":\n\t\t\treturn \"failed\";\n\t\tdefault:\n\t\t\treturn \"idle\";\n\t}\n}\n\nfunction taskStatusFromState(state: TaskAgentState): TaskStatus {\n\tswitch (state) {\n\t\tcase \"done\":\n\t\t\treturn \"done\";\n\t\tcase \"failed\":\n\t\t\treturn \"failed\";\n\t\tcase \"idle\":\n\t\t\t// Idle is settled, not queued: a \"pending\" task here would survive every\n\t\t\t// taskStore.reset() and pin the pane at \"working\" for the whole session.\n\t\t\treturn \"done\";\n\t\tdefault:\n\t\t\treturn \"in_progress\";\n\t}\n}\n\n/** Only these states represent activity worth a task row of its own. */\nfunction stateWarrantsTask(state: TaskAgentState): boolean {\n\treturn state === \"active\" || state === \"running\" || state === \"failed\";\n}\n\n/**\n * Maps team status snapshots and TeamEvents onto task-store patches.\n *\n * Each role owns one roster entry (id `team:<role>`) and at most one task whose\n * title tracks the role's latest activity. Tasks exist only while a role is\n * actually doing something (active/running, or failed so the error is visible);\n * idle roles keep their roster entry but no task, so a quiet team leaves the\n * pane collapsed instead of pinning it at \"working\". Entries are re-created on\n * demand because taskStore.reset() wipes finished tasks between user turns.\n */\nexport class TeamViewMapper {\n\tprivate readonly store: typeof taskStore;\n\tprivate readonly taskIds = new Map<string, number>();\n\n\tconstructor(store: typeof taskStore = taskStore) {\n\t\tthis.store = store;\n\t}\n\n\t/** Register roles from a GET /status snapshot. */\n\tapplyStatus(snapshot: TeamStatusSnapshot): void {\n\t\tfor (const [role, info] of Object.entries(snapshot)) {\n\t\t\tconst state = stateFromStatus(info?.status);\n\t\t\tthis.ensureRole(role, state, info?.lastEventType ?? \"connected\");\n\t\t\tthis.patchRole(role, state);\n\t\t}\n\t}\n\n\t/** Map one TeamEvent from GET /events onto the store. */\n\tapplyEvent(event: TeamViewEvent): void {\n\t\tif (!event || typeof event.role !== \"string\" || event.role.length === 0) return;\n\t\tconst role = event.role;\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\tcase \"turn_start\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"thinking\");\n\t\t\t\tthis.patchRole(role, \"active\", \"thinking\");\n\t\t\t\tbreak;\n\t\t\tcase \"message_start\":\n\t\t\tcase \"message_update\":\n\t\t\tcase \"message_end\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"responding\");\n\t\t\t\tthis.patchRole(role, \"active\");\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_start\":\n\t\t\t\tthis.ensureRole(role, \"running\", `tool: ${event.toolName ?? \"?\"}`);\n\t\t\t\tthis.patchRole(role, \"running\", `tool: ${event.toolName ?? \"?\"}`);\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_end\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"thinking\");\n\t\t\t\tthis.patchRole(role, \"active\", \"thinking\");\n\t\t\t\tbreak;\n\t\t\tcase \"turn_end\":\n\t\t\t\tif (event.message?.role === \"assistant\" && event.message.errorMessage) {\n\t\t\t\t\tthis.ensureRole(role, \"failed\", \"error\");\n\t\t\t\t\tthis.patchRole(role, \"failed\", \"error\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"agent_end\": {\n\t\t\t\t// A failed run stays failed; agent_end only marks clean completions\n\t\t\t\t// (mirrors hooteams' own status tracking).\n\t\t\t\tconst failed = this.store.agents().find((a) => a.id === this.agentId(role))?.state === \"failed\";\n\t\t\t\tif (!failed) {\n\t\t\t\t\tthis.ensureRole(role, \"done\", \"idle\");\n\t\t\t\t\tthis.patchRole(role, \"done\", \"idle\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// Unknown event types still prove the role exists.\n\t\t\t\tthis.ensureRole(role, \"idle\", event.type);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate agentId(role: string): string {\n\t\treturn `team:${role}`;\n\t}\n\n\t/**\n\t * Make sure the role's roster entry exists, plus its task when the state\n\t * warrants one (reset() may have dropped both). Idle/done states never\n\t * create a task — only patch one that live activity already opened.\n\t */\n\tprivate ensureRole(role: string, state: TaskAgentState, title: string): void {\n\t\tconst id = this.agentId(role);\n\t\tthis.store.upsertAgent({ id, name: role, kind: \"role\", state });\n\t\tconst taskId = this.taskIds.get(role);\n\t\tconst existing = taskId !== undefined ? this.store.list().find((task) => task.id === taskId) : undefined;\n\t\tif (!existing && stateWarrantsTask(state)) {\n\t\t\tconst task = this.store.create(title, { agent: id });\n\t\t\tthis.store.update(task.id, { status: taskStatusFromState(state) });\n\t\t\tthis.taskIds.set(role, task.id);\n\t\t}\n\t}\n\n\tprivate patchRole(role: string, state: TaskAgentState, title?: string): void {\n\t\tthis.store.patchAgent(this.agentId(role), { state });\n\t\tconst taskId = this.taskIds.get(role);\n\t\tif (taskId === undefined) return;\n\t\tthis.store.update(taskId, { status: taskStatusFromState(state), ...(title !== undefined ? { title } : {}) });\n\t}\n}\n\nexport interface TeamViewOptions {\n\t/** Warning sink; defaults to console.error. */\n\twarn?: (message: string) => void;\n\t/** Store override for tests. */\n\tstore?: typeof taskStore;\n\t/** Delay between reconnect attempts in ms (default 5000). */\n\tretryDelayMs?: number;\n}\n\nexport interface TeamViewConnection {\n\t/** Close the SSE connection and stop reconnecting. */\n\tstop(): void;\n}\n\nconst STATUS_TIMEOUT_MS = 5000;\n\n/**\n * Start the read-only team view against a hooteams server base URL.\n *\n * Returns immediately; all network work happens in the background and any\n * failure is reported through `warn` without ever throwing.\n */\nexport function connectTeamView(url: string, options: TeamViewOptions = {}): TeamViewConnection {\n\tconst base = url.replace(/\\/+$/, \"\");\n\tconst warn = options.warn ?? ((message: string) => console.error(message));\n\tconst retryDelayMs = options.retryDelayMs ?? 5000;\n\tconst mapper = new TeamViewMapper(options.store);\n\tconst controller = new AbortController();\n\tlet stopped = false;\n\n\tconst run = async (): Promise<void> => {\n\t\t// 1. Status snapshot: register the current roles.\n\t\ttry {\n\t\t\tconst response = await fetch(`${base}/status`, {\n\t\t\t\tsignal: AbortSignal.any([controller.signal, AbortSignal.timeout(STATUS_TIMEOUT_MS)]),\n\t\t\t});\n\t\t\tif (!response.ok) throw new Error(`HTTP ${response.status}`);\n\t\t\tmapper.applyStatus((await response.json()) as TeamStatusSnapshot);\n\t\t} catch (error) {\n\t\t\tif (stopped) return;\n\t\t\twarn(`team view: failed to fetch ${base}/status (${String(error)}); continuing without the team view`);\n\t\t}\n\n\t\t// 2. Single SSE subscription, reconnecting on drops.\n\t\tlet announcedDrop = false;\n\t\twhile (!stopped) {\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(`${base}/events`, { signal: controller.signal });\n\t\t\t\tif (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);\n\t\t\t\tconst reader = response.body.getReader();\n\t\t\t\tconst decoder = new TextDecoder();\n\t\t\t\tlet buffer = \"\";\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst { value, done } = await reader.read();\n\t\t\t\t\tif (done) break;\n\t\t\t\t\t// Only a stream that actually delivers data counts as recovered. A 200\n\t\t\t\t\t// that closes immediately (e.g. a server that answers /events without\n\t\t\t\t\t// streaming) used to re-arm the warning and repeat it every retry.\n\t\t\t\t\tannouncedDrop = false;\n\t\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t\t\tlet index = buffer.indexOf(\"\\n\\n\");\n\t\t\t\t\twhile (index !== -1) {\n\t\t\t\t\t\tconst frame = buffer.slice(0, index);\n\t\t\t\t\t\tbuffer = buffer.slice(index + 2);\n\t\t\t\t\t\tfor (const line of frame.split(\"\\n\")) {\n\t\t\t\t\t\t\tif (!line.startsWith(\"data:\")) continue;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tmapper.applyEvent(JSON.parse(line.slice(5).trim()) as TeamViewEvent);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Malformed frames are dropped; the stream stays up.\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tindex = buffer.indexOf(\"\\n\\n\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (stopped) return;\n\t\t\t\tthrow new Error(\"stream ended\");\n\t\t\t} catch (error) {\n\t\t\t\tif (stopped || controller.signal.aborted) return;\n\t\t\t\tif (!announcedDrop) {\n\t\t\t\t\tannouncedDrop = true;\n\t\t\t\t\twarn(`team view: lost connection to ${base}/events (${String(error)}); retrying in background`);\n\t\t\t\t}\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, retryDelayMs));\n\t\t\t}\n\t\t}\n\t};\n\n\tvoid run().catch((error) => {\n\t\tif (!stopped) warn(`team view: unexpected error (${String(error)})`);\n\t});\n\n\treturn {\n\t\tstop() {\n\t\t\tstopped = true;\n\t\t\tcontroller.abort();\n\t\t},\n\t};\n}\n"]}
|