lonny-agent 0.2.4 → 0.2.7
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/.github/workflows/ci.yml +21 -0
- package/AGENTS.md +106 -0
- package/CHANGELOG.md +24 -0
- package/README.md +326 -326
- package/dist/agent/__tests__/session-restore.test.d.ts +2 -0
- package/dist/agent/__tests__/session-restore.test.d.ts.map +1 -0
- package/dist/agent/__tests__/session-restore.test.js +260 -0
- package/dist/agent/__tests__/session-restore.test.js.map +1 -0
- package/dist/agent/commands.d.ts +26 -0
- package/dist/agent/commands.d.ts.map +1 -0
- package/dist/agent/commands.js +253 -0
- package/dist/agent/commands.js.map +1 -0
- package/dist/agent/compaction.js +8 -1
- package/dist/agent/compaction.js.map +1 -1
- package/dist/agent/index.d.ts +2 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +2 -2
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/llm.d.ts +4 -0
- package/dist/agent/llm.d.ts.map +1 -1
- package/dist/agent/memory.d.ts +12 -0
- package/dist/agent/memory.d.ts.map +1 -0
- package/dist/agent/memory.js +51 -0
- package/dist/agent/memory.js.map +1 -0
- package/dist/agent/prompt-builder.d.ts +6 -1
- package/dist/agent/prompt-builder.d.ts.map +1 -1
- package/dist/agent/prompt-builder.js +100 -33
- package/dist/agent/prompt-builder.js.map +1 -1
- package/dist/agent/providers/google.d.ts.map +1 -1
- package/dist/agent/providers/google.js.map +1 -1
- package/dist/agent/providers/openai.d.ts +1 -0
- package/dist/agent/providers/openai.d.ts.map +1 -1
- package/dist/agent/providers/openai.js +59 -6
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/session-chat.d.ts +3 -0
- package/dist/agent/session-chat.d.ts.map +1 -0
- package/dist/agent/session-chat.js +384 -0
- package/dist/agent/session-chat.js.map +1 -0
- package/dist/agent/session-display.d.ts +29 -0
- package/dist/agent/session-display.d.ts.map +1 -0
- package/dist/agent/session-display.js +179 -0
- package/dist/agent/session-display.js.map +1 -0
- package/dist/agent/session-persistence.d.ts +55 -0
- package/dist/agent/session-persistence.d.ts.map +1 -0
- package/dist/agent/session-persistence.js +101 -0
- package/dist/agent/session-persistence.js.map +1 -0
- package/dist/agent/session-utils.d.ts +5 -0
- package/dist/agent/session-utils.d.ts.map +1 -0
- package/dist/agent/session-utils.js +51 -0
- package/dist/agent/session-utils.js.map +1 -0
- package/dist/agent/session.d.ts +19 -17
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +234 -578
- package/dist/agent/session.js.map +1 -1
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +67 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +7 -1
- package/dist/config/index.js.map +1 -1
- package/dist/index.js +24 -3
- package/dist/index.js.map +1 -1
- package/dist/tools/__tests__/bash.test.js +169 -2
- package/dist/tools/__tests__/bash.test.js.map +1 -1
- package/dist/tools/__tests__/edit.test.js +1091 -340
- package/dist/tools/__tests__/edit.test.js.map +1 -1
- package/dist/tools/__tests__/fetch.test.js +1 -1
- package/dist/tools/__tests__/fetch.test.js.map +1 -1
- package/dist/tools/__tests__/glob.test.js +1 -1
- package/dist/tools/__tests__/glob.test.js.map +1 -1
- package/dist/tools/__tests__/grep.test.js +33 -1
- package/dist/tools/__tests__/grep.test.js.map +1 -1
- package/dist/tools/__tests__/ls.test.js +1 -1
- package/dist/tools/__tests__/ls.test.js.map +1 -1
- package/dist/tools/__tests__/read.test.js +1 -1
- package/dist/tools/__tests__/read.test.js.map +1 -1
- package/dist/tools/__tests__/registry.test.js +313 -0
- package/dist/tools/__tests__/registry.test.js.map +1 -1
- package/dist/tools/__tests__/sed.test.d.ts +2 -0
- package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
- package/dist/tools/__tests__/sed.test.js +228 -0
- package/dist/tools/__tests__/sed.test.js.map +1 -0
- package/dist/tools/__tests__/write_plan.test.js +1 -1
- package/dist/tools/__tests__/write_plan.test.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +711 -18
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts +2 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js +7 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts +6 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-final.js +11 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-final.js.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts +2 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-test.js +9 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-test.js.map +1 -0
- package/dist/tools/codebase/ast/__tests__/integration.test.d.ts +2 -0
- package/dist/tools/codebase/ast/__tests__/integration.test.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/integration.test.js +415 -0
- package/dist/tools/codebase/ast/__tests__/integration.test.js.map +1 -0
- package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts +2 -0
- package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js +414 -0
- package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js.map +1 -0
- package/dist/tools/codebase/ast/adapter.d.ts +23 -0
- package/dist/tools/codebase/ast/adapter.d.ts.map +1 -0
- package/dist/tools/codebase/ast/adapter.js +11 -0
- package/dist/tools/codebase/ast/adapter.js.map +1 -0
- package/dist/tools/codebase/ast/tools.d.ts +3 -0
- package/dist/tools/codebase/ast/tools.d.ts.map +1 -0
- package/dist/tools/codebase/ast/tools.js +286 -0
- package/dist/tools/codebase/ast/tools.js.map +1 -0
- package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts +3 -0
- package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts.map +1 -0
- package/dist/tools/codebase/ast/tree-sitter-adapter.js +412 -0
- package/dist/tools/codebase/ast/tree-sitter-adapter.js.map +1 -0
- package/dist/tools/codebase/ast/tree-sitter-converters.d.ts +4 -0
- package/dist/tools/codebase/ast/tree-sitter-converters.d.ts.map +1 -0
- package/dist/tools/codebase/ast/tree-sitter-converters.js +447 -0
- package/dist/tools/codebase/ast/tree-sitter-converters.js.map +1 -0
- package/dist/tools/codebase/ast/types.d.ts +103 -0
- package/dist/tools/codebase/ast/types.d.ts.map +1 -0
- package/dist/tools/codebase/ast/types.js +2 -0
- package/dist/tools/codebase/ast/types.js.map +1 -0
- package/dist/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
- package/dist/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
- package/dist/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
- package/dist/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
- package/dist/tools/codebase/find.d.ts +3 -0
- package/dist/tools/codebase/find.d.ts.map +1 -0
- package/dist/tools/codebase/find.js +61 -0
- package/dist/tools/codebase/find.js.map +1 -0
- package/dist/tools/codebase/glob.d.ts +3 -0
- package/dist/tools/codebase/glob.d.ts.map +1 -0
- package/dist/tools/codebase/glob.js +37 -0
- package/dist/tools/codebase/glob.js.map +1 -0
- package/dist/tools/codebase/grep.d.ts +3 -0
- package/dist/tools/codebase/grep.d.ts.map +1 -0
- package/dist/tools/codebase/grep.js +169 -0
- package/dist/tools/codebase/grep.js.map +1 -0
- package/dist/tools/codebase/ls.d.ts +3 -0
- package/dist/tools/codebase/ls.d.ts.map +1 -0
- package/dist/tools/codebase/ls.js +39 -0
- package/dist/tools/codebase/ls.js.map +1 -0
- package/dist/tools/codebase/read.d.ts +10 -0
- package/dist/tools/codebase/read.d.ts.map +1 -0
- package/dist/tools/codebase/read.js +81 -0
- package/dist/tools/codebase/read.js.map +1 -0
- package/dist/tools/delete_memory.d.ts +3 -0
- package/dist/tools/delete_memory.d.ts.map +1 -0
- package/dist/tools/delete_memory.js +46 -0
- package/dist/tools/delete_memory.js.map +1 -0
- package/dist/tools/edit/diff-compute.d.ts +4 -0
- package/dist/tools/edit/diff-compute.d.ts.map +1 -0
- package/dist/tools/edit/diff-compute.js +18 -0
- package/dist/tools/edit/diff-compute.js.map +1 -0
- package/dist/tools/edit/diff-render.d.ts +18 -0
- package/dist/tools/edit/diff-render.d.ts.map +1 -0
- package/dist/tools/edit/diff-render.js +128 -0
- package/dist/tools/edit/diff-render.js.map +1 -0
- package/dist/tools/edit/edit.d.ts +4 -0
- package/dist/tools/edit/edit.d.ts.map +1 -0
- package/dist/tools/edit/edit.js +440 -0
- package/dist/tools/edit/edit.js.map +1 -0
- package/dist/tools/edit/matcher.d.ts +24 -0
- package/dist/tools/edit/matcher.d.ts.map +1 -0
- package/dist/tools/edit/matcher.js +66 -0
- package/dist/tools/edit/matcher.js.map +1 -0
- package/dist/tools/edit/parser.d.ts +10 -0
- package/dist/tools/edit/parser.d.ts.map +1 -0
- package/dist/tools/edit/parser.js +120 -0
- package/dist/tools/edit/parser.js.map +1 -0
- package/dist/tools/edit/types.d.ts +16 -0
- package/dist/tools/edit/types.d.ts.map +1 -0
- package/dist/tools/edit/types.js +2 -0
- package/dist/tools/edit/types.js.map +1 -0
- package/dist/tools/edit/write_plan.d.ts +5 -0
- package/dist/tools/edit/write_plan.d.ts.map +1 -0
- package/dist/tools/edit/write_plan.js +98 -0
- package/dist/tools/edit/write_plan.js.map +1 -0
- package/dist/tools/edit.d.ts +50 -9
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +394 -159
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/execute/bash/constants.d.ts +24 -0
- package/dist/tools/execute/bash/constants.d.ts.map +1 -0
- package/dist/tools/execute/bash/constants.js +88 -0
- package/dist/tools/execute/bash/constants.js.map +1 -0
- package/dist/tools/execute/bash/errors.d.ts +39 -0
- package/dist/tools/execute/bash/errors.d.ts.map +1 -0
- package/dist/tools/execute/bash/errors.js +249 -0
- package/dist/tools/execute/bash/errors.js.map +1 -0
- package/dist/tools/execute/bash/execution.d.ts +18 -0
- package/dist/tools/execute/bash/execution.d.ts.map +1 -0
- package/dist/tools/execute/bash/execution.js +207 -0
- package/dist/tools/execute/bash/execution.js.map +1 -0
- package/dist/tools/execute/bash/index.d.ts +3 -0
- package/dist/tools/execute/bash/index.d.ts.map +1 -0
- package/dist/tools/execute/bash/index.js +117 -0
- package/dist/tools/execute/bash/index.js.map +1 -0
- package/dist/tools/execute/bash/platform.d.ts +6 -0
- package/dist/tools/execute/bash/platform.d.ts.map +1 -0
- package/dist/tools/execute/bash/platform.js +64 -0
- package/dist/tools/execute/bash/platform.js.map +1 -0
- package/dist/tools/execute/bash/security.d.ts +12 -0
- package/dist/tools/execute/bash/security.d.ts.map +1 -0
- package/dist/tools/execute/bash/security.js +26 -0
- package/dist/tools/execute/bash/security.js.map +1 -0
- package/dist/tools/execute/bash/validation.d.ts +16 -0
- package/dist/tools/execute/bash/validation.d.ts.map +1 -0
- package/dist/tools/execute/bash/validation.js +57 -0
- package/dist/tools/execute/bash/validation.js.map +1 -0
- package/dist/tools/execute/bash.d.ts +3 -0
- package/dist/tools/execute/bash.d.ts.map +1 -0
- package/dist/tools/execute/bash.js +728 -0
- package/dist/tools/execute/bash.js.map +1 -0
- package/dist/tools/execute/git.d.ts +3 -0
- package/dist/tools/execute/git.d.ts.map +1 -0
- package/dist/tools/execute/git.js +183 -0
- package/dist/tools/execute/git.js.map +1 -0
- package/dist/tools/execute/task_complete.d.ts +7 -0
- package/dist/tools/execute/task_complete.d.ts.map +1 -0
- package/dist/tools/execute/task_complete.js +38 -0
- package/dist/tools/execute/task_complete.js.map +1 -0
- package/dist/tools/fetch.d.ts.map +1 -1
- package/dist/tools/fetch.js +2 -0
- package/dist/tools/fetch.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +2 -0
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/git.d.ts.map +1 -1
- package/dist/tools/git.js +2 -0
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/glob.d.ts.map +1 -1
- package/dist/tools/glob.js +2 -0
- package/dist/tools/glob.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +56 -30
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/install/clawhub.d.ts +26 -0
- package/dist/tools/install/clawhub.d.ts.map +1 -0
- package/dist/tools/install/clawhub.js +182 -0
- package/dist/tools/install/clawhub.js.map +1 -0
- package/dist/tools/install/constants.d.ts +5 -0
- package/dist/tools/install/constants.d.ts.map +1 -0
- package/dist/tools/install/constants.js +5 -0
- package/dist/tools/install/constants.js.map +1 -0
- package/dist/tools/install/install_skill.d.ts +3 -0
- package/dist/tools/install/install_skill.d.ts.map +1 -0
- package/dist/tools/install/install_skill.js +132 -0
- package/dist/tools/install/install_skill.js.map +1 -0
- package/dist/tools/install/npm.d.ts +8 -0
- package/dist/tools/install/npm.d.ts.map +1 -0
- package/dist/tools/install/npm.js +52 -0
- package/dist/tools/install/npm.js.map +1 -0
- package/dist/tools/install/skill-content.d.ts +9 -0
- package/dist/tools/install/skill-content.d.ts.map +1 -0
- package/dist/tools/install/skill-content.js +89 -0
- package/dist/tools/install/skill-content.js.map +1 -0
- package/dist/tools/install_skill.d.ts.map +1 -1
- package/dist/tools/install_skill.js +2 -0
- package/dist/tools/install_skill.js.map +1 -1
- package/dist/tools/list_memory.d.ts +3 -0
- package/dist/tools/list_memory.d.ts.map +1 -0
- package/dist/tools/list_memory.js +55 -0
- package/dist/tools/list_memory.js.map +1 -0
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +2 -0
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/memory/delete_memory.d.ts +3 -0
- package/dist/tools/memory/delete_memory.d.ts.map +1 -0
- package/dist/tools/memory/delete_memory.js +46 -0
- package/dist/tools/memory/delete_memory.js.map +1 -0
- package/dist/tools/memory/list_memory.d.ts +3 -0
- package/dist/tools/memory/list_memory.d.ts.map +1 -0
- package/dist/tools/memory/list_memory.js +55 -0
- package/dist/tools/memory/list_memory.js.map +1 -0
- package/dist/tools/memory/save_memory.d.ts +3 -0
- package/dist/tools/memory/save_memory.d.ts.map +1 -0
- package/dist/tools/memory/save_memory.js +45 -0
- package/dist/tools/memory/save_memory.js.map +1 -0
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +2 -0
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/registry.d.ts +7 -8
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +114 -46
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/save_memory.d.ts +3 -0
- package/dist/tools/save_memory.d.ts.map +1 -0
- package/dist/tools/save_memory.js +45 -0
- package/dist/tools/save_memory.js.map +1 -0
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +2 -0
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/sed.d.ts +4 -0
- package/dist/tools/sed.d.ts.map +1 -0
- package/dist/tools/sed.js +121 -0
- package/dist/tools/sed.js.map +1 -0
- package/dist/tools/tree.d.ts +12 -0
- package/dist/tools/tree.d.ts.map +1 -0
- package/dist/tools/tree.js +87 -0
- package/dist/tools/tree.js.map +1 -0
- package/dist/tools/types.d.ts +12 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/web/fetch.d.ts +3 -0
- package/dist/tools/web/fetch.d.ts.map +1 -0
- package/dist/tools/web/fetch.js +49 -0
- package/dist/tools/web/fetch.js.map +1 -0
- package/dist/tools/web/search.d.ts +3 -0
- package/dist/tools/web/search.d.ts.map +1 -0
- package/dist/tools/web/search.js +134 -0
- package/dist/tools/web/search.js.map +1 -0
- package/dist/tools/write_plan.d.ts.map +1 -1
- package/dist/tools/write_plan.js +2 -0
- package/dist/tools/write_plan.js.map +1 -1
- package/dist/tui/commands.d.ts +4 -0
- package/dist/tui/commands.d.ts.map +1 -0
- package/dist/tui/commands.js +118 -0
- package/dist/tui/commands.js.map +1 -0
- package/dist/tui/components/colors.d.ts +36 -0
- package/dist/tui/components/colors.d.ts.map +1 -0
- package/dist/tui/components/colors.js +48 -0
- package/dist/tui/components/colors.js.map +1 -0
- package/dist/tui/components/header-bar.d.ts +26 -0
- package/dist/tui/components/header-bar.d.ts.map +1 -0
- package/dist/tui/components/header-bar.js +78 -0
- package/dist/tui/components/header-bar.js.map +1 -0
- package/dist/tui/components/index.d.ts +9 -0
- package/dist/tui/components/index.d.ts.map +1 -0
- package/dist/tui/components/index.js +9 -0
- package/dist/tui/components/index.js.map +1 -0
- package/dist/tui/components/landing-screen.d.ts +12 -0
- package/dist/tui/components/landing-screen.d.ts.map +1 -0
- package/dist/tui/components/landing-screen.js +53 -0
- package/dist/tui/components/landing-screen.js.map +1 -0
- package/dist/tui/components/pixel-logo.d.ts +5 -0
- package/dist/tui/components/pixel-logo.d.ts.map +1 -0
- package/dist/tui/components/pixel-logo.js +27 -0
- package/dist/tui/components/pixel-logo.js.map +1 -0
- package/dist/tui/components/plan-utils.d.ts +11 -0
- package/dist/tui/components/plan-utils.d.ts.map +1 -0
- package/dist/tui/components/plan-utils.js +62 -0
- package/dist/tui/components/plan-utils.js.map +1 -0
- package/dist/tui/components/plans-list.d.ts +18 -0
- package/dist/tui/components/plans-list.d.ts.map +1 -0
- package/dist/tui/components/plans-list.js +46 -0
- package/dist/tui/components/plans-list.js.map +1 -0
- package/dist/tui/components/rich-footer.d.ts +29 -0
- package/dist/tui/components/rich-footer.d.ts.map +1 -0
- package/dist/tui/components/rich-footer.js +124 -0
- package/dist/tui/components/rich-footer.js.map +1 -0
- package/dist/tui/components/todo-panel.d.ts +14 -0
- package/dist/tui/components/todo-panel.d.ts.map +1 -0
- package/dist/tui/components/todo-panel.js +83 -0
- package/dist/tui/components/todo-panel.js.map +1 -0
- package/dist/tui/components.d.ts.map +1 -1
- package/dist/tui/components.js +14 -7
- package/dist/tui/components.js.map +1 -1
- package/dist/tui/index.d.ts +2 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +99 -505
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/overlays.d.ts +28 -0
- package/dist/tui/overlays.d.ts.map +1 -0
- package/dist/tui/overlays.js +154 -0
- package/dist/tui/overlays.js.map +1 -0
- package/dist/tui/themes.d.ts +5 -0
- package/dist/tui/themes.d.ts.map +1 -0
- package/dist/tui/themes.js +38 -0
- package/dist/tui/themes.js.map +1 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +62 -101
- package/dist/web/index.js.map +1 -1
- package/dist/web/public/app.js +5 -855
- package/dist/web/public/confirm.js +70 -0
- package/dist/web/public/index.html +121 -120
- package/dist/web/public/input.js +107 -0
- package/dist/web/public/messages.js +411 -0
- package/dist/web/public/sidebar.js +82 -0
- package/dist/web/public/state.js +72 -0
- package/dist/web/public/style.css +247 -70
- package/dist/web/public/utils.js +37 -0
- package/dist/web/public/websocket.js +267 -0
- package/dist/web/public/ws.js +19 -0
- package/dist/web/session-bridge.d.ts.map +1 -1
- package/dist/web/session-bridge.js +85 -10
- package/dist/web/session-bridge.js.map +1 -1
- package/docs/ast-tools.md +117 -0
- package/package.json +11 -5
- package/scripts/check-line-count.mjs +53 -0
- package/scripts/copy-ast-wasm.mjs +32 -0
- package/scripts/copy-native.mjs +24 -0
- package/scripts/copy-web.mjs +15 -0
- package/src/agent/__tests__/session-restore.test.ts +284 -0
- package/src/agent/commands.ts +274 -0
- package/src/agent/compaction.ts +9 -2
- package/src/agent/index.ts +7 -2
- package/src/agent/llm.ts +4 -0
- package/src/agent/memory.ts +60 -0
- package/src/agent/prompt-builder.ts +111 -33
- package/src/agent/providers/google.ts +0 -1
- package/src/agent/providers/openai.ts +79 -8
- package/src/agent/session-chat.ts +480 -0
- package/src/agent/session-display.ts +195 -0
- package/src/agent/session-persistence.ts +146 -0
- package/src/agent/session-utils.ts +54 -0
- package/src/agent/session.ts +269 -670
- package/src/cli/index.ts +73 -4
- package/src/config/index.ts +14 -7
- package/src/index.ts +30 -3
- package/src/tools/__tests__/bash.test.ts +188 -2
- package/src/tools/__tests__/edit.test.ts +1216 -356
- package/src/tools/__tests__/fetch.test.ts +1 -1
- package/src/tools/__tests__/glob.test.ts +1 -1
- package/src/tools/__tests__/grep.test.ts +36 -1
- package/src/tools/__tests__/ls.test.ts +1 -1
- package/src/tools/__tests__/read.test.ts +1 -1
- package/src/tools/__tests__/registry.test.ts +332 -0
- package/src/tools/__tests__/write_plan.test.ts +1 -1
- package/src/tools/codebase/ast/__tests__/integration.test.ts +452 -0
- package/src/tools/codebase/ast/__tests__/tree-sitter.test.ts +466 -0
- package/src/tools/codebase/ast/adapter.ts +44 -0
- package/src/tools/codebase/ast/tools.ts +322 -0
- package/src/tools/codebase/ast/tree-sitter-adapter.ts +468 -0
- package/src/tools/codebase/ast/tree-sitter-converters.ts +480 -0
- package/src/tools/codebase/ast/types.ts +102 -0
- package/src/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
- package/src/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
- package/src/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
- package/src/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
- package/src/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
- package/src/tools/{find.ts → codebase/find.ts} +4 -2
- package/src/tools/{glob.ts → codebase/glob.ts} +4 -2
- package/src/tools/{grep.ts → codebase/grep.ts} +61 -32
- package/src/tools/{ls.ts → codebase/ls.ts} +4 -2
- package/src/tools/{read.ts → codebase/read.ts} +5 -3
- package/src/tools/edit/diff-compute.ts +21 -0
- package/src/tools/edit/diff-render.ts +145 -0
- package/src/tools/edit/edit.ts +468 -0
- package/src/tools/edit/matcher.ts +70 -0
- package/src/tools/edit/parser.ts +131 -0
- package/src/tools/edit/types.ts +19 -0
- package/src/tools/{write_plan.ts → edit/write_plan.ts} +4 -2
- package/src/tools/execute/bash/constants.ts +93 -0
- package/src/tools/execute/bash/errors.ts +304 -0
- package/src/tools/execute/bash/execution.ts +211 -0
- package/src/tools/execute/bash/index.ts +131 -0
- package/src/tools/execute/bash/platform.ts +65 -0
- package/src/tools/execute/bash/security.ts +28 -0
- package/src/tools/execute/bash/validation.ts +78 -0
- package/src/tools/execute/git.ts +209 -0
- package/src/tools/execute/task_complete.ts +40 -0
- package/src/tools/install/clawhub.ts +222 -0
- package/src/tools/install/constants.ts +4 -0
- package/src/tools/install/install_skill.ts +150 -0
- package/src/tools/install/npm.ts +60 -0
- package/src/tools/install/skill-content.ts +108 -0
- package/src/tools/memory/delete_memory.ts +46 -0
- package/src/tools/memory/list_memory.ts +53 -0
- package/src/tools/memory/save_memory.ts +45 -0
- package/src/tools/registry.ts +121 -50
- package/src/tools/tree.ts +103 -0
- package/src/tools/types.ts +13 -0
- package/src/tools/{fetch.ts → web/fetch.ts} +4 -2
- package/src/tools/{search.ts → web/search.ts} +4 -2
- package/src/tui/commands.ts +135 -0
- package/src/tui/components/colors.ts +52 -0
- package/src/tui/components/header-bar.ts +88 -0
- package/src/tui/components/index.ts +8 -0
- package/src/tui/components/landing-screen.ts +67 -0
- package/src/tui/components/pixel-logo.ts +33 -0
- package/src/tui/components/plan-utils.ts +69 -0
- package/src/tui/components/plans-list.ts +55 -0
- package/src/tui/components/rich-footer.ts +146 -0
- package/src/tui/components/todo-panel.ts +95 -0
- package/src/tui/index.ts +110 -561
- package/src/tui/overlays.ts +203 -0
- package/src/tui/themes.ts +41 -0
- package/src/web/index.ts +66 -107
- package/src/web/public/app.js +5 -855
- package/src/web/public/confirm.js +70 -0
- package/src/web/public/index.html +121 -120
- package/src/web/public/input.js +107 -0
- package/src/web/public/messages.js +411 -0
- package/src/web/public/sidebar.js +82 -0
- package/src/web/public/state.js +72 -0
- package/src/web/public/style.css +247 -70
- package/src/web/public/utils.js +37 -0
- package/src/web/public/websocket.js +267 -0
- package/src/web/public/ws.js +19 -0
- package/src/web/session-bridge.ts +90 -12
- package/.claude/settings.local.json +0 -14
- package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
- package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
- package/.lonny/plan-web-cwd-status.md +0 -38
- package/AGENT.md +0 -99
- package/src/tools/bash.ts +0 -39
- package/src/tools/edit.ts +0 -554
- package/src/tools/exec.ts +0 -348
- package/src/tools/git.ts +0 -76
- package/src/tools/install_skill.ts +0 -539
- package/src/tui/components.ts +0 -635
package/src/agent/session.ts
CHANGED
|
@@ -1,236 +1,40 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
1
|
import * as fs from 'node:fs'
|
|
3
|
-
import * as os from 'node:os'
|
|
4
2
|
import * as path from 'node:path'
|
|
5
3
|
import type { Config } from '../config/index.js'
|
|
6
|
-
import { saveTokenUsage } from '../config/tokens.js'
|
|
7
4
|
import { FileReadTracker } from '../diff/apply.js'
|
|
8
|
-
import { fmtErr } from '../tools/errors.js'
|
|
9
5
|
import { ToolRegistry } from '../tools/registry.js'
|
|
10
|
-
import type { ToolCall
|
|
11
|
-
import { compact, estimateMessagesTokens, shouldCompact } from './compaction.js'
|
|
12
|
-
import { EventChannels, getGlobalEventBus } from './event-bus.js'
|
|
6
|
+
import type { ToolCall } from '../tools/types.js'
|
|
13
7
|
import type { LLMMessage, LLMProvider } from './llm.js'
|
|
14
8
|
import { buildSystemPrompt } from './prompt-builder.js'
|
|
15
9
|
import { AnthropicProvider } from './providers/anthropic.js'
|
|
16
10
|
import { GoogleProvider } from './providers/google.js'
|
|
17
11
|
import { OllamaProvider } from './providers/ollama.js'
|
|
18
12
|
import { OpenAIProvider } from './providers/openai.js'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getSessionFilePath(cwd: string): string {
|
|
39
|
-
const absPath = path.resolve(cwd)
|
|
40
|
-
const hash = createHash('sha256').update(absPath, 'utf-8').digest('hex').slice(0, 12)
|
|
41
|
-
const dirName = path.basename(absPath)
|
|
42
|
-
const safeName = dirName.replace(/[<>:"/\\|?*]/g, '_')
|
|
43
|
-
return path.join(getSessionDir(), `${safeName}-${hash}.json`)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function ensureDir(dir: string): void {
|
|
47
|
-
if (!fs.existsSync(dir)) {
|
|
48
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ── Colors ─────────────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
const CY = '\x1b[36m'
|
|
55
|
-
const GR = '\x1b[32m'
|
|
56
|
-
const YE = '\x1b[33m'
|
|
57
|
-
const RE = '\x1b[31m'
|
|
58
|
-
const MG = '\x1b[35m'
|
|
59
|
-
const GY = '\x1b[90m'
|
|
60
|
-
const RS = '\x1b[0m'
|
|
61
|
-
const BLD = '\x1b[1m'
|
|
62
|
-
const TH = '\x1b[48;2;22;22;32m\x1b[38;2;150;150;170m' // dark bg + dim fg for thinking
|
|
63
|
-
|
|
64
|
-
/** Get terminal width (columns), default to 80. */
|
|
65
|
-
function termWidth(): number {
|
|
66
|
-
return process.stdout.columns ?? 80
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Visible width of a string (strip ANSI codes). ASCII=1, CJK/non-ASCII=2. */
|
|
70
|
-
function visibleWidth(s: string): number {
|
|
71
|
-
let w = 0
|
|
72
|
-
for (let i = 0; i < s.length; i++) {
|
|
73
|
-
if (s.charCodeAt(i) === 0x1b) {
|
|
74
|
-
// Skip past escape sequence
|
|
75
|
-
while (i < s.length && s[i] !== 'm') i++
|
|
76
|
-
continue
|
|
77
|
-
}
|
|
78
|
-
w += s.charCodeAt(i) > 0x7e ? 2 : 1
|
|
79
|
-
}
|
|
80
|
-
return w
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Visible prefix width for thinking box lines: " │" = 3 */
|
|
84
|
-
const THINK_PREFIX_WIDTH = 3
|
|
85
|
-
|
|
86
|
-
/** Build the top border of the thinking box */
|
|
87
|
-
function thinkTopBorder(): string {
|
|
88
|
-
return `\n ${GY}╭───────${RS}${TH} Think ${GY}────────────────────${RS}\n`
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Build the bottom border of the thinking box */
|
|
92
|
-
function thinkBottomBorder(): string {
|
|
93
|
-
return ` ${GY}╰${'─'.repeat(42)}${RS}\n\n`
|
|
94
|
-
}
|
|
13
|
+
import { runChat } from './session-chat.js'
|
|
14
|
+
import {
|
|
15
|
+
ensureDir,
|
|
16
|
+
findLegacySessionFile,
|
|
17
|
+
generateId,
|
|
18
|
+
getSessionDir,
|
|
19
|
+
getSessionFilePath,
|
|
20
|
+
getSessionFilesForCwd,
|
|
21
|
+
migrateSessionData,
|
|
22
|
+
type SessionData,
|
|
23
|
+
type SessionInfo,
|
|
24
|
+
} from './session-persistence.js'
|
|
25
|
+
import { sanitizeMessages } from './session-utils.js'
|
|
26
|
+
|
|
27
|
+
export { formatToolInput } from './session-display.js'
|
|
28
|
+
|
|
29
|
+
// ── Session output interface ────────────────────────────────────────────────
|
|
95
30
|
|
|
96
31
|
export interface SessionOutput {
|
|
97
32
|
write: (text: string) => void
|
|
98
|
-
/** When true, tool invocation/result formatting is skipped (TUI handles it via event bus) */
|
|
99
33
|
suppressToolOutput?: boolean
|
|
100
|
-
/**
|
|
101
|
-
* When autoApprove is false, called before dispatching write-type tool calls.
|
|
102
|
-
* Return true to allow execution, false to reject. Tools are batched — one confirmation per turn.
|
|
103
|
-
*/
|
|
104
34
|
confirmTool?: (toolCalls: ToolCall[]) => Promise<boolean>
|
|
105
35
|
}
|
|
106
36
|
|
|
107
|
-
|
|
108
|
-
if (output) {
|
|
109
|
-
output.write(text)
|
|
110
|
-
} else {
|
|
111
|
-
process.stdout.write(text)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function printUserMessage(prompt: string, output?: SessionOutput): void {
|
|
116
|
-
// When suppressToolOutput is true (Web UI mode), the frontend already
|
|
117
|
-
// displays the user message, so skip sending it as a chunk.
|
|
118
|
-
if (output?.suppressToolOutput) return
|
|
119
|
-
const line = ` ${GY}┃${RS} ${BLD}${CY}You${RS}`
|
|
120
|
-
writeOut(`\n${line} ${prompt}\n\n`, output)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function printToolInvocation(tc: ToolCall, output?: SessionOutput): void {
|
|
124
|
-
const detail = formatToolInput(tc)
|
|
125
|
-
const isWrite = tc.name === 'write_plan' || tc.name === 'edit'
|
|
126
|
-
const icon = isWrite ? `${YE}◆${RS}` : `${GR}◇${RS}`
|
|
127
|
-
const label = isWrite ? `${YE}${tc.name}${RS}` : `${GR}${tc.name}${RS}`
|
|
128
|
-
writeOut(`\n ${GY}│${RS} ${icon} ${label}${detail ? ` ${GY}${detail}${RS}` : ''}\n`, output)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function printToolResult(tc: ToolCall, result: ToolResult, output?: SessionOutput): void {
|
|
132
|
-
if (!result.success) {
|
|
133
|
-
writeOut(` ${GY}│${RS} ${RE}✖${RS} ${RE}${result.error}${RS}\n`, output)
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
if (tc.name === 'read') {
|
|
137
|
-
const fileCount = (result.output.match(/^=== /gm) || []).length
|
|
138
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} read ${fileCount} file(s)\n`, output)
|
|
139
|
-
for (const line of result.output.split('\n')) {
|
|
140
|
-
if (line.startsWith('=== ')) {
|
|
141
|
-
const fp = line.slice(4, line.includes(' ===') ? line.indexOf(' ===') + 4 : undefined)
|
|
142
|
-
writeOut(` ${GY}│${RS} ${GY}${fp}${RS}\n`, output)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
} else if (tc.name === 'glob') {
|
|
146
|
-
const count = result.output.split('\n').filter(l => l && !l.startsWith('No')).length
|
|
147
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} glob ${count} match(es)\n`, output)
|
|
148
|
-
} else if (tc.name === 'grep') {
|
|
149
|
-
const count = result.output.split('\n').filter(l => l && !l.startsWith('No')).length
|
|
150
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} grep ${count} match(es)\n`, output)
|
|
151
|
-
} else if (tc.name === 'bash') {
|
|
152
|
-
const outLines = result.output.split('\n')
|
|
153
|
-
const summary = outLines.length > 1 ? `(${outLines.length} lines)` : ''
|
|
154
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} bash ${summary}\n`, output)
|
|
155
|
-
} else if (tc.name === 'edit') {
|
|
156
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} edit\n`, output)
|
|
157
|
-
if (result.output) {
|
|
158
|
-
for (const l of result.output.split('\n')) {
|
|
159
|
-
if (l.trim()) writeOut(` ${GY}│${RS} ${l.trim()}\n`, output)
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
} else if (tc.name === 'write_plan') {
|
|
163
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} ${result.output || tc.name}\n`, output)
|
|
164
|
-
} else if (tc.name === 'search') {
|
|
165
|
-
writeOut(
|
|
166
|
-
` ${GY}│${RS} ${GR}✔${RS} search: ${String(tc.input.query || '').slice(0, 80)}\n`,
|
|
167
|
-
output,
|
|
168
|
-
)
|
|
169
|
-
} else {
|
|
170
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} ${tc.name}\n`, output)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
interface SingleEditShape {
|
|
175
|
-
file_path: string
|
|
176
|
-
old_string: string
|
|
177
|
-
new_string: string
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function isSingleEditShape(v: unknown): v is SingleEditShape {
|
|
181
|
-
return (
|
|
182
|
-
typeof v === 'object' &&
|
|
183
|
-
v !== null &&
|
|
184
|
-
'file_path' in v &&
|
|
185
|
-
'old_string' in v &&
|
|
186
|
-
'new_string' in v
|
|
187
|
-
)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export function formatToolInput(tc: ToolCall): string {
|
|
191
|
-
const parts: string[] = []
|
|
192
|
-
if (tc.name === 'read' && Array.isArray(tc.input.paths)) {
|
|
193
|
-
parts.push(tc.input.paths.join(', '))
|
|
194
|
-
} else if (tc.name === 'glob' && typeof tc.input.pattern === 'string') {
|
|
195
|
-
parts.push(tc.input.pattern)
|
|
196
|
-
} else if (tc.name === 'grep') {
|
|
197
|
-
if (typeof tc.input.pattern === 'string') parts.push(`/${tc.input.pattern}/`)
|
|
198
|
-
if (typeof tc.input.include === 'string') parts.push(`in:${tc.input.include}`)
|
|
199
|
-
} else if (tc.name === 'ls') {
|
|
200
|
-
parts.push(typeof tc.input.path === 'string' ? tc.input.path : '.')
|
|
201
|
-
} else if (tc.name === 'bash') {
|
|
202
|
-
const cmd = typeof tc.input.command === 'string' ? tc.input.command : ''
|
|
203
|
-
parts.push(cmd.length > 80 ? `${cmd.slice(0, 80)}\u2026` : cmd)
|
|
204
|
-
} else if (tc.name === 'search') {
|
|
205
|
-
if (typeof tc.input.query === 'string') parts.push(tc.input.query.slice(0, 120))
|
|
206
|
-
} else if (tc.name === 'write_plan') {
|
|
207
|
-
if (typeof tc.input.filename === 'string') parts.push(tc.input.filename)
|
|
208
|
-
} else if (tc.name === 'edit') {
|
|
209
|
-
if (Array.isArray(tc.input.edits) && tc.input.edits.every(isSingleEditShape)) {
|
|
210
|
-
const paths = tc.input.edits.map(e => e.file_path)
|
|
211
|
-
parts.push(paths.join(', '))
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return parts.join(' \u2502 ')
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function printTokenStats(
|
|
218
|
-
turnIn: number,
|
|
219
|
-
turnOut: number,
|
|
220
|
-
totalIn: number,
|
|
221
|
-
totalOut: number,
|
|
222
|
-
turnApi: number,
|
|
223
|
-
totalApi: number,
|
|
224
|
-
output?: SessionOutput,
|
|
225
|
-
): void {
|
|
226
|
-
const bus = getGlobalEventBus()
|
|
227
|
-
bus.emit(EventChannels.TOKEN_STATS, { turnIn, turnOut, totalIn, totalOut, turnApi, totalApi })
|
|
228
|
-
// Skip terminal output in Web UI mode
|
|
229
|
-
if (output?.suppressToolOutput) return
|
|
230
|
-
const total = totalIn + totalOut
|
|
231
|
-
const msg = ` ${GY}┃${RS} ${GY}${BLD}▴${RS}${GY}${turnIn}${RS} ${GY}${BLD}▾${RS}${GY}${turnOut}${RS} ${GY}total${RS} ${total} ${GY}calls${RS} ${turnApi}(${totalApi})`
|
|
232
|
-
writeOut(`\n${msg}\n`, output)
|
|
233
|
-
}
|
|
37
|
+
// ── Session class ────────────────────────────────────────────────────────────
|
|
234
38
|
|
|
235
39
|
export class Session {
|
|
236
40
|
messages: LLMMessage[]
|
|
@@ -240,12 +44,10 @@ export class Session {
|
|
|
240
44
|
config: Config
|
|
241
45
|
output?: SessionOutput
|
|
242
46
|
private _onPlanWritten?: (display: string) => void
|
|
243
|
-
|
|
47
|
+
|
|
244
48
|
set onPlanWritten(cb: ((display: string) => void) | undefined) {
|
|
245
49
|
this._onPlanWritten = cb
|
|
246
|
-
// Update context so setMode() picks it up too
|
|
247
50
|
this.registry.updateContext({ onPlanWritten: cb })
|
|
248
|
-
// Re-register write_plan tool with the new callback
|
|
249
51
|
if (this.registry.has('write_plan')) {
|
|
250
52
|
this.registry.reRegisterWritePlan(this.config.cwd, cb)
|
|
251
53
|
}
|
|
@@ -259,8 +61,12 @@ export class Session {
|
|
|
259
61
|
turnOutputTokens: number = 0
|
|
260
62
|
turnApiCalls: number = 0
|
|
261
63
|
totalApiCalls: number = 0
|
|
262
|
-
|
|
263
|
-
|
|
64
|
+
turnCacheHitTokens: number = 0
|
|
65
|
+
turnCacheMissTokens: number = 0
|
|
66
|
+
totalCacheHitTokens: number = 0
|
|
67
|
+
totalCacheMissTokens: number = 0
|
|
68
|
+
stopped: boolean = false
|
|
69
|
+
abortController: AbortController | null = null
|
|
264
70
|
|
|
265
71
|
constructor(config: Config, output?: SessionOutput) {
|
|
266
72
|
this.config = config
|
|
@@ -292,521 +98,314 @@ export class Session {
|
|
|
292
98
|
this.provider = new AnthropicProvider(config.apiKey, config.baseUrl, config.model)
|
|
293
99
|
}
|
|
294
100
|
|
|
295
|
-
// Placeholder until initSystemPrompt() is called
|
|
296
101
|
this.messages = [{ role: 'system', content: '' }]
|
|
297
|
-
// Initialize system prompt asynchronously
|
|
298
102
|
this.initSystemPrompt(config)
|
|
299
103
|
}
|
|
300
104
|
|
|
301
|
-
/** Initialize the system prompt asynchronously */
|
|
302
105
|
private initSystemPrompt(config: Config): void {
|
|
303
|
-
buildSystemPrompt(config).then(prompt => {
|
|
304
|
-
this.messages
|
|
106
|
+
buildSystemPrompt(config, this.registry.getDefinitions()).then(prompt => {
|
|
107
|
+
if (this.messages.length <= 1) {
|
|
108
|
+
this.messages = [{ role: 'system', content: prompt }]
|
|
109
|
+
}
|
|
305
110
|
})
|
|
306
111
|
}
|
|
307
112
|
|
|
308
|
-
|
|
113
|
+
sessionId: string = generateId()
|
|
114
|
+
sessionTitle: string = ''
|
|
115
|
+
sessionCreatedAt: string = new Date().toISOString()
|
|
116
|
+
|
|
309
117
|
save(): void {
|
|
310
118
|
const dir = getSessionDir()
|
|
311
119
|
ensureDir(dir)
|
|
312
|
-
const filePath = getSessionFilePath(this.config.cwd)
|
|
120
|
+
const filePath = getSessionFilePath(this.config.cwd, this.sessionId)
|
|
121
|
+
const now = new Date().toISOString()
|
|
122
|
+
let title = this.sessionTitle
|
|
123
|
+
if (!title) {
|
|
124
|
+
const firstUserMsg = this.messages.find(m => m.role === 'user')
|
|
125
|
+
if (firstUserMsg && typeof firstUserMsg.content === 'string') {
|
|
126
|
+
title = firstUserMsg.content.slice(0, 80).replace(/\n/g, ' ')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
313
129
|
const data: SessionData = {
|
|
130
|
+
id: this.sessionId,
|
|
131
|
+
title: title || undefined,
|
|
314
132
|
cwd: path.resolve(this.config.cwd),
|
|
315
133
|
messages: this.messages,
|
|
316
134
|
totalInputTokens: this.totalInputTokens,
|
|
317
135
|
totalOutputTokens: this.totalOutputTokens,
|
|
318
136
|
totalApiCalls: this.totalApiCalls,
|
|
137
|
+
totalCacheHitTokens: this.totalCacheHitTokens || undefined,
|
|
138
|
+
totalCacheMissTokens: this.totalCacheMissTokens || undefined,
|
|
319
139
|
mode: this.config.mode,
|
|
320
140
|
model: this.config.model,
|
|
321
141
|
provider: this.config.provider,
|
|
322
|
-
|
|
142
|
+
createdAt: this.sessionCreatedAt,
|
|
143
|
+
updatedAt: now,
|
|
323
144
|
}
|
|
324
145
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
325
146
|
}
|
|
326
147
|
|
|
327
|
-
/** Try to load a saved session for the given cwd. Returns null if none exists. */
|
|
328
148
|
static async load(config: Config, output?: SessionOutput): Promise<Session | null> {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
149
|
+
const cwdSessions = getSessionFilesForCwd(config.cwd)
|
|
150
|
+
console.log(
|
|
151
|
+
`[session] Found ${cwdSessions.length} session files for cwd "${config.cwd}":`,
|
|
152
|
+
cwdSessions
|
|
153
|
+
.map(
|
|
154
|
+
f =>
|
|
155
|
+
`${f.fileName} (updatedAt=${f.data.updatedAt?.slice(0, 19)}, messages=${f.data.messages?.length || 0})`,
|
|
156
|
+
)
|
|
157
|
+
.join(', '),
|
|
158
|
+
)
|
|
159
|
+
if (cwdSessions.length > 0) {
|
|
160
|
+
cwdSessions.sort((a, b) => {
|
|
161
|
+
const aLen = a.data.messages?.length || 0
|
|
162
|
+
const bLen = b.data.messages?.length || 0
|
|
163
|
+
if (aLen !== bLen) return bLen - aLen
|
|
164
|
+
return b.data.updatedAt.localeCompare(a.data.updatedAt)
|
|
165
|
+
})
|
|
166
|
+
console.log(`[session] Loading session file: ${cwdSessions[0].fileName}`)
|
|
167
|
+
return Session.loadFromData(cwdSessions[0].data, config, output)
|
|
335
168
|
}
|
|
336
169
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
170
|
+
const legacyPath = findLegacySessionFile(config.cwd)
|
|
171
|
+
if (legacyPath) {
|
|
172
|
+
try {
|
|
173
|
+
const raw = JSON.parse(fs.readFileSync(legacyPath, 'utf-8')) as Record<string, unknown>
|
|
174
|
+
const data = migrateSessionData(raw)
|
|
175
|
+
const session = await Session.loadFromData(data, config, output)
|
|
176
|
+
session.save()
|
|
177
|
+
try {
|
|
178
|
+
fs.unlinkSync(legacyPath)
|
|
179
|
+
} catch {
|
|
180
|
+
/* ignore */
|
|
181
|
+
}
|
|
182
|
+
return session
|
|
183
|
+
} catch {
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
static async loadById(
|
|
192
|
+
sessionId: string,
|
|
193
|
+
config: Config,
|
|
194
|
+
output?: SessionOutput,
|
|
195
|
+
): Promise<Session | null> {
|
|
196
|
+
const allSessions = Session.listSessions()
|
|
197
|
+
const target = allSessions.find(s => s.id === sessionId)
|
|
198
|
+
if (!target) return null
|
|
199
|
+
const legacyPath = findLegacySessionFile(config.cwd)
|
|
200
|
+
if (legacyPath) {
|
|
201
|
+
try {
|
|
202
|
+
const raw = JSON.parse(fs.readFileSync(legacyPath, 'utf-8')) as Record<string, unknown>
|
|
203
|
+
const data = migrateSessionData(raw)
|
|
204
|
+
if (data.id === sessionId) {
|
|
205
|
+
return Session.loadFromData(data, config, output)
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
/* ignore */
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const filePath = getSessionFilePath(target.cwd, sessionId)
|
|
212
|
+
try {
|
|
213
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, unknown>
|
|
214
|
+
return Session.loadFromData(migrateSessionData(raw), config, output)
|
|
215
|
+
} catch {
|
|
341
216
|
return null
|
|
342
217
|
}
|
|
218
|
+
}
|
|
343
219
|
|
|
220
|
+
private static async loadFromData(
|
|
221
|
+
data: SessionData,
|
|
222
|
+
config: Config,
|
|
223
|
+
output?: SessionOutput,
|
|
224
|
+
): Promise<Session> {
|
|
344
225
|
const session = new Session(config, output)
|
|
345
|
-
// Restore messages (replace the default system prompt with the saved one)
|
|
346
226
|
session.messages = data.messages
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
data.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
227
|
+
console.log(`[session] Loading session ${data.id}: ${data.messages.length} messages from disk`)
|
|
228
|
+
session.messages = sanitizeMessages(session.messages)
|
|
229
|
+
console.log(
|
|
230
|
+
`[session] After sanitize: ${session.messages.length} messages (removed ${data.messages.length - session.messages.length})`,
|
|
231
|
+
)
|
|
232
|
+
session.sessionId = data.id
|
|
233
|
+
session.sessionTitle = data.title || ''
|
|
234
|
+
session.sessionCreatedAt = data.createdAt
|
|
235
|
+
config.mode = data.mode
|
|
236
|
+
session.registry.setMode(data.mode)
|
|
237
|
+
if (data.model !== config.model || data.provider !== config.provider) {
|
|
238
|
+
const prompt = await buildSystemPrompt(config, session.registry.getDefinitions())
|
|
355
239
|
session.messages[0] = { role: 'system', content: prompt }
|
|
356
240
|
}
|
|
357
|
-
// Restore token stats
|
|
358
241
|
session.totalInputTokens = data.totalInputTokens
|
|
359
242
|
session.totalOutputTokens = data.totalOutputTokens
|
|
360
243
|
session.totalApiCalls = data.totalApiCalls
|
|
244
|
+
session.totalCacheHitTokens = data.totalCacheHitTokens ?? 0
|
|
245
|
+
session.totalCacheMissTokens = data.totalCacheMissTokens ?? 0
|
|
361
246
|
return session
|
|
362
247
|
}
|
|
363
248
|
|
|
364
|
-
/** Remove the saved session file for the given cwd. */
|
|
365
249
|
static clearSavedSession(cwd: string): void {
|
|
366
|
-
const
|
|
250
|
+
const cwdSessions = getSessionFilesForCwd(cwd)
|
|
251
|
+
for (const { fileName } of cwdSessions) {
|
|
252
|
+
try {
|
|
253
|
+
fs.unlinkSync(path.join(getSessionDir(), fileName))
|
|
254
|
+
} catch {
|
|
255
|
+
/* ignore */
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const legacyPath = findLegacySessionFile(cwd)
|
|
259
|
+
if (legacyPath) {
|
|
260
|
+
try {
|
|
261
|
+
fs.unlinkSync(legacyPath)
|
|
262
|
+
} catch {
|
|
263
|
+
/* ignore */
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
static listSessions(maxCount?: number): SessionInfo[] {
|
|
269
|
+
const dir = getSessionDir()
|
|
367
270
|
try {
|
|
368
|
-
fs.
|
|
271
|
+
if (!fs.existsSync(dir)) return []
|
|
272
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'))
|
|
273
|
+
const sessions: SessionInfo[] = []
|
|
274
|
+
for (const fileName of files) {
|
|
275
|
+
try {
|
|
276
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, fileName), 'utf-8')) as Record<
|
|
277
|
+
string,
|
|
278
|
+
unknown
|
|
279
|
+
>
|
|
280
|
+
const data = migrateSessionData(raw)
|
|
281
|
+
const firstUserMsg = data.messages.find(m => m.role === 'user')
|
|
282
|
+
let title = data.title || ''
|
|
283
|
+
if (!title && firstUserMsg && typeof firstUserMsg.content === 'string') {
|
|
284
|
+
title = firstUserMsg.content.slice(0, 80).replace(/\n/g, ' ')
|
|
285
|
+
}
|
|
286
|
+
sessions.push({
|
|
287
|
+
id: data.id,
|
|
288
|
+
cwd: data.cwd,
|
|
289
|
+
title,
|
|
290
|
+
messageCount: data.messages.length,
|
|
291
|
+
mode: data.mode,
|
|
292
|
+
model: data.model,
|
|
293
|
+
provider: data.provider,
|
|
294
|
+
totalInputTokens: data.totalInputTokens,
|
|
295
|
+
totalOutputTokens: data.totalOutputTokens,
|
|
296
|
+
totalApiCalls: data.totalApiCalls,
|
|
297
|
+
createdAt: data.createdAt,
|
|
298
|
+
updatedAt: data.updatedAt,
|
|
299
|
+
fileName,
|
|
300
|
+
})
|
|
301
|
+
} catch {
|
|
302
|
+
// Skip corrupted files
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
306
|
+
if (maxCount && maxCount > 0) {
|
|
307
|
+
return sessions.slice(0, maxCount)
|
|
308
|
+
}
|
|
309
|
+
return sessions
|
|
310
|
+
} catch {
|
|
311
|
+
return []
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
static deleteSession(id: string): boolean {
|
|
316
|
+
const sessions = Session.listSessions()
|
|
317
|
+
const target = sessions.find(s => s.id === id)
|
|
318
|
+
if (!target) return false
|
|
319
|
+
try {
|
|
320
|
+
fs.unlinkSync(path.join(getSessionDir(), target.fileName))
|
|
321
|
+
return true
|
|
369
322
|
} catch {
|
|
370
|
-
|
|
323
|
+
return false
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
clearSavedSession(): void {
|
|
328
|
+
Session.clearSavedSession(this.config.cwd)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
fork(): Session {
|
|
332
|
+
const forked = new Session(this.config, this.output)
|
|
333
|
+
forked.messages = [...this.messages]
|
|
334
|
+
forked.onPlanWritten = this.onPlanWritten
|
|
335
|
+
const baseTitle = this.sessionTitle || 'forked session'
|
|
336
|
+
forked.sessionTitle = `${baseTitle} (fork)`
|
|
337
|
+
forked.sessionId = generateId()
|
|
338
|
+
forked.sessionCreatedAt = new Date().toISOString()
|
|
339
|
+
forked.save()
|
|
340
|
+
return forked
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
exportSession(filePath?: string): string {
|
|
344
|
+
const dir = filePath || path.join(getSessionDir(), `export-${this.sessionId}.json`)
|
|
345
|
+
const now = new Date().toISOString()
|
|
346
|
+
let title = this.sessionTitle
|
|
347
|
+
if (!title) {
|
|
348
|
+
const firstUserMsg = this.messages.find(m => m.role === 'user')
|
|
349
|
+
if (firstUserMsg && typeof firstUserMsg.content === 'string') {
|
|
350
|
+
title = firstUserMsg.content.slice(0, 80).replace(/\n/g, ' ')
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const exportData = {
|
|
354
|
+
id: this.sessionId,
|
|
355
|
+
title: title || '(untitled)',
|
|
356
|
+
cwd: path.resolve(this.config.cwd),
|
|
357
|
+
mode: this.config.mode,
|
|
358
|
+
model: this.config.model,
|
|
359
|
+
provider: this.config.provider,
|
|
360
|
+
totalInputTokens: this.totalInputTokens,
|
|
361
|
+
totalOutputTokens: this.totalOutputTokens,
|
|
362
|
+
totalApiCalls: this.totalApiCalls,
|
|
363
|
+
totalCacheHitTokens: this.totalCacheHitTokens,
|
|
364
|
+
totalCacheMissTokens: this.totalCacheMissTokens,
|
|
365
|
+
createdAt: this.sessionCreatedAt,
|
|
366
|
+
exportedAt: now,
|
|
367
|
+
messages: this.messages.map(m => ({
|
|
368
|
+
role: m.role,
|
|
369
|
+
content: typeof m.content === 'string' ? m.content : null,
|
|
370
|
+
tool_calls: m.tool_calls
|
|
371
|
+
? m.tool_calls.map(tc => ({
|
|
372
|
+
id: tc.id,
|
|
373
|
+
name: tc.name,
|
|
374
|
+
input: tc.input,
|
|
375
|
+
}))
|
|
376
|
+
: undefined,
|
|
377
|
+
reasoning_content: m.reasoning_content,
|
|
378
|
+
})),
|
|
371
379
|
}
|
|
380
|
+
const fullPath = path.resolve(dir)
|
|
381
|
+
ensureDir(path.dirname(fullPath))
|
|
382
|
+
fs.writeFileSync(fullPath, JSON.stringify(exportData, null, 2), 'utf-8')
|
|
383
|
+
return fullPath
|
|
372
384
|
}
|
|
373
385
|
|
|
374
|
-
async setMode(mode: 'code' | 'plan' | 'ask'): Promise<void> {
|
|
386
|
+
async setMode(mode: 'code' | 'plan' | 'ask' | 'loop'): Promise<void> {
|
|
375
387
|
this.config.mode = mode
|
|
376
|
-
const prompt = await buildSystemPrompt(this.config)
|
|
377
|
-
this.messages[0] = { role: 'system', content: prompt }
|
|
378
388
|
this.registry.setMode(mode)
|
|
389
|
+
const prompt = await buildSystemPrompt(this.config, this.registry.getDefinitions())
|
|
390
|
+
this.messages[0] = { role: 'system', content: prompt }
|
|
379
391
|
this.save()
|
|
380
392
|
}
|
|
381
393
|
|
|
382
|
-
/** Stop the current conversation gracefully */
|
|
383
394
|
stop(): void {
|
|
384
395
|
this.stopped = true
|
|
385
|
-
// Abort any in-flight LLM stream to stop token consumption immediately
|
|
386
396
|
this.abortController?.abort()
|
|
387
397
|
}
|
|
388
398
|
|
|
389
|
-
/** Check if the session was stopped */
|
|
390
399
|
isStopped(): boolean {
|
|
391
400
|
return this.stopped
|
|
392
401
|
}
|
|
393
402
|
|
|
394
|
-
/** Reset the stopped flag for a new conversation */
|
|
395
403
|
resetStopped(): void {
|
|
396
404
|
this.stopped = false
|
|
397
|
-
// Create new AbortController for next conversation
|
|
398
405
|
this.abortController = new AbortController()
|
|
399
406
|
}
|
|
400
407
|
|
|
401
408
|
async chat(userPrompt: string): Promise<void> {
|
|
402
|
-
|
|
403
|
-
const out = this.output
|
|
404
|
-
printUserMessage(userPrompt, out)
|
|
405
|
-
this.messages.push({ role: 'user', content: userPrompt })
|
|
406
|
-
// Save immediately after user message so page refresh doesn't lose it
|
|
407
|
-
this.save()
|
|
408
|
-
|
|
409
|
-
// Reset per-turn counters
|
|
410
|
-
this.turnInputTokens = 0
|
|
411
|
-
this.turnOutputTokens = 0
|
|
412
|
-
this.turnApiCalls = 0
|
|
413
|
-
|
|
414
|
-
let iterations = 0
|
|
415
|
-
const maxIterations = 30
|
|
416
|
-
|
|
417
|
-
// Reset stopped flag for new conversation
|
|
418
|
-
this.resetStopped()
|
|
419
|
-
// Create a new AbortController for this chat invocation
|
|
420
|
-
// (a new controller is needed each time because abort() is one-shot)
|
|
421
|
-
this.abortController = new AbortController()
|
|
422
|
-
|
|
423
|
-
// Declare toolCalls outside the loop so we can reference it in stop check
|
|
424
|
-
let toolCalls: ToolCall[] = []
|
|
425
|
-
|
|
426
|
-
while (iterations < maxIterations) {
|
|
427
|
-
// Check if stop was requested
|
|
428
|
-
if (this.isStopped()) {
|
|
429
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
|
|
430
|
-
this.save()
|
|
431
|
-
return
|
|
432
|
-
}
|
|
433
|
-
iterations++
|
|
434
|
-
this.turnApiCalls++
|
|
435
|
-
this.totalApiCalls++
|
|
436
|
-
toolCalls = []
|
|
437
|
-
let fullResponse = ''
|
|
438
|
-
let reasoningContent: string | undefined
|
|
439
|
-
let reasoningOutput = false
|
|
440
|
-
let reasoningLineStart = false
|
|
441
|
-
|
|
442
|
-
bus.emit(EventChannels.TURN_START, { prompt: userPrompt, iteration: iterations })
|
|
443
|
-
bus.emit(EventChannels.LLM_STREAM_START, { iteration: iterations })
|
|
444
|
-
|
|
445
|
-
const stream = this.provider.chat(
|
|
446
|
-
this.messages,
|
|
447
|
-
this.registry.getDefinitions(),
|
|
448
|
-
this.abortController.signal,
|
|
449
|
-
)
|
|
450
|
-
|
|
451
|
-
try {
|
|
452
|
-
for await (const chunk of stream) {
|
|
453
|
-
if (chunk.reasoning_content) {
|
|
454
|
-
reasoningContent = chunk.reasoning_content
|
|
455
|
-
// Stream reasoning content in real-time (only when no text in same chunk)
|
|
456
|
-
if (!chunk.text) {
|
|
457
|
-
// Emit thinking via EventBus for Web UI
|
|
458
|
-
bus.emit(EventChannels.THINKING, { text: chunk.reasoning_content })
|
|
459
|
-
if (!reasoningOutput) {
|
|
460
|
-
reasoningOutput = true
|
|
461
|
-
reasoningLineStart = true
|
|
462
|
-
if (!out?.suppressToolOutput) {
|
|
463
|
-
writeOut(thinkTopBorder(), out)
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
// Terminal display with box drawing (skip in Web UI mode, handled by EventBus)
|
|
467
|
-
if (!out?.suppressToolOutput) {
|
|
468
|
-
// Track column position on current line for wrapping
|
|
469
|
-
let thinkCol = 0
|
|
470
|
-
// Handle newlines in streamed content - add left border on each new line
|
|
471
|
-
// Also manually wrap long lines so wrapped lines keep the │ prefix.
|
|
472
|
-
let remaining = chunk.reasoning_content
|
|
473
|
-
const maxContentWidth = termWidth() - THINK_PREFIX_WIDTH
|
|
474
|
-
while (remaining.length > 0) {
|
|
475
|
-
if (reasoningLineStart) {
|
|
476
|
-
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
477
|
-
reasoningLineStart = false
|
|
478
|
-
thinkCol = 0
|
|
479
|
-
}
|
|
480
|
-
const nlIdx = remaining.indexOf('\n')
|
|
481
|
-
if (nlIdx === -1) {
|
|
482
|
-
// No newline — write as much as fits on current line, wrap if needed
|
|
483
|
-
while (remaining.length > 0) {
|
|
484
|
-
const segWidth = visibleWidth(remaining)
|
|
485
|
-
const avail = maxContentWidth - thinkCol
|
|
486
|
-
if (segWidth <= avail) {
|
|
487
|
-
// Fits entirely on current line
|
|
488
|
-
writeOut(remaining, out)
|
|
489
|
-
thinkCol += segWidth
|
|
490
|
-
remaining = ''
|
|
491
|
-
} else if (avail <= 0) {
|
|
492
|
-
// Current line is full, wrap to next
|
|
493
|
-
writeOut(`${RS}\n`, out)
|
|
494
|
-
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
495
|
-
thinkCol = 0
|
|
496
|
-
} else {
|
|
497
|
-
// Write first part that fits, then wrap
|
|
498
|
-
// Find character boundary that fits within avail
|
|
499
|
-
let cut = avail
|
|
500
|
-
while (cut > 0 && visibleWidth(remaining.slice(0, cut)) > avail) cut--
|
|
501
|
-
if (cut <= 0) cut = 1
|
|
502
|
-
writeOut(remaining.slice(0, cut), out)
|
|
503
|
-
writeOut(`${RS}\n`, out)
|
|
504
|
-
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
505
|
-
thinkCol = 0
|
|
506
|
-
remaining = remaining.slice(cut)
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
} else {
|
|
510
|
-
// Has newline — process the segment up to newline
|
|
511
|
-
const segment = remaining.slice(0, nlIdx)
|
|
512
|
-
const segWidth = visibleWidth(segment)
|
|
513
|
-
const avail = maxContentWidth - thinkCol
|
|
514
|
-
if (segWidth <= avail) {
|
|
515
|
-
// Segment fits on current line
|
|
516
|
-
writeOut(segment, out)
|
|
517
|
-
writeOut(`${RS}\n`, out)
|
|
518
|
-
reasoningLineStart = true
|
|
519
|
-
thinkCol = 0
|
|
520
|
-
} else {
|
|
521
|
-
// Segment too long — write what fits, wrap, then rest
|
|
522
|
-
let rest = segment
|
|
523
|
-
// Write remainder of current line
|
|
524
|
-
if (avail > 0) {
|
|
525
|
-
let cut = avail
|
|
526
|
-
while (cut > 0 && visibleWidth(rest.slice(0, cut)) > avail) cut--
|
|
527
|
-
if (cut <= 0) cut = 1
|
|
528
|
-
writeOut(rest.slice(0, cut), out)
|
|
529
|
-
rest = rest.slice(cut)
|
|
530
|
-
}
|
|
531
|
-
writeOut(`${RS}\n`, out)
|
|
532
|
-
reasoningLineStart = true
|
|
533
|
-
thinkCol = 0
|
|
534
|
-
// Write rest of segment on continuation line(s)
|
|
535
|
-
if (rest.length > 0) {
|
|
536
|
-
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
537
|
-
reasoningLineStart = false
|
|
538
|
-
while (rest.length > 0) {
|
|
539
|
-
const rw = visibleWidth(rest)
|
|
540
|
-
if (rw <= maxContentWidth) {
|
|
541
|
-
writeOut(rest, out)
|
|
542
|
-
thinkCol = rw
|
|
543
|
-
rest = ''
|
|
544
|
-
} else {
|
|
545
|
-
let cut = maxContentWidth
|
|
546
|
-
while (cut > 0 && visibleWidth(rest.slice(0, cut)) > maxContentWidth)
|
|
547
|
-
cut--
|
|
548
|
-
if (cut <= 0) cut = 1
|
|
549
|
-
writeOut(rest.slice(0, cut), out)
|
|
550
|
-
writeOut(`${RS}\n`, out)
|
|
551
|
-
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
552
|
-
rest = rest.slice(cut)
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
writeOut(`${RS}\n`, out)
|
|
557
|
-
reasoningLineStart = true
|
|
558
|
-
thinkCol = 0
|
|
559
|
-
}
|
|
560
|
-
remaining = remaining.slice(nlIdx + 1)
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
if (chunk.type === 'text' && chunk.text) {
|
|
567
|
-
if (reasoningOutput) {
|
|
568
|
-
bus.emit(EventChannels.THINKING_END, {})
|
|
569
|
-
if (!out?.suppressToolOutput) {
|
|
570
|
-
writeOut(`${RS}\n`, out)
|
|
571
|
-
writeOut(thinkBottomBorder(), out)
|
|
572
|
-
}
|
|
573
|
-
reasoningOutput = false
|
|
574
|
-
reasoningLineStart = false
|
|
575
|
-
}
|
|
576
|
-
fullResponse += chunk.text
|
|
577
|
-
writeOut(chunk.text, out)
|
|
578
|
-
} else if (chunk.type === 'tool_use' && chunk.tool_call) {
|
|
579
|
-
toolCalls.push(chunk.tool_call)
|
|
580
|
-
} else if (chunk.type === 'complete') {
|
|
581
|
-
if (chunk.usage) {
|
|
582
|
-
this.turnInputTokens += chunk.usage.input_tokens
|
|
583
|
-
this.turnOutputTokens += chunk.usage.output_tokens
|
|
584
|
-
this.totalInputTokens += chunk.usage.input_tokens
|
|
585
|
-
this.totalOutputTokens += chunk.usage.output_tokens
|
|
586
|
-
}
|
|
587
|
-
if (chunk.finish_reason === 'stop' || chunk.finish_reason === 'end_turn') {
|
|
588
|
-
if (toolCalls.length === 0) {
|
|
589
|
-
const finalAssistantMsg: LLMMessage = {
|
|
590
|
-
role: 'assistant',
|
|
591
|
-
content: fullResponse || null,
|
|
592
|
-
reasoning_content: reasoningContent,
|
|
593
|
-
}
|
|
594
|
-
this.messages.push(finalAssistantMsg)
|
|
595
|
-
printTokenStats(
|
|
596
|
-
this.turnInputTokens,
|
|
597
|
-
this.turnOutputTokens,
|
|
598
|
-
this.totalInputTokens,
|
|
599
|
-
this.totalOutputTokens,
|
|
600
|
-
this.turnApiCalls,
|
|
601
|
-
this.totalApiCalls,
|
|
602
|
-
out,
|
|
603
|
-
)
|
|
604
|
-
writeOut('\n\n', out)
|
|
605
|
-
saveTokenUsage(
|
|
606
|
-
this.config.cwd,
|
|
607
|
-
this.turnInputTokens,
|
|
608
|
-
this.turnOutputTokens,
|
|
609
|
-
this.turnApiCalls,
|
|
610
|
-
)
|
|
611
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
|
|
612
|
-
this.save()
|
|
613
|
-
return
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
} catch (e) {
|
|
619
|
-
const errMsg = fmtErr(e)
|
|
620
|
-
const partialContent = fullResponse ? fullResponse.slice(0, 500) : '(empty)'
|
|
621
|
-
if (!out?.suppressToolOutput) {
|
|
622
|
-
writeOut(`\n${RE}Stream error:${RS} ${errMsg}`, out)
|
|
623
|
-
writeOut(`\n ${GY}┃${RS} Partial response: ${partialContent}\n`, out)
|
|
624
|
-
}
|
|
625
|
-
console.error('[session] Stream error:', errMsg, '| Partial response:', partialContent)
|
|
626
|
-
if (reasoningOutput) {
|
|
627
|
-
bus.emit(EventChannels.THINKING_END, {})
|
|
628
|
-
if (!out?.suppressToolOutput) {
|
|
629
|
-
writeOut(`${RS}\n`, out)
|
|
630
|
-
writeOut(thinkBottomBorder(), out)
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
bus.emit(EventChannels.LLM_STREAM_END, { iteration: iterations, toolCallCount: 0 })
|
|
634
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
|
|
635
|
-
|
|
636
|
-
// If there are pending toolCalls that weren't executed due to abort,
|
|
637
|
-
// add an assistant message so the model knows they weren't executed
|
|
638
|
-
if (toolCalls.length > 0) {
|
|
639
|
-
const interruptedMsg: LLMMessage = {
|
|
640
|
-
role: 'assistant',
|
|
641
|
-
content: null,
|
|
642
|
-
tool_calls: toolCalls,
|
|
643
|
-
reasoning_content: reasoningContent,
|
|
644
|
-
}
|
|
645
|
-
this.messages.push(interruptedMsg)
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
saveTokenUsage(
|
|
649
|
-
this.config.cwd,
|
|
650
|
-
this.turnInputTokens,
|
|
651
|
-
this.turnOutputTokens,
|
|
652
|
-
this.turnApiCalls,
|
|
653
|
-
)
|
|
654
|
-
this.save()
|
|
655
|
-
return
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
bus.emit(EventChannels.LLM_STREAM_END, {
|
|
659
|
-
iteration: iterations,
|
|
660
|
-
toolCallCount: toolCalls.length,
|
|
661
|
-
})
|
|
662
|
-
|
|
663
|
-
// Close reasoning display if still open (model ended with tool calls, no text)
|
|
664
|
-
if (reasoningOutput) {
|
|
665
|
-
bus.emit(EventChannels.THINKING_END, {})
|
|
666
|
-
if (!out?.suppressToolOutput) {
|
|
667
|
-
writeOut(`${RS}\n`, out)
|
|
668
|
-
writeOut(thinkBottomBorder(), out)
|
|
669
|
-
}
|
|
670
|
-
reasoningOutput = false
|
|
671
|
-
reasoningLineStart = false
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
if (toolCalls.length === 0) {
|
|
675
|
-
if (fullResponse) {
|
|
676
|
-
const finalAssistantMsg: LLMMessage = {
|
|
677
|
-
role: 'assistant',
|
|
678
|
-
content: fullResponse,
|
|
679
|
-
reasoning_content: reasoningContent,
|
|
680
|
-
}
|
|
681
|
-
this.messages.push(finalAssistantMsg)
|
|
682
|
-
printTokenStats(
|
|
683
|
-
this.turnInputTokens,
|
|
684
|
-
this.turnOutputTokens,
|
|
685
|
-
this.totalInputTokens,
|
|
686
|
-
this.totalOutputTokens,
|
|
687
|
-
this.turnApiCalls,
|
|
688
|
-
this.totalApiCalls,
|
|
689
|
-
out,
|
|
690
|
-
)
|
|
691
|
-
writeOut('\n\n', out)
|
|
692
|
-
}
|
|
693
|
-
saveTokenUsage(
|
|
694
|
-
this.config.cwd,
|
|
695
|
-
this.turnInputTokens,
|
|
696
|
-
this.turnOutputTokens,
|
|
697
|
-
this.turnApiCalls,
|
|
698
|
-
)
|
|
699
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
|
|
700
|
-
this.save()
|
|
701
|
-
return
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
const assistantMsg: LLMMessage = {
|
|
705
|
-
role: 'assistant',
|
|
706
|
-
content: fullResponse || null,
|
|
707
|
-
tool_calls: toolCalls,
|
|
708
|
-
reasoning_content: reasoningContent,
|
|
709
|
-
}
|
|
710
|
-
this.messages.push(assistantMsg)
|
|
711
|
-
this.save()
|
|
712
|
-
|
|
713
|
-
// ── User confirmation for write-type tool calls ──
|
|
714
|
-
if (!this.config.autoApprove && this.output?.confirmTool && toolCalls.length > 0) {
|
|
715
|
-
const writeTools = ['edit', 'bash', 'write_plan', 'exec', 'install_skill']
|
|
716
|
-
const needsConfirm = toolCalls.filter(tc => writeTools.includes(tc.name))
|
|
717
|
-
if (needsConfirm.length > 0) {
|
|
718
|
-
const approved = await this.output.confirmTool(needsConfirm)
|
|
719
|
-
if (!approved) {
|
|
720
|
-
const rejectMsg: LLMMessage = {
|
|
721
|
-
role: 'tool',
|
|
722
|
-
content:
|
|
723
|
-
'USER_REJECTED: The user declined to execute the requested tool calls. Try a different approach.',
|
|
724
|
-
tool_call_id: needsConfirm[0].id,
|
|
725
|
-
name: 'user_feedback',
|
|
726
|
-
}
|
|
727
|
-
this.messages.push(rejectMsg)
|
|
728
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
|
|
729
|
-
continue
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
for (let i = 0; i < toolCalls.length; i++) {
|
|
735
|
-
const tc = toolCalls[i]
|
|
736
|
-
// Check if stop was requested after each tool call
|
|
737
|
-
if (this.isStopped()) {
|
|
738
|
-
// Add assistant message with remaining tool_calls so model knows they weren't executed
|
|
739
|
-
const remainingToolCalls = toolCalls.slice(i)
|
|
740
|
-
if (remainingToolCalls.length > 0) {
|
|
741
|
-
const interruptedMsg: LLMMessage = {
|
|
742
|
-
role: 'assistant',
|
|
743
|
-
content: null,
|
|
744
|
-
tool_calls: remainingToolCalls,
|
|
745
|
-
}
|
|
746
|
-
this.messages.push(interruptedMsg)
|
|
747
|
-
}
|
|
748
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
|
|
749
|
-
this.save()
|
|
750
|
-
return
|
|
751
|
-
}
|
|
752
|
-
bus.emit(EventChannels.TOOL_CALL, { name: tc.name, input: tc.input, id: tc.id })
|
|
753
|
-
if (!out?.suppressToolOutput) {
|
|
754
|
-
printToolInvocation(tc, out)
|
|
755
|
-
}
|
|
756
|
-
const result: ToolResult = await this.registry.dispatch(tc)
|
|
757
|
-
if (result.success) {
|
|
758
|
-
bus.emit(EventChannels.TOOL_RESULT, { name: tc.name, id: tc.id, output: result.output })
|
|
759
|
-
} else {
|
|
760
|
-
bus.emit(EventChannels.TOOL_ERROR, { name: tc.name, id: tc.id, error: result.error })
|
|
761
|
-
}
|
|
762
|
-
if (!out?.suppressToolOutput) {
|
|
763
|
-
printToolResult(tc, result, out)
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
const resultMsg: LLMMessage = {
|
|
767
|
-
role: 'tool',
|
|
768
|
-
content: result.success ? result.output : `ERROR: ${result.error}`,
|
|
769
|
-
tool_call_id: tc.id,
|
|
770
|
-
name: tc.name,
|
|
771
|
-
}
|
|
772
|
-
this.messages.push(resultMsg)
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
this.save()
|
|
776
|
-
|
|
777
|
-
// End the current turn before next iteration — frontend creates a new message
|
|
778
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
|
|
779
|
-
|
|
780
|
-
// Check if compaction is needed
|
|
781
|
-
if (shouldCompact(this.messages, this.config.contextWindow)) {
|
|
782
|
-
const before = this.messages.length
|
|
783
|
-
const result = compact(this.messages, this.config.contextWindow)
|
|
784
|
-
if (result.compressed) {
|
|
785
|
-
this.messages = result.messages
|
|
786
|
-
bus.emit(EventChannels.COMPACTION_TRIGGERED, { before, after: result.newCount })
|
|
787
|
-
if (out && !out.suppressToolOutput) {
|
|
788
|
-
out.write(
|
|
789
|
-
`\n ${GY}┃${RS} ${GY}📦 Compressed context: ${before} → ${result.newCount} messages${RS}\n`,
|
|
790
|
-
)
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
if (iterations >= maxIterations) {
|
|
797
|
-
printTokenStats(
|
|
798
|
-
this.turnInputTokens,
|
|
799
|
-
this.turnOutputTokens,
|
|
800
|
-
this.totalInputTokens,
|
|
801
|
-
this.totalOutputTokens,
|
|
802
|
-
this.turnApiCalls,
|
|
803
|
-
this.totalApiCalls,
|
|
804
|
-
out,
|
|
805
|
-
)
|
|
806
|
-
writeOut('\nAgent reached maximum iterations. Stopping.\n', out)
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
saveTokenUsage(this.config.cwd, this.turnInputTokens, this.turnOutputTokens, this.turnApiCalls)
|
|
810
|
-
this.save()
|
|
409
|
+
return runChat(this, userPrompt)
|
|
811
410
|
}
|
|
812
411
|
}
|