@compilr-dev/cli 0.5.0 → 0.5.2
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/LICENSE +108 -0
- package/README.md +237 -69
- package/dist/.tsbuildinfo.app +1 -0
- package/dist/.tsbuildinfo.data +1 -0
- package/dist/.tsbuildinfo.domain +1 -0
- package/dist/.tsbuildinfo.foundation +1 -0
- package/dist/agent.d.ts +61 -4
- package/dist/agent.js +241 -245
- package/dist/anchors/index.d.ts +1 -1
- package/dist/anchors/index.js +1 -1
- package/dist/anchors/project-anchors.d.ts +2 -2
- package/dist/anchors/project-anchors.js +1 -1
- package/dist/auth/api-client.d.ts +124 -0
- package/dist/auth/api-client.js +261 -0
- package/dist/auth/index.d.ts +172 -0
- package/dist/auth/index.js +545 -0
- package/dist/auth/storage.d.ts +52 -0
- package/dist/auth/storage.js +118 -0
- package/dist/changelog/index.d.ts +16 -0
- package/dist/changelog/index.js +24 -0
- package/dist/changelog/releases.d.ts +17 -0
- package/dist/changelog/releases.js +63 -0
- package/dist/commands-v2/handlers/auth.d.ts +10 -0
- package/dist/commands-v2/handlers/auth.js +118 -0
- package/dist/commands-v2/handlers/background.d.ts +14 -0
- package/dist/commands-v2/handlers/background.js +276 -0
- package/dist/commands-v2/handlers/context.js +286 -81
- package/dist/commands-v2/handlers/core.d.ts +1 -0
- package/dist/commands-v2/handlers/core.js +133 -8
- package/dist/commands-v2/handlers/debug.js +18 -0
- package/dist/commands-v2/handlers/delegations.d.ts +8 -0
- package/dist/commands-v2/handlers/delegations.js +29 -0
- package/dist/commands-v2/handlers/files.d.ts +8 -0
- package/dist/commands-v2/handlers/files.js +162 -0
- package/dist/commands-v2/handlers/filter.d.ts +9 -0
- package/dist/commands-v2/handlers/filter.js +130 -0
- package/dist/commands-v2/handlers/games.d.ts +7 -0
- package/dist/commands-v2/handlers/games.js +57 -0
- package/dist/commands-v2/handlers/index.d.ts +13 -0
- package/dist/commands-v2/handlers/index.js +39 -0
- package/dist/commands-v2/handlers/mcp.d.ts +8 -0
- package/dist/commands-v2/handlers/mcp.js +39 -0
- package/dist/commands-v2/handlers/notifications.d.ts +9 -0
- package/dist/commands-v2/handlers/notifications.js +34 -0
- package/dist/commands-v2/handlers/project.js +295 -31
- package/dist/commands-v2/handlers/reset.d.ts +11 -0
- package/dist/commands-v2/handlers/reset.js +118 -0
- package/dist/commands-v2/handlers/session.d.ts +161 -0
- package/dist/commands-v2/handlers/session.js +805 -0
- package/dist/commands-v2/handlers/settings.d.ts +2 -0
- package/dist/commands-v2/handlers/settings.js +217 -35
- package/dist/commands-v2/handlers/tasks.d.ts +5 -0
- package/dist/commands-v2/handlers/tasks.js +36 -0
- package/dist/commands-v2/handlers/team.d.ts +9 -0
- package/dist/commands-v2/handlers/team.js +549 -0
- package/dist/commands-v2/handlers/terminals.d.ts +9 -0
- package/dist/commands-v2/handlers/terminals.js +34 -0
- package/dist/commands-v2/index.d.ts +3 -2
- package/dist/commands-v2/index.js +4 -1
- package/dist/commands-v2/registry.d.ts +15 -0
- package/dist/commands-v2/registry.js +34 -0
- package/dist/commands-v2/types.d.ts +81 -3
- package/dist/commands.js +13 -0
- package/dist/compilr-diff-companion.vsix +0 -0
- package/dist/db/index.js +98 -4
- package/dist/db/repositories/document-repository.d.ts +2 -0
- package/dist/db/repositories/document-repository.js +6 -1
- package/dist/db/repositories/index.d.ts +2 -0
- package/dist/db/repositories/index.js +1 -0
- package/dist/db/repositories/plan-repository.d.ts +101 -0
- package/dist/db/repositories/plan-repository.js +275 -0
- package/dist/db/repositories/project-repository.d.ts +6 -0
- package/dist/db/repositories/project-repository.js +41 -0
- package/dist/db/repositories/work-item-repository.d.ts +15 -0
- package/dist/db/repositories/work-item-repository.js +69 -4
- package/dist/db/schema.d.ts +40 -3
- package/dist/db/schema.js +66 -3
- package/dist/episodes/index.d.ts +20 -0
- package/dist/episodes/index.js +27 -0
- package/dist/episodes/recorder.d.ts +51 -0
- package/dist/episodes/recorder.js +195 -0
- package/dist/episodes/significant-work.d.ts +21 -0
- package/dist/episodes/significant-work.js +56 -0
- package/dist/episodes/store.d.ts +38 -0
- package/dist/episodes/store.js +199 -0
- package/dist/episodes/types.d.ts +35 -0
- package/dist/episodes/types.js +6 -0
- package/dist/episodes/work-at-risk.d.ts +12 -0
- package/dist/episodes/work-at-risk.js +38 -0
- package/dist/episodes/work-summary-anchor.d.ts +23 -0
- package/dist/episodes/work-summary-anchor.js +73 -0
- package/dist/games/coins.d.ts +66 -0
- package/dist/games/coins.js +165 -0
- package/dist/games/game-base.d.ts +84 -0
- package/dist/games/game-base.js +204 -0
- package/dist/games/index.d.ts +16 -0
- package/dist/games/index.js +49 -0
- package/dist/games/scores.d.ts +69 -0
- package/dist/games/scores.js +191 -0
- package/dist/games/tetris/board.d.ts +59 -0
- package/dist/games/tetris/board.js +170 -0
- package/dist/games/tetris/index.d.ts +109 -0
- package/dist/games/tetris/index.js +610 -0
- package/dist/games/tetris/pieces.d.ts +44 -0
- package/dist/games/tetris/pieces.js +271 -0
- package/dist/games/tetris/renderer.d.ts +26 -0
- package/dist/games/tetris/renderer.js +77 -0
- package/dist/guide/guide-content.d.ts +23 -0
- package/dist/guide/guide-content.js +196 -0
- package/dist/guide/index.d.ts +8 -0
- package/dist/guide/index.js +7 -0
- package/dist/guide/shared-content.d.ts +37 -0
- package/dist/guide/shared-content.js +1272 -0
- package/dist/guide/tutorial-helpers.d.ts +57 -0
- package/dist/guide/tutorial-helpers.js +147 -0
- package/dist/handlers/ask-user-handlers.d.ts +32 -0
- package/dist/handlers/ask-user-handlers.js +104 -0
- package/dist/handlers/delegation-handlers.d.ts +34 -0
- package/dist/handlers/delegation-handlers.js +291 -0
- package/dist/handlers/permission-handler.d.ts +30 -0
- package/dist/handlers/permission-handler.js +205 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +448 -271
- package/dist/input-handlers/memory-handler.d.ts +1 -1
- package/dist/input-handlers/memory-handler.js +2 -1
- package/dist/models/index.d.ts +10 -0
- package/dist/models/index.js +12 -0
- package/dist/models/model-registry.d.ts +38 -0
- package/dist/models/model-registry.js +69 -0
- package/dist/models/model-tiers.d.ts +28 -0
- package/dist/models/model-tiers.js +71 -0
- package/dist/models/model-validation.d.ts +25 -0
- package/dist/models/model-validation.js +291 -0
- package/dist/models/ollama-models.d.ts +73 -0
- package/dist/models/ollama-models.js +178 -0
- package/dist/models/provider-types.d.ts +6 -0
- package/dist/models/provider-types.js +1 -0
- package/dist/models/providers.d.ts +35 -0
- package/dist/models/providers.js +58 -0
- package/dist/models/types.d.ts +4 -0
- package/dist/models/types.js +4 -0
- package/dist/multi-agent/activity.d.ts +21 -0
- package/dist/multi-agent/activity.js +34 -0
- package/dist/multi-agent/agent-selection.d.ts +55 -0
- package/dist/multi-agent/agent-selection.js +90 -0
- package/dist/multi-agent/artifacts.d.ts +197 -0
- package/dist/multi-agent/artifacts.js +379 -0
- package/dist/multi-agent/checkpointer.d.ts +138 -0
- package/dist/multi-agent/checkpointer.js +471 -0
- package/dist/multi-agent/collision-utils.d.ts +16 -0
- package/dist/multi-agent/collision-utils.js +28 -0
- package/dist/multi-agent/context-resolver.d.ts +97 -0
- package/dist/multi-agent/context-resolver.js +316 -0
- package/dist/multi-agent/custom-agents.d.ts +83 -0
- package/dist/multi-agent/custom-agents.js +227 -0
- package/dist/multi-agent/delegation-tracker.d.ts +157 -0
- package/dist/multi-agent/delegation-tracker.js +243 -0
- package/dist/multi-agent/file-lock-hook.d.ts +29 -0
- package/dist/multi-agent/file-lock-hook.js +97 -0
- package/dist/multi-agent/file-locks.d.ts +58 -0
- package/dist/multi-agent/file-locks.js +194 -0
- package/dist/multi-agent/index.d.ts +24 -0
- package/dist/multi-agent/index.js +30 -0
- package/dist/multi-agent/mention-parser.d.ts +64 -0
- package/dist/multi-agent/mention-parser.js +146 -0
- package/dist/multi-agent/notification-manager.d.ts +84 -0
- package/dist/multi-agent/notification-manager.js +224 -0
- package/dist/multi-agent/pending-requests.d.ts +122 -0
- package/dist/multi-agent/pending-requests.js +155 -0
- package/dist/multi-agent/session-registry.d.ts +139 -0
- package/dist/multi-agent/session-registry.js +514 -0
- package/dist/multi-agent/shared-context.d.ts +293 -0
- package/dist/multi-agent/shared-context.js +671 -0
- package/dist/multi-agent/skill-requirements.d.ts +66 -0
- package/dist/multi-agent/skill-requirements.js +178 -0
- package/dist/multi-agent/task-assignment.d.ts +69 -0
- package/dist/multi-agent/task-assignment.js +123 -0
- package/dist/multi-agent/task-suggestion.d.ts +31 -0
- package/dist/multi-agent/task-suggestion.js +72 -0
- package/dist/multi-agent/team-agent.d.ts +201 -0
- package/dist/multi-agent/team-agent.js +488 -0
- package/dist/multi-agent/team.d.ts +286 -0
- package/dist/multi-agent/team.js +610 -0
- package/dist/multi-agent/tool-config.d.ts +110 -0
- package/dist/multi-agent/tool-config.js +661 -0
- package/dist/multi-agent/types.d.ts +211 -0
- package/dist/multi-agent/types.js +617 -0
- package/dist/prompts/plan-mode-prompt.d.ts +11 -0
- package/dist/prompts/plan-mode-prompt.js +95 -0
- package/dist/repl-helpers.js +5 -2
- package/dist/repl-v2.d.ts +401 -2
- package/dist/repl-v2.js +2588 -65
- package/dist/session/index.d.ts +6 -0
- package/dist/session/index.js +6 -0
- package/dist/session/project-session-manager.d.ts +158 -0
- package/dist/session/project-session-manager.js +650 -0
- package/dist/settings/index.d.ts +133 -13
- package/dist/settings/index.js +329 -24
- package/dist/settings/mcp-config.d.ts +76 -0
- package/dist/settings/mcp-config.js +143 -0
- package/dist/settings/paths.d.ts +4 -0
- package/dist/settings/paths.js +6 -0
- package/dist/shared-handlers.d.ts +62 -0
- package/dist/shared-handlers.js +48 -0
- package/dist/system-prompt/builder.d.ts +5 -0
- package/dist/system-prompt/builder.js +4 -0
- package/dist/system-prompt/index.d.ts +18 -0
- package/dist/system-prompt/index.js +18 -0
- package/dist/system-prompt/modules.d.ts +5 -0
- package/dist/system-prompt/modules.js +4 -0
- package/dist/tabbed-menu.js +2 -1
- package/dist/templates/compilr-md-import.d.ts +16 -0
- package/dist/templates/compilr-md-import.js +241 -0
- package/dist/templates/compilr-md.js +10 -61
- package/dist/templates/config-json.d.ts +1 -25
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.js +34 -73
- package/dist/tool-names.d.ts +113 -0
- package/dist/tool-names.js +239 -0
- package/dist/tools/ask-user-simple.d.ts +1 -1
- package/dist/tools/ask-user-simple.js +2 -1
- package/dist/tools/ask-user.d.ts +1 -1
- package/dist/tools/ask-user.js +2 -1
- package/dist/tools/backlog.d.ts +2 -2
- package/dist/tools/backlog.js +1 -1
- package/dist/tools/db-tools.d.ts +13 -61
- package/dist/tools/db-tools.js +12 -13
- package/dist/tools/delegate-background.d.ts +27 -0
- package/dist/tools/delegate-background.js +115 -0
- package/dist/tools/delegate.d.ts +22 -0
- package/dist/tools/delegate.js +97 -0
- package/dist/tools/delegation-status.d.ts +16 -0
- package/dist/tools/delegation-status.js +128 -0
- package/dist/tools/guide-tool.d.ts +12 -0
- package/dist/tools/guide-tool.js +59 -0
- package/dist/tools/handoff.d.ts +25 -0
- package/dist/tools/handoff.js +99 -0
- package/dist/tools/meta-tools.d.ts +26 -0
- package/dist/tools/meta-tools.js +47 -0
- package/dist/tools/platform-adapter.d.ts +35 -0
- package/dist/tools/platform-adapter.js +404 -0
- package/dist/tools/project-db.d.ts +5 -73
- package/dist/tools/project-db.js +5 -336
- package/dist/tools.d.ts +67 -2
- package/dist/tools.js +240 -48
- package/dist/ui/autocomplete-controller.d.ts +42 -0
- package/dist/ui/autocomplete-controller.js +384 -0
- package/dist/ui/base/index.d.ts +1 -1
- package/dist/ui/base/index.js +1 -1
- package/dist/ui/base/overlay-base-v2.d.ts +10 -0
- package/dist/ui/base/overlay-base-v2.js +14 -0
- package/dist/ui/base/render-utils.d.ts +19 -0
- package/dist/ui/base/render-utils.js +25 -0
- package/dist/ui/base/tabbed-list-overlay-v2.d.ts +16 -1
- package/dist/ui/base/tabbed-list-overlay-v2.js +19 -1
- package/dist/ui/constants/labels.d.ts +14 -0
- package/dist/ui/constants/labels.js +52 -0
- package/dist/ui/conversation-store.d.ts +55 -0
- package/dist/ui/conversation-store.js +107 -0
- package/dist/ui/conversation.js +11 -13
- package/dist/ui/diff.d.ts +7 -1
- package/dist/ui/diff.js +85 -48
- package/dist/ui/ephemeral.js +3 -9
- package/dist/ui/file-autocomplete.d.ts +24 -0
- package/dist/ui/file-autocomplete.js +56 -0
- package/dist/ui/footer-renderer.d.ts +69 -0
- package/dist/ui/footer-renderer.js +431 -0
- package/dist/ui/footer.d.ts +74 -7
- package/dist/ui/footer.js +173 -16
- package/dist/ui/input-controller.d.ts +51 -0
- package/dist/ui/input-controller.js +176 -0
- package/dist/ui/input-prompt.d.ts +19 -0
- package/dist/ui/input-prompt.js +206 -14
- package/dist/ui/keyboard-handler.d.ts +57 -0
- package/dist/ui/keyboard-handler.js +557 -0
- package/dist/ui/live-region-facade.d.ts +42 -0
- package/dist/ui/live-region-facade.js +205 -0
- package/dist/ui/live-region.d.ts +0 -4
- package/dist/ui/live-region.js +6 -14
- package/dist/ui/mascot/renderer.d.ts +1 -1
- package/dist/ui/mascot/renderer.js +37 -2
- package/dist/ui/overlay/data/tutorial-content.d.ts +9 -0
- package/dist/ui/overlay/data/tutorial-content.js +9 -0
- package/dist/ui/overlay/data/tutorial-registry.d.ts +12 -0
- package/dist/ui/overlay/data/tutorial-registry.js +116 -0
- package/dist/ui/overlay/data/tutorial-types.d.ts +35 -0
- package/dist/ui/overlay/data/tutorial-types.js +6 -0
- package/dist/ui/overlay/data/tutorials/basics/first-conversation.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/basics/first-conversation.js +220 -0
- package/dist/ui/overlay/data/tutorials/basics/first-project.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/basics/first-project.js +284 -0
- package/dist/ui/overlay/data/tutorials/basics/navigation.d.ts +8 -0
- package/dist/ui/overlay/data/tutorials/basics/navigation.js +22 -0
- package/dist/ui/overlay/data/tutorials/basics/welcome.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/basics/welcome.js +174 -0
- package/dist/ui/overlay/data/tutorials/config/context-management.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/config/context-management.js +158 -0
- package/dist/ui/overlay/data/tutorials/config/mcp-servers.d.ts +8 -0
- package/dist/ui/overlay/data/tutorials/config/mcp-servers.js +155 -0
- package/dist/ui/overlay/data/tutorials/config/model-selection.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/config/model-selection.js +162 -0
- package/dist/ui/overlay/data/tutorials/config/permissions-safety.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/config/permissions-safety.js +163 -0
- package/dist/ui/overlay/data/tutorials/config/settings-config.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/config/settings-config.js +166 -0
- package/dist/ui/overlay/data/tutorials/planning/arch.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/arch.js +168 -0
- package/dist/ui/overlay/data/tutorials/planning/backlog.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/backlog.js +103 -0
- package/dist/ui/overlay/data/tutorials/planning/build.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/build.js +173 -0
- package/dist/ui/overlay/data/tutorials/planning/design.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/design.js +205 -0
- package/dist/ui/overlay/data/tutorials/planning/docs.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/docs.js +143 -0
- package/dist/ui/overlay/data/tutorials/planning/prd.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/prd.js +173 -0
- package/dist/ui/overlay/data/tutorials/planning/scaffold.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/scaffold.js +164 -0
- package/dist/ui/overlay/data/tutorials/planning/sketch.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/sketch.js +58 -0
- package/dist/ui/overlay/data/tutorials/projects/anchors.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/projects/anchors.js +248 -0
- package/dist/ui/overlay/data/tutorials/projects/import-project.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/projects/import-project.js +172 -0
- package/dist/ui/overlay/data/tutorials/projects/managing-projects.d.ts +8 -0
- package/dist/ui/overlay/data/tutorials/projects/managing-projects.js +212 -0
- package/dist/ui/overlay/data/tutorials/projects/new-project.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/projects/new-project.js +251 -0
- package/dist/ui/overlay/data/tutorials/projects/session-management.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/projects/session-management.js +169 -0
- package/dist/ui/overlay/data/tutorials/teams/background-execution.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/teams/background-execution.js +171 -0
- package/dist/ui/overlay/data/tutorials/teams/multi-terminal.d.ts +8 -0
- package/dist/ui/overlay/data/tutorials/teams/multi-terminal.js +147 -0
- package/dist/ui/overlay/data/tutorials/teams/task-assignment.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/teams/task-assignment.js +204 -0
- package/dist/ui/overlay/data/tutorials/teams/team-overview.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/teams/team-overview.js +165 -0
- package/dist/ui/overlay/data/tutorials/teams/working-with-agents.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/teams/working-with-agents.js +172 -0
- package/dist/ui/overlay/impl/agents-overlay-v2.js +6 -17
- package/dist/ui/overlay/impl/anchors-overlay-v2.js +30 -64
- package/dist/ui/overlay/impl/artifact-detail-overlay-v2.d.ts +43 -0
- package/dist/ui/overlay/impl/artifact-detail-overlay-v2.js +232 -0
- package/dist/ui/overlay/impl/artifact-overlay-v2.d.ts +40 -0
- package/dist/ui/overlay/impl/artifact-overlay-v2.js +115 -0
- package/dist/ui/overlay/impl/ask-user-overlay-v2.js +2 -5
- package/dist/ui/overlay/impl/background-overlay-v2.d.ts +40 -0
- package/dist/ui/overlay/impl/background-overlay-v2.js +147 -0
- package/dist/ui/overlay/impl/backlog-overlay-v2.d.ts +4 -1
- package/dist/ui/overlay/impl/backlog-overlay-v2.js +55 -16
- package/dist/ui/overlay/impl/changelog-overlay-v2.d.ts +44 -0
- package/dist/ui/overlay/impl/changelog-overlay-v2.js +165 -0
- package/dist/ui/overlay/impl/commands-overlay-v2.js +4 -6
- package/dist/ui/overlay/impl/config-overlay-v2.d.ts +12 -1
- package/dist/ui/overlay/impl/config-overlay-v2.js +164 -100
- package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.d.ts +83 -0
- package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.js +711 -0
- package/dist/ui/overlay/impl/dashboard-overlay-v2.d.ts +2 -0
- package/dist/ui/overlay/impl/dashboard-overlay-v2.js +26 -3
- package/dist/ui/overlay/impl/delegations-overlay-v2.d.ts +28 -0
- package/dist/ui/overlay/impl/delegations-overlay-v2.js +279 -0
- package/dist/ui/overlay/impl/docs-overlay-v2.js +12 -9
- package/dist/ui/overlay/impl/document-detail-overlay-v2.d.ts +7 -0
- package/dist/ui/overlay/impl/document-detail-overlay-v2.js +119 -78
- package/dist/ui/overlay/impl/filter-overlay-v2.d.ts +41 -0
- package/dist/ui/overlay/impl/filter-overlay-v2.js +110 -0
- package/dist/ui/overlay/impl/games-overlay-v2.d.ts +31 -0
- package/dist/ui/overlay/impl/games-overlay-v2.js +135 -0
- package/dist/ui/overlay/impl/help-overlay-v2.d.ts +26 -3
- package/dist/ui/overlay/impl/help-overlay-v2.js +20 -42
- package/dist/ui/overlay/impl/login-overlay-v2.d.ts +49 -0
- package/dist/ui/overlay/impl/login-overlay-v2.js +277 -0
- package/dist/ui/overlay/impl/mcp-overlay-v2.d.ts +63 -0
- package/dist/ui/overlay/impl/mcp-overlay-v2.js +907 -0
- package/dist/ui/overlay/impl/model-overlay-v2.d.ts +57 -13
- package/dist/ui/overlay/impl/model-overlay-v2.js +1086 -61
- package/dist/ui/overlay/impl/new-overlay-v2.d.ts +37 -6
- package/dist/ui/overlay/impl/new-overlay-v2.js +715 -65
- package/dist/ui/overlay/impl/notifications-overlay-v2.d.ts +20 -0
- package/dist/ui/overlay/impl/notifications-overlay-v2.js +116 -0
- package/dist/ui/overlay/impl/onboarding-wizard-overlay-v2.d.ts +76 -0
- package/dist/ui/overlay/impl/onboarding-wizard-overlay-v2.js +728 -0
- package/dist/ui/overlay/impl/pending-overlay-v2.d.ts +51 -0
- package/dist/ui/overlay/impl/pending-overlay-v2.js +445 -0
- package/dist/ui/overlay/impl/permission-overlay-v2.js +5 -5
- package/dist/ui/overlay/impl/permissions-overlay-v2.d.ts +85 -0
- package/dist/ui/overlay/impl/permissions-overlay-v2.js +820 -0
- package/dist/ui/overlay/impl/plan-approval-overlay-v2.d.ts +35 -0
- package/dist/ui/overlay/impl/plan-approval-overlay-v2.js +181 -0
- package/dist/ui/overlay/impl/project-edit-overlay-v2.d.ts +36 -0
- package/dist/ui/overlay/impl/project-edit-overlay-v2.js +195 -0
- package/dist/ui/overlay/impl/projects-overlay-v2.d.ts +1 -0
- package/dist/ui/overlay/impl/projects-overlay-v2.js +278 -44
- package/dist/ui/overlay/impl/reset-overlay-v2.d.ts +39 -0
- package/dist/ui/overlay/impl/reset-overlay-v2.js +107 -0
- package/dist/ui/overlay/impl/resume-overlay-v2.d.ts +60 -0
- package/dist/ui/overlay/impl/resume-overlay-v2.js +414 -0
- package/dist/ui/overlay/impl/session-mode-overlay-v2.d.ts +43 -0
- package/dist/ui/overlay/impl/session-mode-overlay-v2.js +124 -0
- package/dist/ui/overlay/impl/tasks-overlay-v2.d.ts +28 -0
- package/dist/ui/overlay/impl/tasks-overlay-v2.js +283 -0
- package/dist/ui/overlay/impl/team-overlay-v2.d.ts +86 -0
- package/dist/ui/overlay/impl/team-overlay-v2.js +692 -0
- package/dist/ui/overlay/impl/terminals-overlay-v2.d.ts +26 -0
- package/dist/ui/overlay/impl/terminals-overlay-v2.js +217 -0
- package/dist/ui/overlay/impl/tools-overlay-v2.js +3 -7
- package/dist/ui/overlay/impl/tutorial-overlay-v2.d.ts +30 -16
- package/dist/ui/overlay/impl/tutorial-overlay-v2.js +133 -956
- package/dist/ui/overlay/impl/workflow-overlay-v2.d.ts +1 -0
- package/dist/ui/overlay/impl/workflow-overlay-v2.js +10 -4
- package/dist/ui/overlay/index.d.ts +20 -1
- package/dist/ui/overlay/index.js +19 -0
- package/dist/ui/overlay/types.d.ts +5 -0
- package/dist/ui/overlay-manager.d.ts +43 -0
- package/dist/ui/overlay-manager.js +238 -0
- package/dist/ui/overlays.js +4 -16
- package/dist/ui/permission-overlay.js +6 -5
- package/dist/ui/status-bar-controller.d.ts +33 -0
- package/dist/ui/status-bar-controller.js +99 -0
- package/dist/ui/subagent-renderer.js +3 -19
- package/dist/ui/terminal-autocomplete-utils.d.ts +23 -0
- package/dist/ui/terminal-autocomplete-utils.js +83 -0
- package/dist/ui/terminal-line-builders.d.ts +17 -0
- package/dist/ui/terminal-line-builders.js +42 -0
- package/dist/ui/terminal-render-item.d.ts +16 -0
- package/dist/ui/terminal-render-item.js +267 -0
- package/dist/ui/terminal-renderer.d.ts +7 -8
- package/dist/ui/terminal-renderer.js +7 -8
- package/dist/ui/terminal-types.d.ts +179 -0
- package/dist/ui/terminal-types.js +34 -0
- package/dist/ui/terminal-ui.d.ts +144 -276
- package/dist/ui/terminal-ui.js +384 -1861
- package/dist/ui/todo-zone.d.ts +19 -1
- package/dist/ui/todo-zone.js +71 -13
- package/dist/ui/tool-formatters.js +696 -1
- package/dist/ui/turn-metrics.d.ts +56 -0
- package/dist/ui/turn-metrics.js +75 -0
- package/dist/ui/types.d.ts +28 -0
- package/dist/ui/types.js +1 -0
- package/dist/ui/vscode-diff-ipc.d.ts +102 -0
- package/dist/ui/vscode-diff-ipc.js +385 -0
- package/dist/utils/credentials.d.ts +24 -5
- package/dist/utils/credentials.js +123 -9
- package/dist/utils/format-tokens.d.ts +13 -0
- package/dist/utils/format-tokens.js +18 -0
- package/dist/utils/git-config.d.ts +26 -0
- package/dist/utils/git-config.js +54 -0
- package/dist/utils/message-utils.d.ts +61 -0
- package/dist/utils/message-utils.js +72 -0
- package/dist/utils/model-tiers.d.ts +8 -1
- package/dist/utils/model-tiers.js +38 -16
- package/dist/utils/open-browser.d.ts +5 -0
- package/dist/utils/open-browser.js +32 -0
- package/dist/utils/path-safety.js +3 -2
- package/dist/utils/project-detection.d.ts +58 -0
- package/dist/utils/project-detection.js +424 -0
- package/dist/utils/project-memory.js +2 -1
- package/dist/utils/project-status.d.ts +2 -2
- package/dist/utils/startup-perf.d.ts +18 -0
- package/dist/utils/startup-perf.js +60 -0
- package/dist/utils/token-tracker.d.ts +62 -0
- package/dist/utils/token-tracker.js +150 -0
- package/dist/utils/token-types.d.ts +23 -0
- package/dist/utils/token-types.js +18 -0
- package/dist/utils/types/config-types.d.ts +32 -0
- package/dist/utils/types/config-types.js +8 -0
- package/dist/utils/update-checker.d.ts +28 -0
- package/dist/utils/update-checker.js +106 -0
- package/dist/utils/version.d.ts +7 -0
- package/dist/utils/version.js +10 -0
- package/dist/utils/vscode-detect.d.ts +39 -0
- package/dist/utils/vscode-detect.js +137 -0
- package/package.json +27 -13
- package/dist/commands/handler-types.d.ts +0 -68
- package/dist/commands/handler-types.js +0 -8
- package/dist/commands/handlers/agent-commands.d.ts +0 -13
- package/dist/commands/handlers/agent-commands.js +0 -305
- package/dist/commands/handlers/design-commands.d.ts +0 -15
- package/dist/commands/handlers/design-commands.js +0 -334
- package/dist/commands/handlers/index.d.ts +0 -20
- package/dist/commands/handlers/index.js +0 -43
- package/dist/commands/handlers/overlay-commands.d.ts +0 -21
- package/dist/commands/handlers/overlay-commands.js +0 -287
- package/dist/commands/handlers/project-commands.d.ts +0 -11
- package/dist/commands/handlers/project-commands.js +0 -167
- package/dist/commands/handlers/simple-commands.d.ts +0 -19
- package/dist/commands/handlers/simple-commands.js +0 -144
- package/dist/commands/registry.d.ts +0 -50
- package/dist/commands/registry.js +0 -75
- package/dist/index.old.d.ts +0 -7
- package/dist/index.old.js +0 -1014
- package/dist/repl.d.ts +0 -149
- package/dist/repl.js +0 -1151
- package/dist/templates/claude-md.d.ts +0 -7
- package/dist/templates/claude-md.js +0 -189
- package/dist/test-autocomplete.d.ts +0 -7
- package/dist/test-autocomplete.js +0 -85
- package/dist/test-tabbed-menu.d.ts +0 -7
- package/dist/test-tabbed-menu.js +0 -25
- package/dist/tool-selector.d.ts +0 -71
- package/dist/tool-selector.js +0 -184
- package/dist/tools/anchor-tools.d.ts +0 -31
- package/dist/tools/anchor-tools.js +0 -255
- package/dist/tools/backlog-wrappers.d.ts +0 -54
- package/dist/tools/backlog-wrappers.js +0 -338
- package/dist/tools/document-db.d.ts +0 -43
- package/dist/tools/document-db.js +0 -220
- package/dist/tools/workitem-db.d.ts +0 -103
- package/dist/tools/workitem-db.js +0 -549
- package/dist/ui/agents-overlay-v2.d.ts +0 -43
- package/dist/ui/agents-overlay-v2.js +0 -809
- package/dist/ui/agents-overlay.d.ts +0 -12
- package/dist/ui/agents-overlay.js +0 -863
- package/dist/ui/anchors-overlay.d.ts +0 -12
- package/dist/ui/anchors-overlay.js +0 -775
- package/dist/ui/arch-type-overlay.d.ts +0 -15
- package/dist/ui/arch-type-overlay.js +0 -201
- package/dist/ui/ask-user-overlay-v2.d.ts +0 -26
- package/dist/ui/ask-user-overlay-v2.js +0 -555
- package/dist/ui/ask-user-simple-overlay-v2.d.ts +0 -25
- package/dist/ui/ask-user-simple-overlay-v2.js +0 -215
- package/dist/ui/backlog-overlay.d.ts +0 -32
- package/dist/ui/backlog-overlay.js +0 -652
- package/dist/ui/commands-overlay-v2.d.ts +0 -33
- package/dist/ui/commands-overlay-v2.js +0 -441
- package/dist/ui/commands-overlay.d.ts +0 -16
- package/dist/ui/commands-overlay.js +0 -439
- package/dist/ui/config-overlay.d.ts +0 -35
- package/dist/ui/config-overlay.js +0 -707
- package/dist/ui/docs-overlay.d.ts +0 -17
- package/dist/ui/docs-overlay.js +0 -303
- package/dist/ui/footer-v2.d.ts +0 -222
- package/dist/ui/footer-v2.js +0 -1349
- package/dist/ui/help-overlay-v2.d.ts +0 -34
- package/dist/ui/help-overlay-v2.js +0 -309
- package/dist/ui/help-overlay.d.ts +0 -16
- package/dist/ui/help-overlay.js +0 -316
- package/dist/ui/init-overlay-v2.d.ts +0 -34
- package/dist/ui/init-overlay-v2.js +0 -600
- package/dist/ui/init-overlay.d.ts +0 -34
- package/dist/ui/init-overlay.js +0 -604
- package/dist/ui/input-prompt-v2.d.ts +0 -180
- package/dist/ui/input-prompt-v2.js +0 -999
- package/dist/ui/iteration-limit-overlay-v2.d.ts +0 -21
- package/dist/ui/iteration-limit-overlay-v2.js +0 -114
- package/dist/ui/keys-overlay-v2.d.ts +0 -41
- package/dist/ui/keys-overlay-v2.js +0 -248
- package/dist/ui/mascot-overlay-v2.d.ts +0 -41
- package/dist/ui/mascot-overlay-v2.js +0 -138
- package/dist/ui/mascot-overlay.d.ts +0 -21
- package/dist/ui/mascot-overlay.js +0 -146
- package/dist/ui/model-overlay-v2.d.ts +0 -49
- package/dist/ui/model-overlay-v2.js +0 -118
- package/dist/ui/model-overlay.d.ts +0 -27
- package/dist/ui/model-overlay.js +0 -221
- package/dist/ui/model-warning-overlay.d.ts +0 -30
- package/dist/ui/model-warning-overlay.js +0 -169
- package/dist/ui/new-overlay.d.ts +0 -34
- package/dist/ui/new-overlay.js +0 -604
- package/dist/ui/overlay/impl/init-overlay-v2.d.ts +0 -77
- package/dist/ui/overlay/impl/init-overlay-v2.js +0 -593
- package/dist/ui/overlay/overlay-types.d.ts +0 -128
- package/dist/ui/overlay/overlay-types.js +0 -22
- package/dist/ui/overlays/help-overlay-v2.d.ts +0 -28
- package/dist/ui/overlays/help-overlay-v2.js +0 -198
- package/dist/ui/overlays/index.d.ts +0 -11
- package/dist/ui/overlays/index.js +0 -11
- package/dist/ui/permission-overlay-v2.d.ts +0 -36
- package/dist/ui/permission-overlay-v2.js +0 -380
- package/dist/ui/projects-overlay.d.ts +0 -19
- package/dist/ui/projects-overlay.js +0 -484
- package/dist/ui/theme-overlay-v2.d.ts +0 -42
- package/dist/ui/theme-overlay-v2.js +0 -135
- package/dist/ui/theme-overlay.d.ts +0 -24
- package/dist/ui/theme-overlay.js +0 -127
- package/dist/ui/tools-overlay-v2.d.ts +0 -47
- package/dist/ui/tools-overlay-v2.js +0 -218
- package/dist/ui/tools-overlay.d.ts +0 -34
- package/dist/ui/tools-overlay.js +0 -230
- package/dist/ui/tutorial-overlay-v2.d.ts +0 -31
- package/dist/ui/tutorial-overlay-v2.js +0 -1035
- package/dist/ui/tutorial-overlay.d.ts +0 -11
- package/dist/ui/tutorial-overlay.js +0 -1034
- package/dist/ui/workflow-overlay.d.ts +0 -22
- package/dist/ui/workflow-overlay.js +0 -636
package/dist/repl-v2.js
CHANGED
|
@@ -9,35 +9,174 @@
|
|
|
9
9
|
* Can be run standalone: npx tsx src/repl-v2.ts
|
|
10
10
|
* Or imported and used with a real agent from index.ts
|
|
11
11
|
*/
|
|
12
|
+
import { appendFileSync } from 'fs';
|
|
13
|
+
import { generateEditDiff, generateWriteDiff } from './ui/diff.js';
|
|
14
|
+
import { getPendingRequestsManager, setActiveSharedContext } from './multi-agent/index.js';
|
|
15
|
+
import { parseInputForMentions, hasReferences, buildMessageWithContext, ContextResolver, buildContextMap, } from './multi-agent/index.js';
|
|
16
|
+
import { getTeamCheckpointer } from './multi-agent/checkpointer.js';
|
|
17
|
+
import { getDelegationTracker } from './multi-agent/delegation-tracker.js';
|
|
18
|
+
import { initSessionRegistry, getSessionRegistry, setActiveTerminalSessionId, getActiveTerminalSessionId } from './multi-agent/session-registry.js';
|
|
19
|
+
import { initFileLockManager } from './multi-agent/file-locks.js';
|
|
20
|
+
import { initNotificationManager, getNotificationManager } from './multi-agent/notification-manager.js';
|
|
21
|
+
import { getSessionsPath } from './settings/paths.js';
|
|
22
|
+
import { setMetaToolFilter } from './tools.js';
|
|
23
|
+
import { getCurrentProject } from './tools/project-db.js';
|
|
12
24
|
import { TerminalUI } from './ui/terminal-ui.js';
|
|
13
25
|
import * as terminal from './ui/terminal.js';
|
|
14
26
|
import { getStyles } from './themes/index.js';
|
|
15
|
-
import { getSettings, getStartupMode } from './settings/index.js';
|
|
27
|
+
import { getSettings, getStartupMode, syncPermissionModeFromUI, getPermissionMode, permissionModeToAgentMode, getProjectSessionMode, isFirstRunComplete, getSessionRetentionDays, updateSettings } from './settings/index.js';
|
|
16
28
|
import { renderMascotWithLogo } from './ui/mascot/renderer.js';
|
|
17
|
-
import {
|
|
29
|
+
import { getStartupHighlights } from './changelog/index.js';
|
|
30
|
+
import { registerCommands, executeCommand, allCommands, getAutocompleteCommands, saveCurrentSession, saveCurrentTeam, loadProjectSession, archiveCurrentSession, convertMessagesToItems, } from './commands-v2/index.js';
|
|
18
31
|
import { getCustomCommandRegistry } from './commands/custom-registry.js';
|
|
32
|
+
import { getPlanModePrompt } from './prompts/plan-mode-prompt.js';
|
|
33
|
+
import { planRepository } from './db/repositories/index.js';
|
|
34
|
+
import { PlanApprovalOverlayV2, } from './ui/overlay/impl/plan-approval-overlay-v2.js';
|
|
19
35
|
import { PermissionOverlayV2, } from './ui/overlay/impl/permission-overlay-v2.js';
|
|
36
|
+
import { stripThinkingBlocks } from './utils/message-utils.js';
|
|
37
|
+
import { truncate } from './ui/base/index.js';
|
|
20
38
|
import { AskUserSimpleOverlayV2, } from './ui/overlay/impl/ask-user-simple-overlay-v2.js';
|
|
21
39
|
import { GuardrailOverlayV2, } from './ui/overlay/impl/guardrail-overlay-v2.js';
|
|
22
40
|
import { AskUserOverlayV2, } from './ui/overlay/impl/ask-user-overlay-v2.js';
|
|
23
41
|
import { IterationLimitOverlayV2, } from './ui/overlay/impl/iteration-limit-overlay-v2.js';
|
|
42
|
+
import { OnboardingWizardOverlayV2, } from './ui/overlay/impl/onboarding-wizard-overlay-v2.js';
|
|
43
|
+
import { LoginOverlayV2, } from './ui/overlay/impl/login-overlay-v2.js';
|
|
44
|
+
import { getAuthManager } from './auth/index.js';
|
|
45
|
+
import { KeysOverlayV2 } from './ui/overlay/impl/keys-overlay-v2.js';
|
|
46
|
+
import { SessionModeOverlayV2, } from './ui/overlay/impl/session-mode-overlay-v2.js';
|
|
47
|
+
import { DocumentDetailOverlayV2 } from './ui/overlay/impl/document-detail-overlay-v2.js';
|
|
24
48
|
import { formatToolResult } from './ui/tool-formatters.js';
|
|
49
|
+
import { formatTokens } from './ui/base/index.js';
|
|
25
50
|
import { isMemoryInput } from './input-handlers/index.js';
|
|
26
51
|
import { addAnchor } from './anchors/index.js';
|
|
27
52
|
import { getActiveProject } from './tools/project-db.js';
|
|
53
|
+
import { getProjectSessionManager } from './session/project-session-manager.js';
|
|
54
|
+
import { ResumeOverlayV2 } from './ui/overlay/impl/resume-overlay-v2.js';
|
|
55
|
+
import { updateCacheInfo, updateHistoryEstimate, formatBreakdownCompact, formatCacheInfo, estimateTokens } from './utils/token-tracker.js';
|
|
28
56
|
// Version for display (when running standalone)
|
|
29
57
|
const STANDALONE_VERSION = '0.4.0-v2';
|
|
30
58
|
// Register all commands on module load
|
|
31
59
|
registerCommands(allCommands);
|
|
60
|
+
/** Maximum messages per agent queue */
|
|
61
|
+
const MAX_QUEUE_SIZE = 10;
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Debug Logging (set DEBUG_AGENT_REQUESTS=1 to enable)
|
|
64
|
+
// =============================================================================
|
|
65
|
+
const DEBUG_LOG_FILE = '/tmp/compilr-agent-debug.log';
|
|
66
|
+
const DEBUG_ENABLED = process.env.DEBUG_AGENT_REQUESTS === '1';
|
|
67
|
+
let debugRequestCount = 0;
|
|
68
|
+
// Import tool registry functions for debug logging
|
|
69
|
+
import { getDirectTools, getMetaTools, getToolStats } from './tools.js';
|
|
70
|
+
import { getRegisteredMetaTools } from './tools/meta-tools.js';
|
|
71
|
+
/**
|
|
72
|
+
* Write debug info to log file before agent request.
|
|
73
|
+
* Enable with: DEBUG_AGENT_REQUESTS=1 compilr
|
|
74
|
+
* View with: tail -f /tmp/compilr-agent-debug.log
|
|
75
|
+
*/
|
|
76
|
+
function debugLogAgentRequest(agent, message) {
|
|
77
|
+
if (!DEBUG_ENABLED)
|
|
78
|
+
return;
|
|
79
|
+
try {
|
|
80
|
+
debugRequestCount++;
|
|
81
|
+
const state = agent.serialize();
|
|
82
|
+
const timestamp = new Date().toISOString();
|
|
83
|
+
const systemPromptChars = state.systemPrompt.length;
|
|
84
|
+
const systemPromptTokens = estimateTokens(state.systemPrompt);
|
|
85
|
+
const messagesJson = JSON.stringify(state.messages);
|
|
86
|
+
const messagesTokens = estimateTokens(messagesJson);
|
|
87
|
+
// Get tool definitions for logging
|
|
88
|
+
const directTools = getDirectTools();
|
|
89
|
+
const metaTools = getMetaTools();
|
|
90
|
+
const metaRegistryTools = getRegisteredMetaTools();
|
|
91
|
+
const toolStats = getToolStats();
|
|
92
|
+
// Calculate tool definition sizes
|
|
93
|
+
const directToolsJson = JSON.stringify(directTools.map(t => ({
|
|
94
|
+
name: t.definition.name,
|
|
95
|
+
description: t.definition.description,
|
|
96
|
+
inputSchema: t.definition.inputSchema,
|
|
97
|
+
})));
|
|
98
|
+
const metaToolsJson = JSON.stringify(metaTools.map(t => ({
|
|
99
|
+
name: t.definition.name,
|
|
100
|
+
description: t.definition.description,
|
|
101
|
+
inputSchema: t.definition.inputSchema,
|
|
102
|
+
})));
|
|
103
|
+
const directToolsTokens = estimateTokens(directToolsJson);
|
|
104
|
+
const metaToolsTokens = estimateTokens(metaToolsJson);
|
|
105
|
+
const totalToolTokens = directToolsTokens + metaToolsTokens;
|
|
106
|
+
const totalEstimate = systemPromptTokens + messagesTokens + totalToolTokens;
|
|
107
|
+
const logEntry = `
|
|
108
|
+
################################################################################
|
|
109
|
+
# REQUEST #${String(debugRequestCount)} @ ${timestamp}
|
|
110
|
+
################################################################################
|
|
111
|
+
|
|
112
|
+
USER MESSAGE:
|
|
113
|
+
${message.slice(0, 500)}${message.length > 500 ? '...(truncated)' : ''}
|
|
114
|
+
|
|
115
|
+
CONTEXT SIZES:
|
|
116
|
+
- System Prompt: ${systemPromptChars.toLocaleString()} chars (~${systemPromptTokens.toLocaleString()} tokens)
|
|
117
|
+
- Messages: ${messagesJson.length.toLocaleString()} chars (~${messagesTokens.toLocaleString()} tokens)
|
|
118
|
+
- Message Count: ${String(state.messages.length)}
|
|
119
|
+
- Direct Tools: ${directToolsJson.length.toLocaleString()} chars (~${directToolsTokens.toLocaleString()} tokens) [${String(directTools.length)} tools]
|
|
120
|
+
- Meta Tools: ${metaToolsJson.length.toLocaleString()} chars (~${metaToolsTokens.toLocaleString()} tokens) [${String(metaTools.length)} tools]
|
|
121
|
+
- Meta Registry: ${String(metaRegistryTools.length)} tools (accessed via use_tool)
|
|
122
|
+
--------------------------------------------------------------------------------
|
|
123
|
+
ESTIMATED TOTAL: ~${totalEstimate.toLocaleString()} tokens
|
|
124
|
+
(prompt: ${systemPromptTokens.toLocaleString()} + messages: ${messagesTokens.toLocaleString()} + tools: ${totalToolTokens.toLocaleString()})
|
|
125
|
+
|
|
126
|
+
Tool Stats: Direct=${String(toolStats.directTools)}, MetaRegistry=${String(toolStats.metaRegistryTools)}, Savings=~${String(toolStats.tokenSavings)}
|
|
127
|
+
|
|
128
|
+
================================================================================
|
|
129
|
+
FULL SYSTEM PROMPT (${systemPromptChars.toLocaleString()} chars):
|
|
130
|
+
================================================================================
|
|
131
|
+
${state.systemPrompt}
|
|
132
|
+
|
|
133
|
+
================================================================================
|
|
134
|
+
DIRECT TOOLS (${String(directTools.length)} tools, ~${directToolsTokens.toLocaleString()} tokens):
|
|
135
|
+
================================================================================
|
|
136
|
+
${directTools.map(t => `- ${t.definition.name}: ${t.definition.description.slice(0, 100)}...`).join('\n')}
|
|
137
|
+
|
|
138
|
+
================================================================================
|
|
139
|
+
META TOOLS (${String(metaTools.length)} tools, ~${metaToolsTokens.toLocaleString()} tokens):
|
|
140
|
+
================================================================================
|
|
141
|
+
${metaTools.map(t => `- ${t.definition.name}: ${t.definition.description.slice(0, 100)}...`).join('\n')}
|
|
142
|
+
|
|
143
|
+
================================================================================
|
|
144
|
+
META REGISTRY TOOLS (${String(metaRegistryTools.length)} tools, accessed via use_tool):
|
|
145
|
+
================================================================================
|
|
146
|
+
${metaRegistryTools.map(t => `- ${t.definition.name}`).join('\n')}
|
|
147
|
+
|
|
148
|
+
================================================================================
|
|
149
|
+
MESSAGES HISTORY (${String(state.messages.length)} messages):
|
|
150
|
+
================================================================================
|
|
151
|
+
${JSON.stringify(state.messages, null, 2)}
|
|
152
|
+
|
|
153
|
+
`;
|
|
154
|
+
appendFileSync(DEBUG_LOG_FILE, logEntry);
|
|
155
|
+
console.log(`[DEBUG] Request #${String(debugRequestCount)} logged to ${DEBUG_LOG_FILE}`);
|
|
156
|
+
console.log(`[DEBUG] Estimated: sys:${String(systemPromptTokens)}tok + msg:${String(messagesTokens)}tok + tools:${String(totalToolTokens)}tok = ${String(totalEstimate)}tok`);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
console.error('[DEBUG] Failed to log agent request:', error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
32
162
|
// =============================================================================
|
|
33
163
|
// REPL V2 Class
|
|
34
164
|
// =============================================================================
|
|
35
165
|
export class ReplV2 {
|
|
36
166
|
agent;
|
|
167
|
+
team;
|
|
168
|
+
agentFactory;
|
|
37
169
|
model;
|
|
38
170
|
provider;
|
|
39
171
|
version;
|
|
172
|
+
updateAvailable;
|
|
40
173
|
projectName;
|
|
174
|
+
// Multi-session tracking for background agents (Phase 3)
|
|
175
|
+
sessions = new Map();
|
|
176
|
+
foregroundAgentId = null;
|
|
177
|
+
// Per-agent message queues (Phase 3 - queued messages when agent is busy)
|
|
178
|
+
agentMessageQueues = new Map();
|
|
179
|
+
// Foreground session (backward compatible - will be replaced by sessions map)
|
|
41
180
|
currentSession = null;
|
|
42
181
|
ui;
|
|
43
182
|
onUIReady;
|
|
@@ -50,6 +189,20 @@ export class ReplV2 {
|
|
|
50
189
|
onSuggestionReady;
|
|
51
190
|
onSubagentReady;
|
|
52
191
|
clearSubagentTracking;
|
|
192
|
+
showOnboarding;
|
|
193
|
+
showLogin;
|
|
194
|
+
onDelegateReady;
|
|
195
|
+
onPendingDelegation;
|
|
196
|
+
onBackgroundAgentReady;
|
|
197
|
+
onBackgroundDelegationReady;
|
|
198
|
+
onSessionRegistered;
|
|
199
|
+
onTeamReplaced;
|
|
200
|
+
authOnly;
|
|
201
|
+
restoredSessionInfo = null;
|
|
202
|
+
// Phase 3b: Track which background agent is currently executing (for permission routing)
|
|
203
|
+
// This is set during the background agent's stream loop so the permission handler knows
|
|
204
|
+
// which agent is making the request.
|
|
205
|
+
currentExecutingBackgroundAgentId = null;
|
|
53
206
|
// Session stats
|
|
54
207
|
startTime = new Date();
|
|
55
208
|
sessionInputTokens = 0;
|
|
@@ -59,11 +212,17 @@ export class ReplV2 {
|
|
|
59
212
|
textAccumulator = '';
|
|
60
213
|
// Note: Subagent tracking is now simplified - callbacks receive toolUseId directly
|
|
61
214
|
// from agent.ts, so no more FIFO queue correlation needed!
|
|
215
|
+
// Bash abort controllers for Ctrl+B backgrounding feature
|
|
216
|
+
// Maps toolUseId -> AbortController for each running bash command
|
|
217
|
+
bashAbortControllers = new Map();
|
|
62
218
|
constructor(options = {}) {
|
|
63
219
|
this.agent = options.agent;
|
|
220
|
+
this.team = options.team;
|
|
221
|
+
this.agentFactory = options.agentFactory;
|
|
64
222
|
this.model = options.model ?? 'simulation';
|
|
65
223
|
this.provider = options.provider ?? 'simulation';
|
|
66
224
|
this.version = options.version ?? STANDALONE_VERSION;
|
|
225
|
+
this.updateAvailable = options.updateAvailable ?? null;
|
|
67
226
|
this.projectName = options.projectName ?? process.cwd().split('/').pop() ?? 'Project';
|
|
68
227
|
this.onUIReady = options.onUIReady;
|
|
69
228
|
this.onTextBufferReady = options.onTextBufferReady;
|
|
@@ -75,6 +234,15 @@ export class ReplV2 {
|
|
|
75
234
|
this.onSuggestionReady = options.onSuggestionReady;
|
|
76
235
|
this.onSubagentReady = options.onSubagentReady;
|
|
77
236
|
this.clearSubagentTracking = options.clearSubagentTracking;
|
|
237
|
+
this.showOnboarding = options.showOnboarding ?? false;
|
|
238
|
+
this.showLogin = options.showLogin ?? false;
|
|
239
|
+
this.onDelegateReady = options.onDelegateReady;
|
|
240
|
+
this.onPendingDelegation = options.onPendingDelegation;
|
|
241
|
+
this.onBackgroundAgentReady = options.onBackgroundAgentReady;
|
|
242
|
+
this.onBackgroundDelegationReady = options.onBackgroundDelegationReady;
|
|
243
|
+
this.onSessionRegistered = options.onSessionRegistered;
|
|
244
|
+
this.onTeamReplaced = options.onTeamReplaced;
|
|
245
|
+
this.authOnly = options.authOnly ?? false;
|
|
78
246
|
}
|
|
79
247
|
/**
|
|
80
248
|
* Flush accumulated agent text to the UI.
|
|
@@ -83,22 +251,582 @@ export class ReplV2 {
|
|
|
83
251
|
*/
|
|
84
252
|
flushTextBuffer() {
|
|
85
253
|
if (this.textAccumulator.trim()) {
|
|
86
|
-
this.ui.print({
|
|
254
|
+
this.ui.print({
|
|
255
|
+
type: 'agent-text',
|
|
256
|
+
text: this.textAccumulator.trim(),
|
|
257
|
+
expression: this.getActiveAgentMascot(),
|
|
258
|
+
});
|
|
87
259
|
this.textAccumulator = '';
|
|
88
260
|
}
|
|
89
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Get the mascot expression for the currently active agent.
|
|
264
|
+
* Returns the team agent's mascot if active, otherwise default neutral.
|
|
265
|
+
*/
|
|
266
|
+
getActiveAgentMascot() {
|
|
267
|
+
if (this.team) {
|
|
268
|
+
const activeAgent = this.team.getActive();
|
|
269
|
+
return activeAgent.mascot;
|
|
270
|
+
}
|
|
271
|
+
return '[•_•]'; // Default neutral mascot
|
|
272
|
+
}
|
|
273
|
+
// ===========================================================================
|
|
274
|
+
// Session Management (Phase 3 - Background Agents)
|
|
275
|
+
// ===========================================================================
|
|
276
|
+
/**
|
|
277
|
+
* Start a new agent session (foreground or background).
|
|
278
|
+
* For foreground: sets currentSession and foregroundAgentId
|
|
279
|
+
* For background: only adds to sessions map
|
|
280
|
+
*/
|
|
281
|
+
startSession(session) {
|
|
282
|
+
this.sessions.set(session.agentId, session);
|
|
283
|
+
if (!session.isBackground) {
|
|
284
|
+
this.currentSession = session;
|
|
285
|
+
this.foregroundAgentId = session.agentId;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* End an agent session.
|
|
290
|
+
* Removes from sessions map, clears currentSession if foreground,
|
|
291
|
+
* and processes any queued messages for this agent.
|
|
292
|
+
*/
|
|
293
|
+
endSession(agentId) {
|
|
294
|
+
this.sessions.delete(agentId);
|
|
295
|
+
if (this.foregroundAgentId === agentId) {
|
|
296
|
+
this.currentSession = null;
|
|
297
|
+
this.foregroundAgentId = null;
|
|
298
|
+
}
|
|
299
|
+
// Update background agent count in UI
|
|
300
|
+
this.ui.setBackgroundAgentCount(this.getBackgroundSessions().length);
|
|
301
|
+
// Process any queued messages for this agent
|
|
302
|
+
this.processAgentQueue(agentId);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Get a session by agent ID.
|
|
306
|
+
*/
|
|
307
|
+
getSession(agentId) {
|
|
308
|
+
return this.sessions.get(agentId);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Get all background sessions (non-foreground).
|
|
312
|
+
*/
|
|
313
|
+
getBackgroundSessions() {
|
|
314
|
+
return Array.from(this.sessions.values()).filter(s => s.isBackground);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Check if an agent has a running session.
|
|
318
|
+
*/
|
|
319
|
+
hasActiveSession(agentId) {
|
|
320
|
+
return this.sessions.has(agentId);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Check if any background agents are running.
|
|
324
|
+
*/
|
|
325
|
+
hasBackgroundAgents() {
|
|
326
|
+
return this.getBackgroundSessions().length > 0;
|
|
327
|
+
}
|
|
328
|
+
// ===========================================================================
|
|
329
|
+
// Per-Agent Message Queue (Phase 3)
|
|
330
|
+
// ===========================================================================
|
|
331
|
+
/**
|
|
332
|
+
* Queue a message for an agent that is currently busy.
|
|
333
|
+
* Returns the queue position (1-based).
|
|
334
|
+
*/
|
|
335
|
+
queueMessage(agentId, message, displayMessage) {
|
|
336
|
+
const queue = this.agentMessageQueues.get(agentId) ?? [];
|
|
337
|
+
// Check max queue size
|
|
338
|
+
if (queue.length >= MAX_QUEUE_SIZE) {
|
|
339
|
+
return null; // Queue is full
|
|
340
|
+
}
|
|
341
|
+
const queuedMessage = {
|
|
342
|
+
id: `${agentId}-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`,
|
|
343
|
+
message,
|
|
344
|
+
displayMessage,
|
|
345
|
+
queuedAt: new Date(),
|
|
346
|
+
};
|
|
347
|
+
queue.push(queuedMessage);
|
|
348
|
+
this.agentMessageQueues.set(agentId, queue);
|
|
349
|
+
return queue.length; // Return position (1-based)
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Get the number of messages queued for an agent.
|
|
353
|
+
*/
|
|
354
|
+
getQueueLength(agentId) {
|
|
355
|
+
return this.agentMessageQueues.get(agentId)?.length ?? 0;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Clear all queued messages for an agent.
|
|
359
|
+
* Returns the number of messages that were cleared.
|
|
360
|
+
*/
|
|
361
|
+
clearQueue(agentId) {
|
|
362
|
+
const queue = this.agentMessageQueues.get(agentId);
|
|
363
|
+
const count = queue?.length ?? 0;
|
|
364
|
+
this.agentMessageQueues.delete(agentId);
|
|
365
|
+
return count;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Clear all message queues for all agents.
|
|
369
|
+
* Returns the total number of messages cleared.
|
|
370
|
+
*/
|
|
371
|
+
clearAllQueues() {
|
|
372
|
+
let total = 0;
|
|
373
|
+
for (const queue of this.agentMessageQueues.values()) {
|
|
374
|
+
total += queue.length;
|
|
375
|
+
}
|
|
376
|
+
this.agentMessageQueues.clear();
|
|
377
|
+
return total;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Process the next message in an agent's queue.
|
|
381
|
+
* Called after an agent completes a task.
|
|
382
|
+
*/
|
|
383
|
+
processAgentQueue(agentId) {
|
|
384
|
+
const queue = this.agentMessageQueues.get(agentId);
|
|
385
|
+
if (!queue || queue.length === 0) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const nextMessage = queue.shift();
|
|
389
|
+
if (!nextMessage) {
|
|
390
|
+
return; // Should never happen due to length check above
|
|
391
|
+
}
|
|
392
|
+
this.agentMessageQueues.set(agentId, queue);
|
|
393
|
+
// Show that we're processing a queued message
|
|
394
|
+
const label = this.getAgentMascotLabel(agentId);
|
|
395
|
+
const preview = truncate(nextMessage.displayMessage, 50);
|
|
396
|
+
this.ui.print({
|
|
397
|
+
type: 'info',
|
|
398
|
+
message: `${label} processing queued message: "${preview}"`,
|
|
399
|
+
});
|
|
400
|
+
// Run the queued message as a background task (non-blocking)
|
|
401
|
+
void this.runAgentBackground(agentId, nextMessage.message);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Get detailed info about background sessions (for /bg overlay).
|
|
405
|
+
* Phase 3c: Background Status & Control
|
|
406
|
+
*/
|
|
407
|
+
getBackgroundSessionsInfo() {
|
|
408
|
+
const sessions = this.getBackgroundSessions();
|
|
409
|
+
const pendingManager = getPendingRequestsManager();
|
|
410
|
+
return sessions.map((session) => {
|
|
411
|
+
const teamAgent = this.team?.get(session.agentId);
|
|
412
|
+
const pendingForAgent = pendingManager.getByAgent(session.agentId);
|
|
413
|
+
const queuedCount = this.getQueueLength(session.agentId);
|
|
414
|
+
return {
|
|
415
|
+
agentId: session.agentId,
|
|
416
|
+
displayName: teamAgent?.displayName ?? session.agentId,
|
|
417
|
+
mascot: teamAgent?.mascot ?? '[•_•]',
|
|
418
|
+
currentAction: session.currentAction,
|
|
419
|
+
isBlocked: pendingForAgent.length > 0,
|
|
420
|
+
pendingCount: pendingForAgent.length,
|
|
421
|
+
queuedCount,
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Send the current foreground agent to background without aborting it.
|
|
427
|
+
* The agent continues running; the user regains the input prompt.
|
|
428
|
+
* Returns the agent ID that was sent to background, or null if no foreground session.
|
|
429
|
+
*/
|
|
430
|
+
sendForegroundToBackground() {
|
|
431
|
+
if (!this.currentSession || !this.foregroundAgentId) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const agentId = this.foregroundAgentId;
|
|
435
|
+
// Reclassify session as background (agent keeps running)
|
|
436
|
+
this.currentSession.isBackground = true;
|
|
437
|
+
this.foregroundAgentId = null;
|
|
438
|
+
this.currentSession = null;
|
|
439
|
+
// Update UI state — user regains input prompt
|
|
440
|
+
this.ui.setAgentRunning(false);
|
|
441
|
+
this.ui.setThinking(false);
|
|
442
|
+
return agentId;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Consolidated agent-switching method.
|
|
446
|
+
*
|
|
447
|
+
* Performs ALL operations needed to switch the foreground to a different agent:
|
|
448
|
+
* 1. Demote current foreground agent to background (if busy + different agent)
|
|
449
|
+
* 2. team.switchTo(agentId)
|
|
450
|
+
* 3. Update this.agent reference
|
|
451
|
+
* 4. Update meta-tool filter
|
|
452
|
+
* 5. Update footer UI
|
|
453
|
+
* 6. Update foreground session tracking (restore agentRunning/spinner)
|
|
454
|
+
* 7. Record telemetry
|
|
455
|
+
*
|
|
456
|
+
* Callers handle their own messaging (different styles/wording per context).
|
|
457
|
+
*/
|
|
458
|
+
async switchAgent(agentId, options = {}) {
|
|
459
|
+
const { demoteCurrentIfBusy = true, trackSession = true, recordTelemetry = true, } = options;
|
|
460
|
+
if (!this.team) {
|
|
461
|
+
throw new Error('Cannot switch agent: no team configured');
|
|
462
|
+
}
|
|
463
|
+
// 1. Demote current foreground if busy + switching to a different agent
|
|
464
|
+
let demotedAgentId = null;
|
|
465
|
+
if (demoteCurrentIfBusy &&
|
|
466
|
+
this.currentSession &&
|
|
467
|
+
this.foregroundAgentId &&
|
|
468
|
+
this.foregroundAgentId !== agentId) {
|
|
469
|
+
demotedAgentId = this.sendForegroundToBackground();
|
|
470
|
+
}
|
|
471
|
+
// 2. Switch the team's active agent
|
|
472
|
+
const teamAgent = await this.team.switchTo(agentId);
|
|
473
|
+
// 3. Update the REPL's agent reference
|
|
474
|
+
if (teamAgent.agent) {
|
|
475
|
+
this.agent = teamAgent.agent;
|
|
476
|
+
}
|
|
477
|
+
// 4. Update meta-tool filter
|
|
478
|
+
setMetaToolFilter(teamAgent.toolFilter ?? null);
|
|
479
|
+
// 5. Update footer UI
|
|
480
|
+
if (agentId !== 'default') {
|
|
481
|
+
this.ui.setActiveTeamAgent({
|
|
482
|
+
id: teamAgent.id,
|
|
483
|
+
displayName: teamAgent.displayName,
|
|
484
|
+
mascot: teamAgent.mascot,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
this.ui.setActiveTeamAgent(null);
|
|
489
|
+
}
|
|
490
|
+
// 5b. Update session registry with active agent
|
|
491
|
+
try {
|
|
492
|
+
const registry = getSessionRegistry();
|
|
493
|
+
if (registry) {
|
|
494
|
+
registry.setActiveAgent(agentId);
|
|
495
|
+
const agents = this.team.getAll();
|
|
496
|
+
registry.updateAgents(agents.map(a => ({
|
|
497
|
+
id: a.id,
|
|
498
|
+
displayName: a.displayName,
|
|
499
|
+
mascot: a.mascot,
|
|
500
|
+
})));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// Best effort
|
|
505
|
+
}
|
|
506
|
+
// 6. Update foreground session tracking
|
|
507
|
+
if (trackSession) {
|
|
508
|
+
const existingSession = this.sessions.get(agentId);
|
|
509
|
+
if (existingSession && existingSession.isBackground) {
|
|
510
|
+
// Agent has a background session — bring it to foreground
|
|
511
|
+
existingSession.isBackground = false;
|
|
512
|
+
this.foregroundAgentId = agentId;
|
|
513
|
+
this.currentSession = existingSession;
|
|
514
|
+
// Restore busy UI state — session exists means agent is still running
|
|
515
|
+
this.ui.setAgentRunning(true);
|
|
516
|
+
if (existingSession.currentAction) {
|
|
517
|
+
this.ui.setSpinnerText(existingSession.currentAction);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
else if (!existingSession) {
|
|
521
|
+
// No session yet — just update foreground ID so next run creates it correctly
|
|
522
|
+
this.foregroundAgentId = agentId;
|
|
523
|
+
this.currentSession = null;
|
|
524
|
+
}
|
|
525
|
+
// If existingSession exists and is already foreground, leave it as-is
|
|
526
|
+
}
|
|
527
|
+
// 7. Record telemetry
|
|
528
|
+
if (recordTelemetry) {
|
|
529
|
+
getAuthManager().recordAgent(agentId);
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
teamAgent: {
|
|
533
|
+
id: teamAgent.id,
|
|
534
|
+
displayName: teamAgent.displayName,
|
|
535
|
+
mascot: teamAgent.mascot,
|
|
536
|
+
agent: teamAgent.agent,
|
|
537
|
+
toolFilter: teamAgent.toolFilter,
|
|
538
|
+
},
|
|
539
|
+
demotedAgentId,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Bring a background agent to foreground.
|
|
544
|
+
* Validates the agent has a background session, then delegates to switchAgent().
|
|
545
|
+
* Phase 3c: Background Status & Control
|
|
546
|
+
*/
|
|
547
|
+
async bringAgentToForeground(agentId) {
|
|
548
|
+
const session = this.sessions.get(agentId);
|
|
549
|
+
if (!session || !session.isBackground) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
const { demotedAgentId } = await this.switchAgent(agentId);
|
|
553
|
+
// Print demotion notification (if a different agent was sent to background)
|
|
554
|
+
if (demotedAgentId) {
|
|
555
|
+
const demotedLabel = this.getAgentMascotLabel(demotedAgentId);
|
|
556
|
+
this.ui.print({
|
|
557
|
+
type: 'info',
|
|
558
|
+
message: `${demotedLabel} sent to background`,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
// Print foreground notification
|
|
562
|
+
const label = this.getAgentMascotLabel(agentId);
|
|
563
|
+
this.ui.print({
|
|
564
|
+
type: 'info',
|
|
565
|
+
message: `${label} brought to foreground`,
|
|
566
|
+
});
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Kill/cancel a background agent.
|
|
571
|
+
* Phase 3c: Background Status & Control
|
|
572
|
+
*/
|
|
573
|
+
killAgent(agentId) {
|
|
574
|
+
const session = this.sessions.get(agentId);
|
|
575
|
+
if (!session) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
// Abort the session
|
|
579
|
+
session.abortController.abort();
|
|
580
|
+
// Cancel any pending requests from this agent
|
|
581
|
+
const pendingManager = getPendingRequestsManager();
|
|
582
|
+
pendingManager.rejectAllForAgent(agentId, 'Agent killed by user');
|
|
583
|
+
// Clear queued messages (don't process on kill)
|
|
584
|
+
const discardedCount = this.clearQueue(agentId);
|
|
585
|
+
// Print notification
|
|
586
|
+
const label = this.getAgentMascotLabel(agentId);
|
|
587
|
+
let message = `${label} killed`;
|
|
588
|
+
if (discardedCount > 0) {
|
|
589
|
+
message += `. ${String(discardedCount)} queued message${discardedCount !== 1 ? 's' : ''} discarded.`;
|
|
590
|
+
}
|
|
591
|
+
this.ui.print({
|
|
592
|
+
type: 'warning',
|
|
593
|
+
message,
|
|
594
|
+
});
|
|
595
|
+
// Cancel any active delegations for this agent (no completion events)
|
|
596
|
+
getDelegationTracker().cancelAllForAgent(agentId);
|
|
597
|
+
// Session cleanup happens in the finally block of runAgentInBackground
|
|
598
|
+
// Note: We cleared the queue above, so endSession() won't process it
|
|
599
|
+
// Update background agent count immediately for responsive UI
|
|
600
|
+
this.ui.setBackgroundAgentCount(this.getBackgroundSessions().length);
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Get mascot label for an agent (for output labeling).
|
|
605
|
+
* Format: "[◈_◈] $arch" - mascot followed by agent reference
|
|
606
|
+
*/
|
|
607
|
+
getAgentMascotLabel(agentId) {
|
|
608
|
+
if (this.team) {
|
|
609
|
+
const teamAgent = this.team.get(agentId);
|
|
610
|
+
if (teamAgent) {
|
|
611
|
+
return `${teamAgent.mascot} $${agentId}`;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return `$${agentId}`;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Auto-expand conversation filter if user is sending to an agent not in the filter.
|
|
618
|
+
* Shows info message when filter is expanded.
|
|
619
|
+
* @returns true if filter was expanded, false otherwise
|
|
620
|
+
*/
|
|
621
|
+
autoExpandFilterIfNeeded(agentId) {
|
|
622
|
+
const wasAdded = this.ui.addToConversationFilter(agentId);
|
|
623
|
+
if (wasAdded) {
|
|
624
|
+
this.ui.print({
|
|
625
|
+
type: 'info',
|
|
626
|
+
message: `Added $${agentId} to conversation filter`,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
return wasAdded;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Regex to detect PLAN_READY marker in agent text.
|
|
633
|
+
* Format: [PLAN_READY:plan_id:plan_name]
|
|
634
|
+
*/
|
|
635
|
+
static PLAN_READY_REGEX = /\[PLAN_READY:(\d+):([^\]]+)\]/;
|
|
636
|
+
/**
|
|
637
|
+
* Check if text contains PLAN_READY marker and extract plan info.
|
|
638
|
+
*/
|
|
639
|
+
detectPlanReady(text) {
|
|
640
|
+
const match = ReplV2.PLAN_READY_REGEX.exec(text);
|
|
641
|
+
if (match) {
|
|
642
|
+
return {
|
|
643
|
+
planId: parseInt(match[1], 10),
|
|
644
|
+
planName: match[2],
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Handle plan approval flow.
|
|
651
|
+
* Shows the approval overlay and handles the user's choice.
|
|
652
|
+
*/
|
|
653
|
+
async handlePlanApproval(planId, planName) {
|
|
654
|
+
const overlay = new PlanApprovalOverlayV2({ planId, planName });
|
|
655
|
+
const result = await this.ui.showOverlay(overlay);
|
|
656
|
+
if (!result) {
|
|
657
|
+
// User cancelled or overlay closed without result
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
switch (result.action) {
|
|
661
|
+
case 'approve-auto':
|
|
662
|
+
// Update plan status to approved, switch to auto-accept mode
|
|
663
|
+
try {
|
|
664
|
+
planRepository.update(planId, { status: 'approved' });
|
|
665
|
+
this.ui.setMode('auto-accept');
|
|
666
|
+
this.ui.setActivePlan(planId, planName);
|
|
667
|
+
this.ui.print({ type: 'info', message: `Plan "${planName}" approved. Switching to auto-accept mode.` });
|
|
668
|
+
}
|
|
669
|
+
catch (e) {
|
|
670
|
+
this.ui.print({ type: 'error', message: `Failed to approve plan: ${e instanceof Error ? e.message : String(e)}` });
|
|
671
|
+
}
|
|
672
|
+
break;
|
|
673
|
+
case 'approve':
|
|
674
|
+
// Update plan status to approved, switch to normal mode
|
|
675
|
+
try {
|
|
676
|
+
planRepository.update(planId, { status: 'approved' });
|
|
677
|
+
this.ui.setMode('normal');
|
|
678
|
+
this.ui.setActivePlan(planId, planName);
|
|
679
|
+
this.ui.print({ type: 'info', message: `Plan "${planName}" approved. Switching to normal mode.` });
|
|
680
|
+
}
|
|
681
|
+
catch (e) {
|
|
682
|
+
this.ui.print({ type: 'error', message: `Failed to approve plan: ${e instanceof Error ? e.message : String(e)}` });
|
|
683
|
+
}
|
|
684
|
+
break;
|
|
685
|
+
case 'revise':
|
|
686
|
+
// Keep plan as draft, show input for feedback
|
|
687
|
+
this.ui.print({ type: 'info', message: 'Please provide feedback on what you\'d like to change:' });
|
|
688
|
+
// The user will type their feedback as the next message
|
|
689
|
+
// The agent will receive it and update the plan
|
|
690
|
+
break;
|
|
691
|
+
case 'cancel':
|
|
692
|
+
// Mark plan as abandoned
|
|
693
|
+
try {
|
|
694
|
+
planRepository.update(planId, { status: 'abandoned' });
|
|
695
|
+
this.ui.clearActivePlan();
|
|
696
|
+
this.ui.print({ type: 'warning', message: `Plan "${planName}" abandoned.` });
|
|
697
|
+
}
|
|
698
|
+
catch (e) {
|
|
699
|
+
this.ui.print({ type: 'error', message: `Failed to abandon plan: ${e instanceof Error ? e.message : String(e)}` });
|
|
700
|
+
}
|
|
701
|
+
break;
|
|
702
|
+
case 'view':
|
|
703
|
+
// Show the full plan in document detail overlay
|
|
704
|
+
try {
|
|
705
|
+
const plan = planRepository.getById(planId);
|
|
706
|
+
if (plan) {
|
|
707
|
+
// Convert Plan to ProjectDocument for the detail overlay
|
|
708
|
+
const doc = {
|
|
709
|
+
id: plan.id,
|
|
710
|
+
projectId: plan.projectId,
|
|
711
|
+
docType: 'plan',
|
|
712
|
+
title: plan.name,
|
|
713
|
+
content: plan.content,
|
|
714
|
+
status: plan.status,
|
|
715
|
+
workItemId: plan.workItemId,
|
|
716
|
+
createdAt: plan.createdAt,
|
|
717
|
+
updatedAt: plan.updatedAt,
|
|
718
|
+
};
|
|
719
|
+
const detailOverlay = new DocumentDetailOverlayV2(doc);
|
|
720
|
+
await this.ui.showOverlay(detailOverlay);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
catch (e) {
|
|
724
|
+
this.ui.print({ type: 'error', message: `Failed to load plan: ${e instanceof Error ? e.message : String(e)}` });
|
|
725
|
+
}
|
|
726
|
+
// After viewing, re-show the approval overlay
|
|
727
|
+
await this.handlePlanApproval(planId, planName);
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Restore the agent from a previous state.
|
|
733
|
+
* Called by /resume command to swap in a restored agent.
|
|
734
|
+
*/
|
|
735
|
+
restoreAgent(newAgent) {
|
|
736
|
+
this.agent = newAgent;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Get the current agent instance.
|
|
740
|
+
* Used by commands that need to serialize/checkpoint the agent state.
|
|
741
|
+
*/
|
|
742
|
+
getAgent() {
|
|
743
|
+
return this.agent;
|
|
744
|
+
}
|
|
90
745
|
/**
|
|
91
746
|
* Start the REPL.
|
|
92
747
|
*/
|
|
93
748
|
start() {
|
|
94
|
-
//
|
|
749
|
+
// Register this terminal in the session registry (before UI so footer can subscribe)
|
|
750
|
+
try {
|
|
751
|
+
const registry = initSessionRegistry(getSessionsPath());
|
|
752
|
+
const activeProject = getActiveProject();
|
|
753
|
+
const sessionId = registry.register(activeProject?.id ?? null);
|
|
754
|
+
setActiveTerminalSessionId(sessionId);
|
|
755
|
+
// Notify index.ts of session ID (for VS Code diff IPC prefix)
|
|
756
|
+
this.onSessionRegistered?.(sessionId);
|
|
757
|
+
// Initialize file lock manager (uses same shared DB)
|
|
758
|
+
initFileLockManager();
|
|
759
|
+
// Initialize notification manager (cross-session notifications)
|
|
760
|
+
const notifManager = initNotificationManager();
|
|
761
|
+
notifManager.startPolling();
|
|
762
|
+
// Subscribe to pending requests — notify other terminals
|
|
763
|
+
getPendingRequestsManager().on('request-added', (request) => {
|
|
764
|
+
try {
|
|
765
|
+
const sid = getActiveTerminalSessionId();
|
|
766
|
+
const proj = getActiveProject();
|
|
767
|
+
if (sid && proj) {
|
|
768
|
+
notifManager.insert({
|
|
769
|
+
projectId: proj.id,
|
|
770
|
+
fromSessionId: sid,
|
|
771
|
+
type: 'pending_request',
|
|
772
|
+
title: `${request.agentId} needs ${request.type === 'permission' ? 'approval' : 'input'}`,
|
|
773
|
+
message: request.prompt.slice(0, 200),
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
// Best effort
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
// Sync initial team roster to registry (covers restored teams from disk)
|
|
782
|
+
if (this.team) {
|
|
783
|
+
const agents = this.team.getAll();
|
|
784
|
+
registry.updateAgents(agents.map(a => ({
|
|
785
|
+
id: a.id,
|
|
786
|
+
displayName: a.displayName,
|
|
787
|
+
mascot: a.mascot,
|
|
788
|
+
})));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
catch {
|
|
792
|
+
// Best effort — session registry is non-critical
|
|
793
|
+
}
|
|
794
|
+
// Create TerminalUI with mode synced from settings
|
|
795
|
+
const savedPermissionMode = getPermissionMode();
|
|
796
|
+
const initialUiMode = permissionModeToAgentMode(savedPermissionMode);
|
|
95
797
|
this.ui = new TerminalUI({
|
|
96
|
-
initialMode:
|
|
798
|
+
initialMode: initialUiMode,
|
|
97
799
|
});
|
|
98
800
|
// Print welcome screen
|
|
99
|
-
|
|
801
|
+
// Skip if: login overlay shows its own branded welcome,
|
|
802
|
+
// or startup mode is 'menu' (the /menu command prints its own logo)
|
|
803
|
+
if (!this.showLogin && getStartupMode() !== 'menu') {
|
|
804
|
+
this.printWelcome();
|
|
805
|
+
}
|
|
100
806
|
// Set initial state
|
|
101
807
|
this.ui.setProjectName(this.projectName);
|
|
808
|
+
this.ui.setCommandsCallback(getAutocompleteCommands);
|
|
809
|
+
// Set up agent suggestions callback for $ autocomplete (if team is available)
|
|
810
|
+
if (this.team) {
|
|
811
|
+
const team = this.team; // Capture for closure
|
|
812
|
+
this.ui.setAgentSuggestionsCallback(() => {
|
|
813
|
+
const agents = team.getAll();
|
|
814
|
+
return agents.map((agent) => ({
|
|
815
|
+
id: agent.id,
|
|
816
|
+
displayName: agent.displayName,
|
|
817
|
+
mascot: agent.mascot,
|
|
818
|
+
}));
|
|
819
|
+
});
|
|
820
|
+
// Set the active agent in the footer (for restored teams with non-default active agent)
|
|
821
|
+
const activeAgent = team.getActive();
|
|
822
|
+
if (activeAgent.id !== 'default') {
|
|
823
|
+
this.ui.setActiveTeamAgent({
|
|
824
|
+
id: activeAgent.id,
|
|
825
|
+
displayName: activeAgent.displayName,
|
|
826
|
+
mascot: activeAgent.mascot,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
102
830
|
// Notify caller that UI is ready and provide the V2 permission overlay function
|
|
103
831
|
if (this.onUIReady) {
|
|
104
832
|
const ui = this.ui;
|
|
@@ -149,6 +877,24 @@ export class ReplV2 {
|
|
|
149
877
|
return result ?? { continue: false };
|
|
150
878
|
});
|
|
151
879
|
}
|
|
880
|
+
// Provide the delegate overlay function to the caller
|
|
881
|
+
// Uses AskUserSimpleOverlayV2 with a formatted delegation question
|
|
882
|
+
if (this.onDelegateReady) {
|
|
883
|
+
const ui = this.ui;
|
|
884
|
+
this.onDelegateReady(async (options) => {
|
|
885
|
+
// Build a formatted question for the delegation approval
|
|
886
|
+
const question = options.reason
|
|
887
|
+
? `Delegate to $${options.agentId} (${options.agentDisplayName})?\nReason: ${options.reason}\nTask: ${truncate(options.task, 100)}`
|
|
888
|
+
: `Delegate to $${options.agentId} (${options.agentDisplayName})?\nTask: ${truncate(options.task, 100)}`;
|
|
889
|
+
const overlay = new AskUserSimpleOverlayV2({
|
|
890
|
+
question,
|
|
891
|
+
options: ['Yes, delegate', 'No, continue'],
|
|
892
|
+
allowCustom: false,
|
|
893
|
+
});
|
|
894
|
+
const result = await ui.showOverlay(overlay);
|
|
895
|
+
return { approved: !result?.skipped && result?.answer === 'Yes, delegate' };
|
|
896
|
+
});
|
|
897
|
+
}
|
|
152
898
|
// Provide the suggestion setter function to the caller
|
|
153
899
|
if (this.onSuggestionReady) {
|
|
154
900
|
const ui = this.ui;
|
|
@@ -184,6 +930,53 @@ export class ReplV2 {
|
|
|
184
930
|
},
|
|
185
931
|
});
|
|
186
932
|
}
|
|
933
|
+
// Phase 3b: Provide background agent tracking callbacks
|
|
934
|
+
if (this.onBackgroundAgentReady) {
|
|
935
|
+
this.onBackgroundAgentReady({
|
|
936
|
+
// Check if an agent is running in background
|
|
937
|
+
isBackgroundAgent: (agentId) => {
|
|
938
|
+
if (agentId) {
|
|
939
|
+
const session = this.sessions.get(agentId);
|
|
940
|
+
return session?.isBackground ?? false;
|
|
941
|
+
}
|
|
942
|
+
// If no agentId provided, check if we're currently executing a background agent
|
|
943
|
+
return this.currentExecutingBackgroundAgentId !== null;
|
|
944
|
+
},
|
|
945
|
+
// Get the ID of the background agent currently executing (for permission routing)
|
|
946
|
+
getActiveBackgroundAgentId: () => {
|
|
947
|
+
return this.currentExecutingBackgroundAgentId;
|
|
948
|
+
},
|
|
949
|
+
// Get info about a background agent (mascot, etc.)
|
|
950
|
+
getBackgroundAgentInfo: (agentId) => {
|
|
951
|
+
if (!this.team)
|
|
952
|
+
return null;
|
|
953
|
+
const teamAgent = this.team.get(agentId);
|
|
954
|
+
if (!teamAgent)
|
|
955
|
+
return null;
|
|
956
|
+
return { mascot: teamAgent.mascot };
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
// Phase 3d-beta: Provide background delegation callback
|
|
961
|
+
if (this.onBackgroundDelegationReady) {
|
|
962
|
+
this.onBackgroundDelegationReady((agentId, message) => {
|
|
963
|
+
if (!this.agent)
|
|
964
|
+
return null;
|
|
965
|
+
// Check if agent has an active session (busy)
|
|
966
|
+
const existingSession = this.sessions.get(agentId);
|
|
967
|
+
if (existingSession) {
|
|
968
|
+
// Agent is busy — queue the delegation message
|
|
969
|
+
const position = this.queueMessage(agentId, message, message.slice(0, 80));
|
|
970
|
+
if (position === null) {
|
|
971
|
+
return null; // Queue is full
|
|
972
|
+
}
|
|
973
|
+
return 'queued';
|
|
974
|
+
}
|
|
975
|
+
// Agent is free — start background session immediately
|
|
976
|
+
void this.runAgentBackground(agentId, message);
|
|
977
|
+
return 'running';
|
|
978
|
+
});
|
|
979
|
+
}
|
|
187
980
|
// Event handlers
|
|
188
981
|
this.ui.on('submit', (input) => {
|
|
189
982
|
void this.processInput(input);
|
|
@@ -214,27 +1007,560 @@ export class ReplV2 {
|
|
|
214
1007
|
});
|
|
215
1008
|
this.ui.on('modeChange', () => {
|
|
216
1009
|
const modes = ['normal', 'auto-accept', 'plan'];
|
|
217
|
-
const
|
|
1010
|
+
const currentMode = this.ui.getMode();
|
|
1011
|
+
const currentIndex = modes.indexOf(currentMode);
|
|
218
1012
|
const nextMode = modes[(currentIndex + 1) % modes.length];
|
|
219
1013
|
this.ui.setMode(nextMode);
|
|
220
|
-
|
|
1014
|
+
// Sync the permission mode setting with UI mode
|
|
1015
|
+
syncPermissionModeFromUI(nextMode);
|
|
1016
|
+
// Clear session grants when switching FROM auto-accept TO a more restrictive mode
|
|
1017
|
+
let grantsCleared = 0;
|
|
1018
|
+
if (currentMode === 'auto-accept' && nextMode !== 'auto-accept' && this.agent) {
|
|
1019
|
+
const grants = this.agent.getSessionPermissions();
|
|
1020
|
+
if (grants.length > 0) {
|
|
1021
|
+
grantsCleared = grants.length;
|
|
1022
|
+
this.agent.clearSessionPermissions();
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
// Show mode change message (with grant clearing info if applicable)
|
|
1026
|
+
if (grantsCleared > 0) {
|
|
1027
|
+
this.ui.print({
|
|
1028
|
+
type: 'info',
|
|
1029
|
+
message: `Mode: ${nextMode} (${String(grantsCleared)} grant${grantsCleared > 1 ? 's' : ''} cleared)`,
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
else {
|
|
1033
|
+
this.ui.print({ type: 'info', message: `Mode: ${nextMode}` });
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
// Ctrl+B - show background tasks overlay
|
|
1037
|
+
this.ui.on('showTasks', () => {
|
|
1038
|
+
void (async () => {
|
|
1039
|
+
const { TasksOverlayV2 } = await import('./ui/overlay/impl/tasks-overlay-v2.js');
|
|
1040
|
+
const overlay = new TasksOverlayV2();
|
|
1041
|
+
await this.ui.showOverlay(overlay);
|
|
1042
|
+
})();
|
|
1043
|
+
});
|
|
1044
|
+
// Double Tab - show pending requests overlay
|
|
1045
|
+
this.ui.on('showPending', () => {
|
|
1046
|
+
void executeCommand('pending', '', this.createCommandContext());
|
|
1047
|
+
});
|
|
1048
|
+
// Ctrl+B during bash execution - move to background
|
|
1049
|
+
this.ui.on('backgroundBash', (info) => {
|
|
1050
|
+
const controller = this.bashAbortControllers.get(info.toolUseId);
|
|
1051
|
+
if (controller) {
|
|
1052
|
+
// Signal the bash tool to move process to ShellManager
|
|
1053
|
+
controller.abort('background');
|
|
1054
|
+
this.bashAbortControllers.delete(info.toolUseId);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
// Start UI (skip initial render if a startup overlay will appear immediately)
|
|
1058
|
+
const hasStartupOverlay = this.showLogin || this.showOnboarding || getStartupMode() === 'menu';
|
|
1059
|
+
this.ui.start(hasStartupOverlay);
|
|
1060
|
+
// Startup flow: login → onboarding → normal startup
|
|
1061
|
+
if (this.showLogin) {
|
|
1062
|
+
// Not authenticated — show branded login overlay first
|
|
1063
|
+
void this.handleLoginFlow();
|
|
1064
|
+
}
|
|
1065
|
+
else if (this.showOnboarding) {
|
|
1066
|
+
// Authenticated but first run — show onboarding wizard
|
|
1067
|
+
void this.showOnboardingWizard();
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
// Normal startup — authenticated, onboarding complete
|
|
1071
|
+
void this.handleStartupSequence();
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Handle login flow: show branded login overlay, then continue to onboarding or normal startup.
|
|
1076
|
+
*/
|
|
1077
|
+
async handleLoginFlow() {
|
|
1078
|
+
const loginOverlay = new LoginOverlayV2(this.version);
|
|
1079
|
+
const result = await this.ui.showOverlay(loginOverlay);
|
|
1080
|
+
if (!result?.success) {
|
|
1081
|
+
// Login failed or cancelled — exit
|
|
1082
|
+
this.stop();
|
|
1083
|
+
process.exit(1);
|
|
1084
|
+
}
|
|
1085
|
+
// Send forced heartbeat in background (don't block UI)
|
|
1086
|
+
const auth = getAuthManager();
|
|
1087
|
+
void auth.sendForcedHeartbeat();
|
|
1088
|
+
// Now check if first run
|
|
1089
|
+
if (!isFirstRunComplete()) {
|
|
1090
|
+
await this.showOnboardingWizard();
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
// Re-login (user did /logout then /login or restarted)
|
|
1094
|
+
// Print welcome banner now (was skipped before login overlay)
|
|
1095
|
+
// Skip if startup mode is 'menu' — the /menu command prints its own logo
|
|
1096
|
+
if (getStartupMode() !== 'menu') {
|
|
1097
|
+
this.printWelcome();
|
|
1098
|
+
}
|
|
1099
|
+
// Start session in background, show startup immediately
|
|
1100
|
+
void auth.startSession();
|
|
1101
|
+
auth.setLLMInfo(this.provider, this.model);
|
|
1102
|
+
await this.handleStartupSequence();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Handle startup sequence: session restoration first, then menu if applicable.
|
|
1107
|
+
* This ensures proper ordering of startup overlays.
|
|
1108
|
+
*/
|
|
1109
|
+
async handleStartupSequence() {
|
|
1110
|
+
// Start heartbeat session in background (don't block UI)
|
|
1111
|
+
// Note: auth was already verified in main() — no need to re-check here
|
|
1112
|
+
if (!this.showLogin) {
|
|
1113
|
+
const auth = getAuthManager();
|
|
1114
|
+
void auth.startSession();
|
|
1115
|
+
auth.setLLMInfo(this.provider, this.model);
|
|
1116
|
+
}
|
|
1117
|
+
// Non-blocking cleanup of legacy (pre-multi-terminal) files and old history
|
|
1118
|
+
void getProjectSessionManager().cleanupLegacyFiles();
|
|
1119
|
+
const retentionDays = getSessionRetentionDays();
|
|
1120
|
+
if (retentionDays > 0) {
|
|
1121
|
+
void getProjectSessionManager().cleanupOldSessions(retentionDays);
|
|
1122
|
+
}
|
|
1123
|
+
// If startup mode is 'menu', show menu first (project selection),
|
|
1124
|
+
// then handle session restoration for the selected project.
|
|
1125
|
+
const startupMode = getStartupMode();
|
|
1126
|
+
if (startupMode === 'menu') {
|
|
1127
|
+
await this.handleCommand('menu', '');
|
|
1128
|
+
}
|
|
1129
|
+
// Check session mode setting (runs after project is known)
|
|
1130
|
+
const sessionMode = getProjectSessionMode();
|
|
1131
|
+
if (sessionMode === 'auto') {
|
|
1132
|
+
// Auto mode: silently restore session + team
|
|
1133
|
+
await this.autoRestoreSession();
|
|
1134
|
+
}
|
|
1135
|
+
else if (sessionMode === 'ask') {
|
|
1136
|
+
// Ask mode: show overlay to let user choose
|
|
1137
|
+
await this.handleSessionModePrompt();
|
|
1138
|
+
}
|
|
1139
|
+
// 'fresh' mode: do nothing - start with clean session
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Restore a persisted team for a given terminal prefix.
|
|
1143
|
+
* Replaces the current (fresh) team with the one from disk.
|
|
1144
|
+
* Returns true if a team was restored.
|
|
1145
|
+
*/
|
|
1146
|
+
restoreTeamForPrefix(projectId, terminalPrefix) {
|
|
1147
|
+
if (!this.team || !this.agentFactory)
|
|
1148
|
+
return false;
|
|
1149
|
+
const checkpointer = getTeamCheckpointer();
|
|
1150
|
+
const persistedTeam = checkpointer.loadTeamByPrefix(projectId, terminalPrefix, this.agentFactory);
|
|
1151
|
+
if (!persistedTeam || persistedTeam.size <= 1)
|
|
1152
|
+
return false;
|
|
1153
|
+
// Set the current agent as default in the restored team
|
|
1154
|
+
const currentAgent = this.agent;
|
|
1155
|
+
if (currentAgent) {
|
|
1156
|
+
persistedTeam.setDefaultAgent(currentAgent);
|
|
1157
|
+
}
|
|
1158
|
+
// Replace the team and update UI autocomplete
|
|
1159
|
+
this.team = persistedTeam;
|
|
1160
|
+
this.onTeamReplaced?.(persistedTeam);
|
|
1161
|
+
setActiveSharedContext(persistedTeam.sharedContext);
|
|
1162
|
+
this.ui.setAgentSuggestionsCallback(() => {
|
|
1163
|
+
const agents = persistedTeam.getAll();
|
|
1164
|
+
return agents.map(a => ({ id: a.id, displayName: a.displayName, mascot: a.mascot }));
|
|
1165
|
+
});
|
|
1166
|
+
// Populate project info in shared context
|
|
1167
|
+
const activeProject = getActiveProject();
|
|
1168
|
+
if (activeProject) {
|
|
1169
|
+
persistedTeam.sharedContext.setProject({
|
|
1170
|
+
id: activeProject.id,
|
|
1171
|
+
name: activeProject.displayName,
|
|
1172
|
+
path: activeProject.path,
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
// Set team roster as anchor on the default agent
|
|
1176
|
+
if (persistedTeam.sharedContext.hasTeamRoster() && currentAgent?.hasAnchors()) {
|
|
1177
|
+
currentAgent.addAnchor({
|
|
1178
|
+
id: 'team-roster',
|
|
1179
|
+
content: persistedTeam.sharedContext.formatTeamRoster(),
|
|
1180
|
+
priority: 'info',
|
|
1181
|
+
scope: 'session',
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
// Sync roster to session registry
|
|
1185
|
+
const registry = getSessionRegistry();
|
|
1186
|
+
if (registry) {
|
|
1187
|
+
const agents = persistedTeam.getAll();
|
|
1188
|
+
registry.updateAgents(agents.map(a => ({
|
|
1189
|
+
id: a.id,
|
|
1190
|
+
displayName: a.displayName,
|
|
1191
|
+
mascot: a.mascot,
|
|
1192
|
+
})));
|
|
1193
|
+
}
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Auto-restore session silently (for 'auto' mode).
|
|
1198
|
+
* Restores both conversation history AND team roster.
|
|
1199
|
+
* Called at startup when projectSessionMode='auto'.
|
|
1200
|
+
*
|
|
1201
|
+
* Behavior depends on whether other terminals are active on the same project:
|
|
1202
|
+
* - Single terminal: silently restore the most recent available session + claim it
|
|
1203
|
+
* - Multiple terminals: fall back to 'ask' mode (show overlay)
|
|
1204
|
+
*/
|
|
1205
|
+
async autoRestoreSession() {
|
|
1206
|
+
if (!this.agent)
|
|
1207
|
+
return;
|
|
1208
|
+
const activeProject = getActiveProject();
|
|
1209
|
+
const projectId = activeProject?.id ?? null;
|
|
1210
|
+
const ownPrefix = getActiveTerminalSessionId()?.slice(0, 8) ?? null;
|
|
1211
|
+
// Check how many terminals are active on this project
|
|
1212
|
+
// (our own terminal is already registered, so count=1 means we're alone)
|
|
1213
|
+
const registry = getSessionRegistry();
|
|
1214
|
+
const activeCount = registry?.getActiveSessionCount(projectId) ?? 1;
|
|
1215
|
+
if (activeCount > 1) {
|
|
1216
|
+
// Multiple terminals — can't auto-pick, fall back to ask mode (overlay)
|
|
1217
|
+
await this.offerOtherTerminalSessions(projectId);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
// Single terminal — silently restore the most recent available session
|
|
1221
|
+
const psm = getProjectSessionManager();
|
|
1222
|
+
const allSessions = await psm.listProjectSessions(projectId);
|
|
1223
|
+
// Filter to available sessions (not from active terminals, has content)
|
|
1224
|
+
const available = allSessions.filter(s => {
|
|
1225
|
+
if (s.isActiveTerminal)
|
|
1226
|
+
return false;
|
|
1227
|
+
return s.messageCount > 0 || (s.teamAgentCount !== undefined && s.teamAgentCount > 1);
|
|
1228
|
+
});
|
|
1229
|
+
if (available.length === 0) {
|
|
1230
|
+
// No sessions to restore — start fresh silently
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
// Pick the most recent session (already sorted by updatedAt descending)
|
|
1234
|
+
const target = available[0];
|
|
1235
|
+
const targetPrefix = target.terminalPrefix;
|
|
1236
|
+
// Restore team roster
|
|
1237
|
+
if (targetPrefix) {
|
|
1238
|
+
this.restoreTeamForPrefix(projectId, targetPrefix);
|
|
1239
|
+
}
|
|
1240
|
+
// Load and restore conversation
|
|
1241
|
+
const session = targetPrefix
|
|
1242
|
+
? await psm.loadProjectSessionByPrefix(projectId, targetPrefix)
|
|
1243
|
+
: null;
|
|
1244
|
+
// Claim the session files (rename to our prefix)
|
|
1245
|
+
if (targetPrefix && ownPrefix && targetPrefix !== ownPrefix) {
|
|
1246
|
+
psm.claimSession(projectId, targetPrefix, ownPrefix);
|
|
1247
|
+
getTeamCheckpointer().claimTeamFiles(projectId, targetPrefix, ownPrefix);
|
|
1248
|
+
}
|
|
1249
|
+
if (session && session.messages.length > 0) {
|
|
1250
|
+
try {
|
|
1251
|
+
const filteredMessages = stripThinkingBlocks(session.messages);
|
|
1252
|
+
await this.agent.setHistory(filteredMessages, { turnCount: session.turnCount });
|
|
1253
|
+
const historyItems = convertMessagesToItems(filteredMessages);
|
|
1254
|
+
this.ui.setRestoredHistory(historyItems);
|
|
1255
|
+
const parts = [`${String(session.messages.length)} messages`];
|
|
1256
|
+
if (this.team && this.team.size > 1) {
|
|
1257
|
+
parts.push(`${String(this.team.size)} agents`);
|
|
1258
|
+
}
|
|
1259
|
+
const msg = `Session restored (${parts.join(', ')}). Tip: Use Ctrl+R to view conversation history.`;
|
|
1260
|
+
if (getStartupMode() === 'menu') {
|
|
1261
|
+
this.restoredSessionInfo = msg;
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
this.ui.print({ type: 'info', message: msg });
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
catch {
|
|
1268
|
+
// Silently fail - session restoration is best-effort
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
else if (this.team && this.team.size > 1) {
|
|
1272
|
+
// 0-message session but team was restored
|
|
1273
|
+
const msg = `Team restored (${String(this.team.size)} agents).`;
|
|
1274
|
+
if (getStartupMode() === 'menu') {
|
|
1275
|
+
this.restoredSessionInfo = msg;
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
this.ui.print({ type: 'info', message: msg });
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* When this terminal has no session but other terminals do, show the resume
|
|
1284
|
+
* overlay so the user can pick one or start fresh.
|
|
1285
|
+
*/
|
|
1286
|
+
async offerOtherTerminalSessions(projectId) {
|
|
1287
|
+
if (!this.agent)
|
|
1288
|
+
return;
|
|
1289
|
+
const psm = getProjectSessionManager();
|
|
1290
|
+
const otherSessions = await psm.listProjectSessions(projectId);
|
|
1291
|
+
// Filter out sessions from active terminals (they're "in use")
|
|
1292
|
+
// but include 0-message sessions (they may have a team roster)
|
|
1293
|
+
const validSessions = otherSessions.filter(s => {
|
|
1294
|
+
// Exclude sessions that are actively in use by another terminal
|
|
1295
|
+
if (s.isActiveTerminal)
|
|
1296
|
+
return false;
|
|
1297
|
+
// Include sessions that have messages OR a team roster
|
|
1298
|
+
return s.messageCount > 0 || (s.teamAgentCount !== undefined && s.teamAgentCount > 1);
|
|
1299
|
+
});
|
|
1300
|
+
if (validSessions.length === 0)
|
|
1301
|
+
return;
|
|
1302
|
+
// Show info about available sessions
|
|
1303
|
+
const count = validSessions.length;
|
|
1304
|
+
this.ui.print({ type: 'info', message: `Found ${String(count)} previous session${count > 1 ? 's' : ''} for this project.` });
|
|
1305
|
+
// Show resume overlay
|
|
1306
|
+
const currentProject = getActiveProject();
|
|
1307
|
+
const overlay = new ResumeOverlayV2({
|
|
1308
|
+
sessions: validSessions,
|
|
1309
|
+
currentProjectId: currentProject?.id ?? null,
|
|
1310
|
+
onDelete: async (sessionId) => {
|
|
1311
|
+
// Inline delete handler (same logic as the /resume command)
|
|
1312
|
+
const parsed = this.parseCurrentSessionIdForStartup(sessionId);
|
|
1313
|
+
if (parsed?.terminalPrefix) {
|
|
1314
|
+
return psm.deleteSessionByPrefix(parsed.projectId, parsed.terminalPrefix);
|
|
1315
|
+
}
|
|
1316
|
+
if (parsed) {
|
|
1317
|
+
return psm.deleteCurrentSession(parsed.projectId);
|
|
1318
|
+
}
|
|
1319
|
+
return false;
|
|
1320
|
+
},
|
|
1321
|
+
});
|
|
1322
|
+
const result = await this.ui.showOverlay(overlay);
|
|
1323
|
+
if (result?.deleted) {
|
|
1324
|
+
this.ui.print({ type: 'success', message: 'Session deleted.' });
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (result?.sessionId) {
|
|
1328
|
+
const parsed = this.parseCurrentSessionIdForStartup(result.sessionId);
|
|
1329
|
+
if (!parsed)
|
|
1330
|
+
return;
|
|
1331
|
+
// Restore team roster if the selected session has a terminal prefix
|
|
1332
|
+
if (parsed.terminalPrefix) {
|
|
1333
|
+
this.restoreTeamForPrefix(parsed.projectId, parsed.terminalPrefix);
|
|
1334
|
+
}
|
|
1335
|
+
const selectedSession = parsed.terminalPrefix
|
|
1336
|
+
? await psm.loadProjectSessionByPrefix(parsed.projectId, parsed.terminalPrefix)
|
|
1337
|
+
: await psm.loadProjectSession(parsed.projectId);
|
|
1338
|
+
if (!selectedSession) {
|
|
1339
|
+
this.ui.print({ type: 'error', message: 'Failed to load session.' });
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
// Claim: rename files from old prefix to this terminal's prefix
|
|
1343
|
+
const ownPrefix = getActiveTerminalSessionId()?.slice(0, 8) ?? null;
|
|
1344
|
+
if (parsed.terminalPrefix && ownPrefix && parsed.terminalPrefix !== ownPrefix) {
|
|
1345
|
+
psm.claimSession(parsed.projectId, parsed.terminalPrefix, ownPrefix);
|
|
1346
|
+
getTeamCheckpointer().claimTeamFiles(parsed.projectId, parsed.terminalPrefix, ownPrefix);
|
|
1347
|
+
}
|
|
1348
|
+
// Restore conversation messages (if any)
|
|
1349
|
+
if (selectedSession.messages.length > 0) {
|
|
1350
|
+
try {
|
|
1351
|
+
const filteredMessages = stripThinkingBlocks(selectedSession.messages);
|
|
1352
|
+
await this.agent.setHistory(filteredMessages, { turnCount: selectedSession.turnCount });
|
|
1353
|
+
this.ui.setRestoredHistory(convertMessagesToItems(filteredMessages));
|
|
1354
|
+
}
|
|
1355
|
+
catch {
|
|
1356
|
+
this.ui.print({ type: 'error', message: 'Failed to restore session.' });
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
// Report what was restored
|
|
1361
|
+
const parts = [];
|
|
1362
|
+
if (selectedSession.messages.length > 0) {
|
|
1363
|
+
parts.push(`${String(selectedSession.messages.length)} messages`);
|
|
1364
|
+
}
|
|
1365
|
+
if (this.team && this.team.size > 1) {
|
|
1366
|
+
parts.push(`${String(this.team.size)} agents`);
|
|
1367
|
+
}
|
|
1368
|
+
if (parts.length > 0) {
|
|
1369
|
+
this.ui.print({ type: 'success', message: `Session restored (${parts.join(', ')}).` });
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
this.ui.print({ type: 'success', message: 'Session restored.' });
|
|
1373
|
+
}
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
// User cancelled — start fresh
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Parse session ID format for startup flow (duplicates session handler logic
|
|
1380
|
+
* to avoid circular import).
|
|
1381
|
+
*/
|
|
1382
|
+
parseCurrentSessionIdForStartup(sessionId) {
|
|
1383
|
+
if (!sessionId.startsWith('current-'))
|
|
1384
|
+
return null;
|
|
1385
|
+
const rest = sessionId.slice('current-'.length);
|
|
1386
|
+
const hexMatch = /^([a-f0-9]{8})-(.+)$/.exec(rest);
|
|
1387
|
+
if (hexMatch) {
|
|
1388
|
+
const projectIdStr = hexMatch[2];
|
|
1389
|
+
return { terminalPrefix: hexMatch[1], projectId: projectIdStr === 'global' ? null : parseInt(projectIdStr, 10) };
|
|
1390
|
+
}
|
|
1391
|
+
return { terminalPrefix: null, projectId: rest === 'global' ? null : parseInt(rest, 10) };
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Handle session mode prompt at startup when projectSessionMode='ask'.
|
|
1395
|
+
* Shows overlay to let user choose how to handle existing session.
|
|
1396
|
+
*/
|
|
1397
|
+
async handleSessionModePrompt() {
|
|
1398
|
+
// Get current project
|
|
1399
|
+
const activeProject = getActiveProject();
|
|
1400
|
+
const projectId = activeProject?.id ?? null;
|
|
1401
|
+
const projectName = activeProject?.displayName ?? 'Global';
|
|
1402
|
+
// Check if there's a session to load for THIS terminal
|
|
1403
|
+
const session = await loadProjectSession(projectId);
|
|
1404
|
+
if (!session) {
|
|
1405
|
+
// No own session — check other terminals
|
|
1406
|
+
await this.offerOtherTerminalSessions(projectId);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
// Restore team for own session prefix
|
|
1410
|
+
const ownPrefix = getActiveTerminalSessionId()?.slice(0, 8) ?? null;
|
|
1411
|
+
if (ownPrefix) {
|
|
1412
|
+
this.restoreTeamForPrefix(projectId, ownPrefix);
|
|
1413
|
+
}
|
|
1414
|
+
if (session.messages.length === 0) {
|
|
1415
|
+
// Session exists but no messages — team was already restored above
|
|
1416
|
+
if (this.team && this.team.size > 1) {
|
|
1417
|
+
this.ui.print({ type: 'info', message: `Team restored (${String(this.team.size)} agents).` });
|
|
1418
|
+
}
|
|
1419
|
+
// Also check other terminals for sessions to resume
|
|
1420
|
+
await this.offerOtherTerminalSessions(projectId);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
// Get last user message for preview
|
|
1424
|
+
let lastMessagePreview;
|
|
1425
|
+
const lastUserMsg = session.messages.filter(m => m.role === 'user').pop();
|
|
1426
|
+
if (lastUserMsg) {
|
|
1427
|
+
if (typeof lastUserMsg.content === 'string') {
|
|
1428
|
+
lastMessagePreview = lastUserMsg.content;
|
|
1429
|
+
}
|
|
1430
|
+
else if (Array.isArray(lastUserMsg.content)) {
|
|
1431
|
+
const textBlock = lastUserMsg.content.find((b) => b.type === 'text');
|
|
1432
|
+
if (textBlock) {
|
|
1433
|
+
lastMessagePreview = textBlock.text;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
// Show the session mode overlay
|
|
1438
|
+
const overlay = new SessionModeOverlayV2({
|
|
1439
|
+
projectName,
|
|
1440
|
+
messageCount: session.messages.length,
|
|
1441
|
+
lastActivity: new Date(session.updatedAt),
|
|
1442
|
+
lastMessagePreview,
|
|
221
1443
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
1444
|
+
const result = await this.ui.showOverlay(overlay);
|
|
1445
|
+
if (!result) {
|
|
1446
|
+
// Cancelled - default to resume-full
|
|
1447
|
+
return this.handleSessionModeResult('resume-full', session);
|
|
1448
|
+
}
|
|
1449
|
+
await this.handleSessionModeResult(result.option, session);
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Apply the user's session mode choice.
|
|
1453
|
+
*/
|
|
1454
|
+
async handleSessionModeResult(option, session) {
|
|
1455
|
+
if (!session)
|
|
1456
|
+
return;
|
|
1457
|
+
switch (option) {
|
|
1458
|
+
case 'resume-full':
|
|
1459
|
+
// Load the full session
|
|
1460
|
+
if (this.agent) {
|
|
1461
|
+
try {
|
|
1462
|
+
const filteredFull = stripThinkingBlocks(session.messages);
|
|
1463
|
+
await this.agent.setHistory(filteredFull, { turnCount: session.turnCount });
|
|
1464
|
+
this.ui.setRestoredHistory(convertMessagesToItems(filteredFull));
|
|
1465
|
+
this.ui.print({ type: 'success', message: `Session restored (${String(session.messages.length)} messages).` });
|
|
1466
|
+
this.ui.print({ type: 'info', message: 'Tip: Use Ctrl+R to view conversation history.' });
|
|
1467
|
+
}
|
|
1468
|
+
catch {
|
|
1469
|
+
this.ui.print({ type: 'error', message: 'Failed to restore session.' });
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
break;
|
|
1473
|
+
case 'resume-messages':
|
|
1474
|
+
// Load messages only (context refresh is deferred - same as resume-full for now)
|
|
1475
|
+
if (this.agent) {
|
|
1476
|
+
try {
|
|
1477
|
+
const filteredMsgs = stripThinkingBlocks(session.messages);
|
|
1478
|
+
await this.agent.setHistory(filteredMsgs, { turnCount: session.turnCount });
|
|
1479
|
+
this.ui.setRestoredHistory(convertMessagesToItems(filteredMsgs));
|
|
1480
|
+
this.ui.print({ type: 'success', message: `Messages restored (${String(session.messages.length)} messages).` });
|
|
1481
|
+
this.ui.print({ type: 'info', message: 'Tip: Use Ctrl+R to view conversation history.' });
|
|
1482
|
+
}
|
|
1483
|
+
catch {
|
|
1484
|
+
this.ui.print({ type: 'error', message: 'Failed to restore session.' });
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
break;
|
|
1488
|
+
case 'start-fresh':
|
|
1489
|
+
// Archive old session and start fresh
|
|
1490
|
+
try {
|
|
1491
|
+
await archiveCurrentSession();
|
|
1492
|
+
this.ui.print({ type: 'info', message: 'Previous session archived. Starting fresh.' });
|
|
1493
|
+
}
|
|
1494
|
+
catch {
|
|
1495
|
+
// Silent fail - archiving is best-effort
|
|
1496
|
+
this.ui.print({ type: 'info', message: 'Starting fresh.' });
|
|
1497
|
+
}
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Show the onboarding wizard for first-time users.
|
|
1503
|
+
* Called on first run, or manually via /init command.
|
|
1504
|
+
*/
|
|
1505
|
+
async showOnboardingWizard() {
|
|
1506
|
+
const wizard = new OnboardingWizardOverlayV2();
|
|
1507
|
+
const result = await this.ui.showOverlay(wizard);
|
|
1508
|
+
// Handle result
|
|
1509
|
+
if (result?.openKeys) {
|
|
1510
|
+
// User chose to configure keys - show keys overlay
|
|
1511
|
+
const keysOverlay = new KeysOverlayV2();
|
|
1512
|
+
await this.ui.showOverlay(keysOverlay);
|
|
1513
|
+
// After keys, re-show the onboarding wizard to continue
|
|
1514
|
+
await this.showOnboardingWizard();
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
// Start session heartbeat in background (don't block UI)
|
|
1518
|
+
const auth = getAuthManager();
|
|
1519
|
+
void auth.startSession();
|
|
225
1520
|
const startupMode = getStartupMode();
|
|
226
1521
|
if (startupMode === 'menu') {
|
|
227
|
-
|
|
1522
|
+
await this.handleCommand('menu', '');
|
|
228
1523
|
}
|
|
229
1524
|
}
|
|
230
1525
|
/**
|
|
231
1526
|
* Stop the REPL.
|
|
1527
|
+
* Aborts all running sessions (foreground and background).
|
|
232
1528
|
*/
|
|
1529
|
+
/** Set MCP loading state on the footer status bar. */
|
|
1530
|
+
setMCPLoading(loading, toolCount) {
|
|
1531
|
+
this.ui.setMCPLoading(loading, toolCount);
|
|
1532
|
+
}
|
|
233
1533
|
stop() {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
1534
|
+
// Stop notification polling before unregistering session
|
|
1535
|
+
try {
|
|
1536
|
+
const notifMgr = getNotificationManager();
|
|
1537
|
+
if (notifMgr) {
|
|
1538
|
+
notifMgr.stopPolling();
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
catch {
|
|
1542
|
+
// Best effort
|
|
237
1543
|
}
|
|
1544
|
+
// Unregister from session registry
|
|
1545
|
+
try {
|
|
1546
|
+
const registry = getSessionRegistry();
|
|
1547
|
+
if (registry) {
|
|
1548
|
+
registry.unregister();
|
|
1549
|
+
}
|
|
1550
|
+
setActiveTerminalSessionId(null);
|
|
1551
|
+
}
|
|
1552
|
+
catch {
|
|
1553
|
+
// Best effort
|
|
1554
|
+
}
|
|
1555
|
+
// Abort all sessions
|
|
1556
|
+
for (const [agentId, session] of this.sessions) {
|
|
1557
|
+
session.abortController.abort();
|
|
1558
|
+
this.sessions.delete(agentId);
|
|
1559
|
+
}
|
|
1560
|
+
// Clear all message queues (don't process on shutdown)
|
|
1561
|
+
this.agentMessageQueues.clear();
|
|
1562
|
+
this.currentSession = null;
|
|
1563
|
+
this.foregroundAgentId = null;
|
|
238
1564
|
this.ui.stop();
|
|
239
1565
|
}
|
|
240
1566
|
// ===========================================================================
|
|
@@ -248,8 +1574,13 @@ export class ReplV2 {
|
|
|
248
1574
|
const settings = getSettings();
|
|
249
1575
|
const s = getStyles();
|
|
250
1576
|
terminal.clearScreen();
|
|
251
|
-
//
|
|
252
|
-
const
|
|
1577
|
+
// Show "What's New" highlights only when version has changed since last seen
|
|
1578
|
+
const isNewVersion = settings.lastSeenVersion !== this.version;
|
|
1579
|
+
const highlights = isNewVersion ? getStartupHighlights(this.version) : undefined;
|
|
1580
|
+
const logoLines = renderMascotWithLogo(settings.mascot, this.version, null, highlights);
|
|
1581
|
+
if (isNewVersion) {
|
|
1582
|
+
updateSettings({ lastSeenVersion: this.version });
|
|
1583
|
+
}
|
|
253
1584
|
console.log('');
|
|
254
1585
|
for (const line of logoLines) {
|
|
255
1586
|
console.log(line);
|
|
@@ -275,6 +1606,43 @@ export class ReplV2 {
|
|
|
275
1606
|
const s = getStyles();
|
|
276
1607
|
// Print logo with agent/model info
|
|
277
1608
|
this.printLogo();
|
|
1609
|
+
// Show update notification if available
|
|
1610
|
+
if (this.updateAvailable) {
|
|
1611
|
+
console.log(s.warning(` ⬆ Update available: v${this.updateAvailable.current} → v${this.updateAvailable.latest}`));
|
|
1612
|
+
console.log(s.muted(' Run: npm update -g @compilr-dev/cli'));
|
|
1613
|
+
console.log('');
|
|
1614
|
+
}
|
|
1615
|
+
// Show session hint if recent session exists (< 24h)
|
|
1616
|
+
// Uses fast O(1) stat check instead of scanning all session directories
|
|
1617
|
+
try {
|
|
1618
|
+
const activeProject = getActiveProject();
|
|
1619
|
+
const psm = getProjectSessionManager();
|
|
1620
|
+
if (psm.hasRecentSessionFast(activeProject?.id ?? null)) {
|
|
1621
|
+
console.log(s.info('Tip: Recent session available. Use /resume to continue.'));
|
|
1622
|
+
console.log('');
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
catch {
|
|
1626
|
+
// Silently ignore errors checking for sessions
|
|
1627
|
+
}
|
|
1628
|
+
// Show in_progress plans notification
|
|
1629
|
+
try {
|
|
1630
|
+
const activeProject = getActiveProject();
|
|
1631
|
+
if (activeProject) {
|
|
1632
|
+
const inProgressPlans = planRepository.getInProgress(activeProject.id);
|
|
1633
|
+
if (inProgressPlans.length === 1) {
|
|
1634
|
+
console.log(s.info(`Tip: Plan "${inProgressPlans[0].name}" is in progress. Use /docs or Plan Mode to continue.`));
|
|
1635
|
+
console.log('');
|
|
1636
|
+
}
|
|
1637
|
+
else if (inProgressPlans.length > 1) {
|
|
1638
|
+
console.log(s.info(`Tip: You have ${String(inProgressPlans.length)} plan(s) in progress. Use /docs to view.`));
|
|
1639
|
+
console.log('');
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
catch {
|
|
1644
|
+
// Silently ignore errors checking for plans
|
|
1645
|
+
}
|
|
278
1646
|
// Hints
|
|
279
1647
|
console.log(s.muted('Type a message to start'));
|
|
280
1648
|
console.log(s.muted('Type /help for commands, /exit to quit'));
|
|
@@ -287,10 +1655,36 @@ export class ReplV2 {
|
|
|
287
1655
|
return {
|
|
288
1656
|
ui: this.ui,
|
|
289
1657
|
version: this.version,
|
|
290
|
-
|
|
1658
|
+
updateAvailable: this.updateAvailable,
|
|
1659
|
+
printWelcome: () => { this.printWelcome(); return Promise.resolve(); },
|
|
291
1660
|
printLogo: () => { this.printLogo(); },
|
|
1661
|
+
consumeRestoredSessionInfo: () => {
|
|
1662
|
+
const info = this.restoredSessionInfo;
|
|
1663
|
+
this.restoredSessionInfo = null;
|
|
1664
|
+
return info;
|
|
1665
|
+
},
|
|
292
1666
|
// Agent integration
|
|
293
1667
|
agent: this.agent,
|
|
1668
|
+
team: this.team,
|
|
1669
|
+
agentFactory: this.agentFactory,
|
|
1670
|
+
setTeam: (newTeam) => {
|
|
1671
|
+
const oldTeam = this.team;
|
|
1672
|
+
this.team = newTeam;
|
|
1673
|
+
this.onTeamReplaced?.(newTeam);
|
|
1674
|
+
// Update agent suggestions callback with new team
|
|
1675
|
+
this.ui.setAgentSuggestionsCallback(() => {
|
|
1676
|
+
const agents = newTeam.getAll();
|
|
1677
|
+
return agents.map((agent) => ({
|
|
1678
|
+
id: agent.id,
|
|
1679
|
+
displayName: agent.displayName,
|
|
1680
|
+
mascot: agent.mascot,
|
|
1681
|
+
}));
|
|
1682
|
+
});
|
|
1683
|
+
return oldTeam;
|
|
1684
|
+
},
|
|
1685
|
+
restoreAgent: (newAgent) => {
|
|
1686
|
+
this.restoreAgent(newAgent);
|
|
1687
|
+
},
|
|
294
1688
|
model: this.model,
|
|
295
1689
|
provider: this.provider,
|
|
296
1690
|
getContextManager: () => {
|
|
@@ -308,9 +1702,30 @@ export class ReplV2 {
|
|
|
308
1702
|
sessionInputTokens: this.sessionInputTokens,
|
|
309
1703
|
sessionOutputTokens: this.sessionOutputTokens,
|
|
310
1704
|
sessionRequests: this.sessionRequests,
|
|
1705
|
+
// Background session management (Phase 3c)
|
|
1706
|
+
getBackgroundSessions: () => {
|
|
1707
|
+
return this.getBackgroundSessionsInfo();
|
|
1708
|
+
},
|
|
1709
|
+
bringToForeground: async (agentId) => {
|
|
1710
|
+
return this.bringAgentToForeground(agentId);
|
|
1711
|
+
},
|
|
1712
|
+
switchAgent: async (agentId) => {
|
|
1713
|
+
await this.switchAgent(agentId);
|
|
1714
|
+
},
|
|
1715
|
+
killBackgroundAgent: (agentId) => {
|
|
1716
|
+
return this.killAgent(agentId);
|
|
1717
|
+
},
|
|
1718
|
+
sendForegroundToBackground: () => {
|
|
1719
|
+
return this.sendForegroundToBackground();
|
|
1720
|
+
},
|
|
1721
|
+
clearAllQueues: () => {
|
|
1722
|
+
return this.clearAllQueues();
|
|
1723
|
+
},
|
|
311
1724
|
};
|
|
312
1725
|
}
|
|
313
1726
|
async handleCommand(cmd, args) {
|
|
1727
|
+
// Record command usage for telemetry
|
|
1728
|
+
getAuthManager().recordCommand(cmd);
|
|
314
1729
|
const ctx = this.createCommandContext();
|
|
315
1730
|
const result = await executeCommand(cmd, args, ctx);
|
|
316
1731
|
if (result === null) {
|
|
@@ -337,7 +1752,8 @@ export class ReplV2 {
|
|
|
337
1752
|
this.ui.print({ type: 'info', message: 'Type /help to see available commands' });
|
|
338
1753
|
}
|
|
339
1754
|
else if (!result) {
|
|
340
|
-
// result === false means exit
|
|
1755
|
+
// result === false means exit — clean up before exiting
|
|
1756
|
+
this.stop();
|
|
341
1757
|
process.exit(0);
|
|
342
1758
|
}
|
|
343
1759
|
// Process any queued agent messages (from commands like /arch, /design)
|
|
@@ -366,33 +1782,285 @@ export class ReplV2 {
|
|
|
366
1782
|
}
|
|
367
1783
|
}
|
|
368
1784
|
}
|
|
369
|
-
// ===========================================================================
|
|
370
|
-
// Input Processing
|
|
371
|
-
// ===========================================================================
|
|
372
|
-
async processInput(input) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
1785
|
+
// ===========================================================================
|
|
1786
|
+
// Input Processing
|
|
1787
|
+
// ===========================================================================
|
|
1788
|
+
async processInput(input) {
|
|
1789
|
+
// Ignore empty input
|
|
1790
|
+
if (!input.trim()) {
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
// Auth-only mode: restrict to /login, /logout, /exit, /help
|
|
1794
|
+
if (this.authOnly) {
|
|
1795
|
+
const allowedCommands = ['login', 'logout', 'exit', 'help'];
|
|
1796
|
+
if (input.startsWith('/')) {
|
|
1797
|
+
const spaceIndex = input.indexOf(' ');
|
|
1798
|
+
const cmd = spaceIndex > 0 ? input.slice(1, spaceIndex) : input.slice(1);
|
|
1799
|
+
if (allowedCommands.includes(cmd.toLowerCase())) {
|
|
1800
|
+
const args = spaceIndex > 0 ? input.slice(spaceIndex + 1) : '';
|
|
1801
|
+
await this.handleCommand(cmd, args);
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
// Block all other input
|
|
1806
|
+
this.ui.print({ type: 'warning', message: 'Please log in first to use the CLI.' });
|
|
1807
|
+
this.ui.print({ type: 'info', message: 'Use /login to authenticate or /exit to quit.' });
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
// Handle $name prefix (agent switching)
|
|
1811
|
+
// e.g., "$arch Design the API" → switch to arch, send message
|
|
1812
|
+
// e.g., "$arch Design the API &" → switch to arch, run in background
|
|
1813
|
+
// e.g., "$pm /backlog" → switch to pm, run /backlog command
|
|
1814
|
+
// e.g., "$arch" → just switch to arch (no message sent)
|
|
1815
|
+
if (input.startsWith('$') && this.team) {
|
|
1816
|
+
const parsed = await this.parseAgentPrefix(input);
|
|
1817
|
+
if (parsed) {
|
|
1818
|
+
// Only process remaining input if there is one
|
|
1819
|
+
if (parsed.remainingInput.trim()) {
|
|
1820
|
+
if (parsed.isBackground) {
|
|
1821
|
+
// Background execution - fire and forget
|
|
1822
|
+
this.processInputBackground(parsed.agentId, parsed.remainingInput);
|
|
1823
|
+
}
|
|
1824
|
+
else {
|
|
1825
|
+
// Foreground execution - await
|
|
1826
|
+
await this.processInput(parsed.remainingInput);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
// Otherwise just switch was done, nothing more to do
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
// If parsing failed (unknown agent), fall through to treat as regular input
|
|
1833
|
+
}
|
|
1834
|
+
if (input.startsWith('/')) {
|
|
1835
|
+
const spaceIndex = input.indexOf(' ');
|
|
1836
|
+
const cmd = spaceIndex > 0 ? input.slice(1, spaceIndex) : input.slice(1);
|
|
1837
|
+
const args = spaceIndex > 0 ? input.slice(spaceIndex + 1) : '';
|
|
1838
|
+
await this.handleCommand(cmd, args);
|
|
1839
|
+
await this.processQueue();
|
|
1840
|
+
}
|
|
1841
|
+
else if (input.startsWith('!')) {
|
|
1842
|
+
// Handle bash command ("!ls", "!git status", etc.)
|
|
1843
|
+
await this.handleBashCommand(input.slice(1).trim());
|
|
1844
|
+
await this.processQueue();
|
|
1845
|
+
}
|
|
1846
|
+
else if (isMemoryInput(input)) {
|
|
1847
|
+
// Handle memory note ("# note")
|
|
1848
|
+
this.handleMemoryInputV2(input);
|
|
1849
|
+
}
|
|
1850
|
+
else {
|
|
1851
|
+
// Check for & suffix (background execution with current agent)
|
|
1852
|
+
const isBackground = input.trim().endsWith('&');
|
|
1853
|
+
const cleanInput = isBackground ? input.slice(0, -1).trim() : input;
|
|
1854
|
+
if (isBackground && this.team && this.agent) {
|
|
1855
|
+
// Background execution with current active agent
|
|
1856
|
+
const activeAgent = this.team.getActive();
|
|
1857
|
+
// Auto-expand filter if user is sending to agent not in filter
|
|
1858
|
+
this.autoExpandFilterIfNeeded(activeAgent.id);
|
|
1859
|
+
this.ui.print({ type: 'user-message', text: `${cleanInput} [background: $${activeAgent.id}]`, agentId: activeAgent.id });
|
|
1860
|
+
const messageToSend = this.resolveAndInjectMentions(cleanInput);
|
|
1861
|
+
const label = this.getAgentMascotLabel(activeAgent.id);
|
|
1862
|
+
this.ui.print({
|
|
1863
|
+
type: 'info',
|
|
1864
|
+
message: `${label} Started in background`,
|
|
1865
|
+
});
|
|
1866
|
+
void this.runAgentBackground(activeAgent.id, messageToSend);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
// Get active agent ID for tracking
|
|
1870
|
+
const activeAgentId = this.team?.getActive().id ?? 'default';
|
|
1871
|
+
// Auto-expand filter if user is sending to agent not in filter
|
|
1872
|
+
this.autoExpandFilterIfNeeded(activeAgentId);
|
|
1873
|
+
// Print user message
|
|
1874
|
+
this.ui.print({ type: 'user-message', text: cleanInput, agentId: activeAgentId });
|
|
1875
|
+
// Resolve $agent mention references and inject context
|
|
1876
|
+
const messageToSend = this.resolveAndInjectMentions(cleanInput);
|
|
1877
|
+
// Check if the current agent has an active session (still running)
|
|
1878
|
+
// If so, queue the message and return immediately (non-blocking)
|
|
1879
|
+
const existingSession = this.sessions.get(activeAgentId);
|
|
1880
|
+
if (existingSession) {
|
|
1881
|
+
const label = this.getAgentMascotLabel(activeAgentId);
|
|
1882
|
+
const position = this.queueMessage(activeAgentId, messageToSend, cleanInput);
|
|
1883
|
+
if (position === null) {
|
|
1884
|
+
// Queue is full
|
|
1885
|
+
this.ui.print({
|
|
1886
|
+
type: 'warning',
|
|
1887
|
+
message: `${label} queue is full (${String(MAX_QUEUE_SIZE)} messages). Wait for some to complete.`,
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
else {
|
|
1891
|
+
// Message queued successfully
|
|
1892
|
+
const queueInfo = position === 1 ? 'next in queue' : `position ${String(position)} in queue`;
|
|
1893
|
+
this.ui.print({
|
|
1894
|
+
type: 'info',
|
|
1895
|
+
message: `${label} is busy, message queued (${queueInfo})`,
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
return; // Return immediately - user can switch agents or continue
|
|
1899
|
+
}
|
|
1900
|
+
// Run agent (real or simulation)
|
|
1901
|
+
if (this.agent) {
|
|
1902
|
+
await this.runAgentReal(messageToSend);
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
await this.runAgentSimulation(messageToSend);
|
|
1906
|
+
}
|
|
1907
|
+
await this.processQueue();
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Process input in background mode.
|
|
1912
|
+
* Starts the agent without blocking, allowing user to continue with foreground work.
|
|
1913
|
+
*
|
|
1914
|
+
* @param agentId - The agent to run in background
|
|
1915
|
+
* @param input - The message to send to the agent
|
|
1916
|
+
*/
|
|
1917
|
+
processInputBackground(agentId, input) {
|
|
1918
|
+
// Check if agent already has a running session
|
|
1919
|
+
if (this.hasActiveSession(agentId)) {
|
|
1920
|
+
this.ui.print({
|
|
1921
|
+
type: 'error',
|
|
1922
|
+
message: `Agent '${agentId}' is already running. Use /kill $${agentId} to stop it first.`,
|
|
1923
|
+
});
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
// Get mascot label for the agent
|
|
1927
|
+
const label = this.getAgentMascotLabel(agentId);
|
|
1928
|
+
// Auto-expand filter if user is sending to agent not in filter
|
|
1929
|
+
this.autoExpandFilterIfNeeded(agentId);
|
|
1930
|
+
// Print user message with background indicator
|
|
1931
|
+
this.ui.print({ type: 'user-message', text: `${input} [background: $${agentId}]`, agentId });
|
|
1932
|
+
// Resolve $agent mention references and inject context
|
|
1933
|
+
const messageToSend = this.resolveAndInjectMentions(input);
|
|
1934
|
+
// Show that background agent is starting
|
|
1935
|
+
this.ui.print({
|
|
1936
|
+
type: 'info',
|
|
1937
|
+
message: `${label} Started in background`,
|
|
1938
|
+
});
|
|
1939
|
+
// Start agent in background (don't await)
|
|
1940
|
+
if (this.agent) {
|
|
1941
|
+
void this.runAgentBackground(agentId, messageToSend);
|
|
1942
|
+
}
|
|
1943
|
+
else {
|
|
1944
|
+
// Simulation mode - just show a message
|
|
1945
|
+
this.ui.print({
|
|
1946
|
+
type: 'info',
|
|
1947
|
+
message: `${label} [simulation] Would run: "${messageToSend}"`,
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Parse $name prefix from input.
|
|
1953
|
+
* Returns the agent ID, remaining input, and whether this is a background request.
|
|
1954
|
+
* Returns null if the agent doesn't exist.
|
|
1955
|
+
*
|
|
1956
|
+
* Examples:
|
|
1957
|
+
* "$arch Design the API" → { agentId: 'arch', remainingInput: 'Design the API', isBackground: false }
|
|
1958
|
+
* "$arch Design the API &" → { agentId: 'arch', remainingInput: 'Design the API', isBackground: true }
|
|
1959
|
+
* "$pm /backlog" → { agentId: 'pm', remainingInput: '/backlog', isBackground: false }
|
|
1960
|
+
* "$arch" → { agentId: 'arch', remainingInput: '', isBackground: false }
|
|
1961
|
+
*/
|
|
1962
|
+
async parseAgentPrefix(input) {
|
|
1963
|
+
if (!this.team || !input.startsWith('$')) {
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
// Check for background flag (& at end of input)
|
|
1967
|
+
const isBackground = input.trim().endsWith('&');
|
|
1968
|
+
const cleanInput = isBackground ? input.slice(0, -1).trim() : input;
|
|
1969
|
+
// Extract agent ID: everything after $ until space or end
|
|
1970
|
+
// Note: Agent IDs are case-insensitive (stored lowercase by /team command)
|
|
1971
|
+
const spaceIndex = cleanInput.indexOf(' ');
|
|
1972
|
+
const agentId = (spaceIndex > 0 ? cleanInput.slice(1, spaceIndex) : cleanInput.slice(1)).toLowerCase();
|
|
1973
|
+
const remainingInput = spaceIndex > 0 ? cleanInput.slice(spaceIndex + 1).trim() : '';
|
|
1974
|
+
// Check if agent exists in team
|
|
1975
|
+
if (!this.team.has(agentId)) {
|
|
1976
|
+
this.ui.print({
|
|
1977
|
+
type: 'error',
|
|
1978
|
+
message: `Agent '${agentId}' not found in team`,
|
|
1979
|
+
});
|
|
1980
|
+
this.ui.print({
|
|
1981
|
+
type: 'info',
|
|
1982
|
+
message: `Available agents: ${this.team.getAgentIds().join(', ')}`,
|
|
1983
|
+
});
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
// Switch to the agent (consolidated logic)
|
|
1987
|
+
try {
|
|
1988
|
+
const { teamAgent, demotedAgentId } = await this.switchAgent(agentId);
|
|
1989
|
+
if (demotedAgentId) {
|
|
1990
|
+
const demotedLabel = this.getAgentMascotLabel(demotedAgentId);
|
|
1991
|
+
this.ui.print({
|
|
1992
|
+
type: 'info',
|
|
1993
|
+
message: `${demotedLabel} sent to background (still running)`,
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
this.ui.print({
|
|
1997
|
+
type: 'info',
|
|
1998
|
+
message: `Switched to ${teamAgent.mascot} ${teamAgent.displayName}`,
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
catch (err) {
|
|
2002
|
+
this.ui.print({
|
|
2003
|
+
type: 'error',
|
|
2004
|
+
message: `Failed to switch to '${agentId}': ${err instanceof Error ? err.message : 'Unknown error'}`,
|
|
2005
|
+
});
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
return { agentId, remainingInput, isBackground };
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Resolve $agent mention references in input and inject context.
|
|
2012
|
+
*
|
|
2013
|
+
* This handles mid-sentence mentions like:
|
|
2014
|
+
* "What did $arch propose for the API?"
|
|
2015
|
+
* "Based on $pm's plan, estimate the timeline"
|
|
2016
|
+
*
|
|
2017
|
+
* The resolved context is prepended to the message before sending to the agent.
|
|
2018
|
+
*/
|
|
2019
|
+
resolveAndInjectMentions(input) {
|
|
2020
|
+
// Skip if no team
|
|
2021
|
+
if (!this.team) {
|
|
2022
|
+
return input;
|
|
379
2023
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
2024
|
+
// Get valid agent IDs
|
|
2025
|
+
const validAgents = new Set(this.team.getAgentIds());
|
|
2026
|
+
// Parse input for mentions
|
|
2027
|
+
const parsed = parseInputForMentions(input, validAgents);
|
|
2028
|
+
// If no references (just a switch or no mentions), return as-is
|
|
2029
|
+
if (!hasReferences(parsed)) {
|
|
2030
|
+
return input;
|
|
383
2031
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
2032
|
+
// Get artifact store for context resolution
|
|
2033
|
+
const currentProject = getCurrentProject();
|
|
2034
|
+
const projectId = currentProject?.id ?? null;
|
|
2035
|
+
const checkpointer = getTeamCheckpointer();
|
|
2036
|
+
const artifactStore = checkpointer.getArtifactStore(projectId);
|
|
2037
|
+
// Create context resolver
|
|
2038
|
+
const resolver = new ContextResolver(this.team, artifactStore);
|
|
2039
|
+
// Resolve all mentions
|
|
2040
|
+
const resolved = resolver.resolveAll(parsed.references);
|
|
2041
|
+
// Check if any were resolved
|
|
2042
|
+
let anyResolved = false;
|
|
2043
|
+
for (const r of resolved.values()) {
|
|
2044
|
+
if (r.resolved) {
|
|
2045
|
+
anyResolved = true;
|
|
2046
|
+
break;
|
|
390
2047
|
}
|
|
391
|
-
else {
|
|
392
|
-
await this.runAgentSimulation(input);
|
|
393
|
-
}
|
|
394
|
-
await this.processQueue();
|
|
395
2048
|
}
|
|
2049
|
+
if (!anyResolved) {
|
|
2050
|
+
return input;
|
|
2051
|
+
}
|
|
2052
|
+
// Build context map and inject into message
|
|
2053
|
+
const contextMap = buildContextMap(resolved);
|
|
2054
|
+
const enhancedMessage = buildMessageWithContext(parsed, contextMap);
|
|
2055
|
+
// Log that context was injected (for debugging)
|
|
2056
|
+
const resolvedAgents = Array.from(resolved.entries())
|
|
2057
|
+
.filter(([_, r]) => r.resolved)
|
|
2058
|
+
.map(([id, r]) => `$${id} (${r.source}: ${r.sourceName})`);
|
|
2059
|
+
this.ui.print({
|
|
2060
|
+
type: 'info',
|
|
2061
|
+
message: `Injected context from: ${resolvedAgents.join(', ')}`,
|
|
2062
|
+
});
|
|
2063
|
+
return enhancedMessage;
|
|
396
2064
|
}
|
|
397
2065
|
/**
|
|
398
2066
|
* Handle memory note input ("# note") - V2 compatible version
|
|
@@ -422,7 +2090,7 @@ export class ReplV2 {
|
|
|
422
2090
|
// Show confirmation
|
|
423
2091
|
const scope = activeProject ? `project "${activeProject.displayName || activeProject.name}"` : 'global';
|
|
424
2092
|
const priorityLabel = priority === 'info' ? '' : ` (${priority})`;
|
|
425
|
-
const truncatedNote = note
|
|
2093
|
+
const truncatedNote = truncate(note, 50);
|
|
426
2094
|
this.ui.print({ type: 'success', message: `Saved${priorityLabel}: "${truncatedNote}" → ${scope}` });
|
|
427
2095
|
// Also add to agent's anchor manager if enabled (for current session)
|
|
428
2096
|
if (this.agent?.getAnchorManager()) {
|
|
@@ -435,6 +2103,67 @@ export class ReplV2 {
|
|
|
435
2103
|
});
|
|
436
2104
|
}
|
|
437
2105
|
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Handle bash command ("!ls", "!git status", etc.)
|
|
2108
|
+
* Runs the command directly and displays output.
|
|
2109
|
+
* Special handling for 'cd' to change the CLI's working directory.
|
|
2110
|
+
*/
|
|
2111
|
+
async handleBashCommand(command) {
|
|
2112
|
+
if (!command) {
|
|
2113
|
+
this.ui.print({ type: 'error', message: 'Usage: !<command> (e.g., !ls, !git status, !cd <path>)' });
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
// Special handling for 'cd' command - use process.chdir() instead of subprocess
|
|
2117
|
+
// This allows the CLI's working directory to actually change
|
|
2118
|
+
const cdMatch = command.match(/^cd\s+(.+)$/);
|
|
2119
|
+
if (cdMatch || command === 'cd') {
|
|
2120
|
+
const targetDir = cdMatch ? cdMatch[1].trim() : process.env.HOME || '/';
|
|
2121
|
+
// Remove quotes if present (e.g., cd "path with spaces")
|
|
2122
|
+
const cleanDir = targetDir.replace(/^["']|["']$/g, '');
|
|
2123
|
+
try {
|
|
2124
|
+
const path = await import('path');
|
|
2125
|
+
const resolvedPath = path.resolve(process.cwd(), cleanDir);
|
|
2126
|
+
process.chdir(resolvedPath);
|
|
2127
|
+
this.ui.print({ type: 'info', message: `$ cd ${cleanDir}` });
|
|
2128
|
+
this.ui.print({ type: 'success', message: process.cwd() });
|
|
2129
|
+
}
|
|
2130
|
+
catch (error) {
|
|
2131
|
+
this.ui.print({ type: 'info', message: `$ cd ${cleanDir}` });
|
|
2132
|
+
const err = error;
|
|
2133
|
+
this.ui.print({ type: 'error', message: err.message || `Cannot change to directory: ${cleanDir}` });
|
|
2134
|
+
}
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
// Show the command being run
|
|
2138
|
+
this.ui.print({ type: 'info', message: `$ ${command}` });
|
|
2139
|
+
try {
|
|
2140
|
+
const { execSync } = await import('child_process');
|
|
2141
|
+
const output = execSync(command, {
|
|
2142
|
+
encoding: 'utf8',
|
|
2143
|
+
cwd: process.cwd(),
|
|
2144
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
2145
|
+
timeout: 30000, // 30 second timeout
|
|
2146
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
2147
|
+
});
|
|
2148
|
+
if (output.trim()) {
|
|
2149
|
+
// Print output using raw type (no formatting, proper footer handling)
|
|
2150
|
+
this.ui.print({ type: 'raw', text: output.trimEnd() });
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
catch (error) {
|
|
2154
|
+
const err = error;
|
|
2155
|
+
if (err.stderr) {
|
|
2156
|
+
// Print stderr using raw type
|
|
2157
|
+
this.ui.print({ type: 'raw', text: err.stderr.trimEnd() });
|
|
2158
|
+
}
|
|
2159
|
+
else if (err.message) {
|
|
2160
|
+
this.ui.print({ type: 'error', message: err.message });
|
|
2161
|
+
}
|
|
2162
|
+
if (err.status !== undefined) {
|
|
2163
|
+
this.ui.print({ type: 'info', message: `Exit code: ${String(err.status)}` });
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
438
2167
|
async processQueue() {
|
|
439
2168
|
while (this.ui.hasQueuedInput()) {
|
|
440
2169
|
const queued = this.ui.popQueuedInput();
|
|
@@ -461,24 +2190,71 @@ export class ReplV2 {
|
|
|
461
2190
|
this.clearSubagentTracking?.();
|
|
462
2191
|
// - LiveRegion entries (visual display of running tools)
|
|
463
2192
|
this.ui.clearLiveRegion();
|
|
464
|
-
|
|
2193
|
+
// Get active agent ID for session tracking
|
|
2194
|
+
const activeAgentId = this.team?.getActive().id ?? 'default';
|
|
2195
|
+
// Create session object
|
|
2196
|
+
const session = {
|
|
2197
|
+
agentId: activeAgentId,
|
|
2198
|
+
isBackground: false, // Foreground execution
|
|
465
2199
|
abortController,
|
|
466
2200
|
currentAction: null,
|
|
467
2201
|
promise: (async () => {
|
|
468
2202
|
try {
|
|
469
2203
|
this.ui.setAgentRunning(true);
|
|
2204
|
+
this.ui.resetTurnMetrics(); // Reset token tracking for this turn
|
|
470
2205
|
setAction('Thinking...');
|
|
471
2206
|
this.ui.setSpinnerText('Thinking...');
|
|
472
2207
|
// Reset text accumulator for this run (class-level so it can be flushed externally)
|
|
473
2208
|
this.textAccumulator = '';
|
|
474
2209
|
let lastToolInput = null;
|
|
2210
|
+
let pendingDiffLines;
|
|
475
2211
|
// Stream agent events (agent is guaranteed to exist in this method)
|
|
476
2212
|
const agent = this.agent;
|
|
477
2213
|
if (!agent)
|
|
478
2214
|
return; // Type guard
|
|
479
|
-
|
|
2215
|
+
// Update history token estimate for breakdown display
|
|
2216
|
+
const historyMessages = agent.getHistory();
|
|
2217
|
+
const historyJson = JSON.stringify(historyMessages);
|
|
2218
|
+
updateHistoryEstimate(estimateTokens(historyJson));
|
|
2219
|
+
// In plan mode, prepend plan mode instructions to the first message
|
|
2220
|
+
// This dynamically injects the plan mode context without recreating the agent
|
|
2221
|
+
let messageToSend = userMessage;
|
|
2222
|
+
if (this.ui.getMode() === 'plan') {
|
|
2223
|
+
const activePlan = this.ui.getActivePlan();
|
|
2224
|
+
const planModePrompt = getPlanModePrompt(activePlan.id ?? undefined, activePlan.name ?? undefined);
|
|
2225
|
+
messageToSend = `${planModePrompt}\n\n---\n\nUser request: ${userMessage}`;
|
|
2226
|
+
}
|
|
2227
|
+
// Inject delegation completion notifications (deferred delivery)
|
|
2228
|
+
// If background agents completed while coordinator was busy,
|
|
2229
|
+
// prepend their results to the next coordinator message
|
|
2230
|
+
const delegationTracker = getDelegationTracker();
|
|
2231
|
+
if (delegationTracker.hasCompletionEvents()) {
|
|
2232
|
+
const completionEvents = delegationTracker.drainCompletionEvents();
|
|
2233
|
+
const completionPrefix = this.formatCompletionMessage(completionEvents);
|
|
2234
|
+
messageToSend = `${completionPrefix}\n\n---\n\n${messageToSend}`;
|
|
2235
|
+
}
|
|
2236
|
+
// Debug logging (enable with DEBUG_AGENT_REQUESTS=1)
|
|
2237
|
+
debugLogAgentRequest(agent, messageToSend);
|
|
2238
|
+
for await (const event of agent.stream(messageToSend, {
|
|
480
2239
|
signal,
|
|
481
2240
|
chatOptions: { model: this.model },
|
|
2241
|
+
// Provide tool-specific context for bash backgrounding (Ctrl+B)
|
|
2242
|
+
getToolContext: (toolName, toolUseId) => {
|
|
2243
|
+
if (toolName === 'bash') {
|
|
2244
|
+
// Create an AbortController for this bash command
|
|
2245
|
+
const bashAbortController = new AbortController();
|
|
2246
|
+
this.bashAbortControllers.set(toolUseId, bashAbortController);
|
|
2247
|
+
return {
|
|
2248
|
+
abortSignal: bashAbortController.signal,
|
|
2249
|
+
onBackground: (_shellId, _partialOutput) => {
|
|
2250
|
+
// Clean up the controller after backgrounding
|
|
2251
|
+
this.bashAbortControllers.delete(toolUseId);
|
|
2252
|
+
// The UI already handled the notification
|
|
2253
|
+
},
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
return {};
|
|
2257
|
+
},
|
|
482
2258
|
})) {
|
|
483
2259
|
if (signal.aborted)
|
|
484
2260
|
break;
|
|
@@ -486,19 +2262,87 @@ export class ReplV2 {
|
|
|
486
2262
|
if (event.chunk.type === 'text' && event.chunk.text) {
|
|
487
2263
|
this.textAccumulator += event.chunk.text;
|
|
488
2264
|
}
|
|
2265
|
+
else if (event.chunk.type === 'thinking_start') {
|
|
2266
|
+
// Extended thinking started - show indicator
|
|
2267
|
+
this.ui.setThinking(true);
|
|
2268
|
+
}
|
|
2269
|
+
else if (event.chunk.type === 'thinking_end') {
|
|
2270
|
+
// Extended thinking ended - hide indicator
|
|
2271
|
+
this.ui.setThinking(false);
|
|
2272
|
+
}
|
|
489
2273
|
else if (event.chunk.type === 'done' && event.chunk.usage) {
|
|
490
2274
|
this.sessionInputTokens += event.chunk.usage.inputTokens;
|
|
491
2275
|
this.sessionOutputTokens += event.chunk.usage.outputTokens;
|
|
492
2276
|
this.sessionRequests++;
|
|
2277
|
+
// Record message for telemetry heartbeat
|
|
2278
|
+
getAuthManager().recordMessage(event.chunk.usage.inputTokens, event.chunk.usage.outputTokens);
|
|
2279
|
+
// Update live token display (including thinking tokens for Gemini 2.5+)
|
|
2280
|
+
const usage = event.chunk.usage;
|
|
2281
|
+
this.ui.addTokens(usage.inputTokens, usage.outputTokens, usage.thinkingTokens, usage.cacheReadTokens, usage.debugPayload);
|
|
2282
|
+
this.ui.incrementApiCalls();
|
|
2283
|
+
// Debug: log each API call's token usage to file (not console, to avoid UI artifacts)
|
|
2284
|
+
if (DEBUG_ENABLED) {
|
|
2285
|
+
const metrics = this.ui.getTurnMetrics();
|
|
2286
|
+
const debugLine = `[API Call #${String(metrics.apiCalls)}] +${String(usage.inputTokens)} in, +${String(usage.outputTokens)} out → Turn total: ${String(metrics.inputTokens)} in, ${String(metrics.outputTokens)} out\n`;
|
|
2287
|
+
appendFileSync(DEBUG_LOG_FILE, debugLine);
|
|
2288
|
+
// Log raw usage object to see all fields
|
|
2289
|
+
appendFileSync(DEBUG_LOG_FILE, ` Raw usage: ${JSON.stringify(event.chunk.usage)}\n`);
|
|
2290
|
+
}
|
|
2291
|
+
// Update cache info for breakdown display
|
|
2292
|
+
updateCacheInfo(usage.cacheReadTokens ?? 0, usage.cacheCreationTokens ?? 0);
|
|
2293
|
+
// Update team agent's token tracking
|
|
2294
|
+
if (this.team) {
|
|
2295
|
+
const activeTeamAgent = this.team.getActive();
|
|
2296
|
+
activeTeamAgent.updateTokenUsage(event.chunk.usage.inputTokens, event.chunk.usage.outputTokens);
|
|
2297
|
+
}
|
|
493
2298
|
}
|
|
494
2299
|
}
|
|
495
2300
|
else if (event.type === 'tool_start') {
|
|
496
2301
|
// Flush accumulated text before tool
|
|
497
2302
|
if (this.textAccumulator.trim()) {
|
|
498
|
-
this.ui.print({
|
|
2303
|
+
this.ui.print({
|
|
2304
|
+
type: 'agent-text',
|
|
2305
|
+
text: this.textAccumulator.trim(),
|
|
2306
|
+
expression: this.getActiveAgentMascot(),
|
|
2307
|
+
agentId: activeAgentId,
|
|
2308
|
+
});
|
|
499
2309
|
this.textAccumulator = '';
|
|
500
2310
|
}
|
|
2311
|
+
// Track tool calls for per-turn metrics
|
|
2312
|
+
this.ui.incrementToolCalls();
|
|
2313
|
+
// Record tool usage for telemetry
|
|
2314
|
+
if (event.name) {
|
|
2315
|
+
getAuthManager().recordTool(event.name);
|
|
2316
|
+
}
|
|
501
2317
|
lastToolInput = event.input;
|
|
2318
|
+
// Pre-compute diff for edit tool (must happen BEFORE tool modifies the file)
|
|
2319
|
+
pendingDiffLines = undefined;
|
|
2320
|
+
{
|
|
2321
|
+
// Determine the actual tool name (handle use_tool wrapping)
|
|
2322
|
+
const actualToolName = event.name === 'use_tool' && typeof event.input.tool_name === 'string'
|
|
2323
|
+
? event.input.tool_name
|
|
2324
|
+
: event.name;
|
|
2325
|
+
const actualInput = event.name === 'use_tool' && typeof event.input.args === 'object' && event.input.args !== null
|
|
2326
|
+
? event.input.args
|
|
2327
|
+
: event.input;
|
|
2328
|
+
if (actualToolName === 'edit') {
|
|
2329
|
+
const filePath = typeof actualInput.filePath === 'string' ? actualInput.filePath : '';
|
|
2330
|
+
const oldText = typeof actualInput.oldString === 'string' ? actualInput.oldString : '';
|
|
2331
|
+
const newText = typeof actualInput.newString === 'string' ? actualInput.newString : '';
|
|
2332
|
+
const replaceAll = Boolean(actualInput.replaceAll);
|
|
2333
|
+
if (filePath && oldText && newText) {
|
|
2334
|
+
const editDiff = generateEditDiff(filePath, oldText, newText, replaceAll);
|
|
2335
|
+
if (editDiff) {
|
|
2336
|
+
pendingDiffLines = editDiff.lines;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
// Debug: log tool calls to file
|
|
2342
|
+
if (DEBUG_ENABLED) {
|
|
2343
|
+
const toolDebugLine = `[Tool Call] ${event.name}(${JSON.stringify(event.input).slice(0, 200)})\n`;
|
|
2344
|
+
appendFileSync(DEBUG_LOG_FILE, toolDebugLine);
|
|
2345
|
+
}
|
|
502
2346
|
// For bash tool, add to live region for streaming output
|
|
503
2347
|
if (event.name === 'bash' && event.toolUseId) {
|
|
504
2348
|
const command = typeof lastToolInput.command === 'string'
|
|
@@ -506,8 +2350,8 @@ export class ReplV2 {
|
|
|
506
2350
|
: '';
|
|
507
2351
|
this.ui.addBashCommand(event.toolUseId, command);
|
|
508
2352
|
}
|
|
509
|
-
// Update spinner with tool name
|
|
510
|
-
if (
|
|
2353
|
+
// Update spinner with tool name (skip silent tools)
|
|
2354
|
+
if (!this.agent?.isToolSilent(event.name)) {
|
|
511
2355
|
if (event.name === 'task' && event.toolUseId) {
|
|
512
2356
|
const subagentType = typeof lastToolInput.subagent_type === 'string'
|
|
513
2357
|
? lastToolInput.subagent_type
|
|
@@ -521,6 +2365,27 @@ export class ReplV2 {
|
|
|
521
2365
|
// No queue needed - callbacks now receive toolUseId directly!
|
|
522
2366
|
this.ui.addSubagent(event.toolUseId, subagentType, description);
|
|
523
2367
|
}
|
|
2368
|
+
else if (event.name === 'use_tool') {
|
|
2369
|
+
// Show the inner tool name being called
|
|
2370
|
+
const innerToolName = typeof lastToolInput.tool_name === 'string'
|
|
2371
|
+
? lastToolInput.tool_name
|
|
2372
|
+
: 'unknown';
|
|
2373
|
+
setAction(innerToolName);
|
|
2374
|
+
this.ui.setSpinnerText(`Running ${innerToolName}...`);
|
|
2375
|
+
}
|
|
2376
|
+
else if (event.name === 'list_tools') {
|
|
2377
|
+
// Quiet display for list_tools
|
|
2378
|
+
setAction('list_tools');
|
|
2379
|
+
this.ui.setSpinnerText('Discovering tools...');
|
|
2380
|
+
}
|
|
2381
|
+
else if (event.name === 'get_tool_info') {
|
|
2382
|
+
// Quiet display for get_tool_info
|
|
2383
|
+
const toolName = typeof lastToolInput.tool_name === 'string'
|
|
2384
|
+
? lastToolInput.tool_name
|
|
2385
|
+
: '?';
|
|
2386
|
+
setAction('get_tool_info');
|
|
2387
|
+
this.ui.setSpinnerText(`Getting schema for ${toolName}...`);
|
|
2388
|
+
}
|
|
524
2389
|
else if (event.name !== 'task') {
|
|
525
2390
|
setAction(event.name);
|
|
526
2391
|
this.ui.setSpinnerText(`Running ${event.name}...`);
|
|
@@ -540,6 +2405,17 @@ export class ReplV2 {
|
|
|
540
2405
|
lastToolInput = null;
|
|
541
2406
|
// Complete bash command in live region
|
|
542
2407
|
if (toolName === 'bash' && event.toolUseId) {
|
|
2408
|
+
// Clean up abort controller
|
|
2409
|
+
this.bashAbortControllers.delete(event.toolUseId);
|
|
2410
|
+
// Check if command was backgrounded
|
|
2411
|
+
if (result.success && typeof result.result === 'object' && result.result !== null) {
|
|
2412
|
+
const res = result.result;
|
|
2413
|
+
if (res.backgrounded === true) {
|
|
2414
|
+
// Command was moved to background - don't commit, just clear
|
|
2415
|
+
this.ui.setCurrentTool(null);
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
543
2419
|
let exitCode = result.success ? 0 : 1;
|
|
544
2420
|
if (result.success && typeof result.result === 'object' && result.result !== null) {
|
|
545
2421
|
const res = result.result;
|
|
@@ -564,15 +2440,100 @@ export class ReplV2 {
|
|
|
564
2440
|
? t.status
|
|
565
2441
|
: 'pending');
|
|
566
2442
|
const activeForm = typeof t.activeForm === 'string' ? t.activeForm : undefined;
|
|
567
|
-
|
|
2443
|
+
const owner = typeof t.owner === 'string' ? t.owner : undefined;
|
|
2444
|
+
const blockedBy = Array.isArray(t.blockedBy)
|
|
2445
|
+
? t.blockedBy.filter((n) => typeof n === 'number')
|
|
2446
|
+
: undefined;
|
|
2447
|
+
return { content, status, activeForm, owner, blockedBy };
|
|
568
2448
|
});
|
|
569
2449
|
this.ui.setTodos(todos);
|
|
570
2450
|
this.ui.setCurrentTool(null);
|
|
571
2451
|
continue;
|
|
572
2452
|
}
|
|
573
2453
|
// Skip silent tools (no output needed)
|
|
574
|
-
if (toolName
|
|
575
|
-
|
|
2454
|
+
if (this.agent?.isToolSilent(toolName)) {
|
|
2455
|
+
this.ui.setCurrentTool(null);
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
2458
|
+
// Handle list_tools - show compact summary
|
|
2459
|
+
if (toolName === 'list_tools') {
|
|
2460
|
+
const innerResult = result.success && typeof result.result === 'object'
|
|
2461
|
+
? result.result
|
|
2462
|
+
: {};
|
|
2463
|
+
const count = typeof innerResult.count === 'number' ? innerResult.count : 0;
|
|
2464
|
+
const categoryParam = toolInput && typeof toolInput.category === 'string'
|
|
2465
|
+
? `category: ${toolInput.category}`
|
|
2466
|
+
: '';
|
|
2467
|
+
this.ui.print({
|
|
2468
|
+
type: 'tool-result',
|
|
2469
|
+
name: 'list_tools',
|
|
2470
|
+
params: categoryParam,
|
|
2471
|
+
summary: `${String(count)} tools available`,
|
|
2472
|
+
success: result.success,
|
|
2473
|
+
agentId: activeAgentId,
|
|
2474
|
+
});
|
|
2475
|
+
this.ui.setCurrentTool(null);
|
|
2476
|
+
continue;
|
|
2477
|
+
}
|
|
2478
|
+
// Handle use_tool - show the inner tool name instead
|
|
2479
|
+
if (toolName === 'use_tool' && toolInput) {
|
|
2480
|
+
const innerToolName = typeof toolInput.tool_name === 'string'
|
|
2481
|
+
? toolInput.tool_name
|
|
2482
|
+
: 'unknown';
|
|
2483
|
+
const innerArgs = typeof toolInput.args === 'object' && toolInput.args !== null
|
|
2484
|
+
? toolInput.args
|
|
2485
|
+
: {};
|
|
2486
|
+
// Extract and format using the inner tool name
|
|
2487
|
+
const { content, summary, success } = this.extractToolResult(innerToolName, result);
|
|
2488
|
+
// Resolve diff lines for edit (pre-computed) or write_file (computed now)
|
|
2489
|
+
let innerDiffLines = pendingDiffLines;
|
|
2490
|
+
pendingDiffLines = undefined;
|
|
2491
|
+
if (!innerDiffLines && (innerToolName === 'write_file' || innerToolName === 'write')) {
|
|
2492
|
+
const filePath = typeof innerArgs.path === 'string' ? innerArgs.path : '';
|
|
2493
|
+
const writeContent = typeof innerArgs.content === 'string' ? innerArgs.content : '';
|
|
2494
|
+
if (filePath && writeContent && success) {
|
|
2495
|
+
const writeDiff = generateWriteDiff(filePath, writeContent, true);
|
|
2496
|
+
innerDiffLines = writeDiff.lines;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
this.ui.print({
|
|
2500
|
+
type: 'tool-result',
|
|
2501
|
+
name: innerToolName,
|
|
2502
|
+
params: this.formatToolParams(innerArgs),
|
|
2503
|
+
summary,
|
|
2504
|
+
content: content || undefined,
|
|
2505
|
+
diffLines: innerDiffLines,
|
|
2506
|
+
success,
|
|
2507
|
+
agentId: activeAgentId,
|
|
2508
|
+
});
|
|
2509
|
+
this.ui.setCurrentTool(null);
|
|
2510
|
+
continue;
|
|
2511
|
+
}
|
|
2512
|
+
// Handle get_tool_info - show compact summary
|
|
2513
|
+
if (toolName === 'get_tool_info') {
|
|
2514
|
+
let name = '?';
|
|
2515
|
+
if (result.success) {
|
|
2516
|
+
if (typeof result.result === 'string') {
|
|
2517
|
+
// Compact schema format (SDK 0.1.15+): "tool_name(...)\n description"
|
|
2518
|
+
const parenIdx = result.result.indexOf('(');
|
|
2519
|
+
if (parenIdx > 0)
|
|
2520
|
+
name = result.result.slice(0, parenIdx);
|
|
2521
|
+
}
|
|
2522
|
+
else if (typeof result.result === 'object' && result.result !== null) {
|
|
2523
|
+
// Legacy object format: { name, description, parameters }
|
|
2524
|
+
const obj = result.result;
|
|
2525
|
+
if (typeof obj.name === 'string')
|
|
2526
|
+
name = obj.name;
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
this.ui.print({
|
|
2530
|
+
type: 'tool-result',
|
|
2531
|
+
name: 'get_tool_info',
|
|
2532
|
+
params: name,
|
|
2533
|
+
summary: `Schema: ${name}`,
|
|
2534
|
+
success: result.success,
|
|
2535
|
+
agentId: activeAgentId,
|
|
2536
|
+
});
|
|
576
2537
|
this.ui.setCurrentTool(null);
|
|
577
2538
|
continue;
|
|
578
2539
|
}
|
|
@@ -588,39 +2549,517 @@ export class ReplV2 {
|
|
|
588
2549
|
}
|
|
589
2550
|
// Extract and format tool result
|
|
590
2551
|
const { content, summary, success } = this.extractToolResult(toolName, result);
|
|
2552
|
+
// Resolve diff lines for edit/write_file tools
|
|
2553
|
+
let diffLines = pendingDiffLines;
|
|
2554
|
+
pendingDiffLines = undefined;
|
|
2555
|
+
// For write_file, generate diff from input (doesn't need pre-edit state)
|
|
2556
|
+
if (!diffLines && toolInput) {
|
|
2557
|
+
const actualToolName = toolName === 'use_tool' && typeof toolInput.tool_name === 'string'
|
|
2558
|
+
? toolInput.tool_name
|
|
2559
|
+
: toolName;
|
|
2560
|
+
const actualInput = toolName === 'use_tool' && typeof toolInput.args === 'object' && toolInput.args !== null
|
|
2561
|
+
? toolInput.args
|
|
2562
|
+
: toolInput;
|
|
2563
|
+
if (actualToolName === 'write_file' || actualToolName === 'write') {
|
|
2564
|
+
const filePath = typeof actualInput.path === 'string' ? actualInput.path : '';
|
|
2565
|
+
const writeContent = typeof actualInput.content === 'string' ? actualInput.content : '';
|
|
2566
|
+
if (filePath && writeContent && success) {
|
|
2567
|
+
const writeDiff = generateWriteDiff(filePath, writeContent, true);
|
|
2568
|
+
diffLines = writeDiff.lines;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
591
2572
|
this.ui.print({
|
|
592
2573
|
type: 'tool-result',
|
|
593
2574
|
name: toolName,
|
|
594
2575
|
params: this.formatToolParams(toolInput),
|
|
595
2576
|
summary,
|
|
596
|
-
content: content
|
|
2577
|
+
content: content || undefined,
|
|
2578
|
+
diffLines,
|
|
597
2579
|
success,
|
|
2580
|
+
agentId: activeAgentId,
|
|
598
2581
|
});
|
|
599
2582
|
this.ui.setCurrentTool(null);
|
|
600
2583
|
}
|
|
2584
|
+
else if (event.type === 'context_compacted') {
|
|
2585
|
+
// Context was compacted - show file restoration hints
|
|
2586
|
+
const hints = agent.formatRestorationHints();
|
|
2587
|
+
const savedTokens = event.tokensBefore - event.tokensAfter;
|
|
2588
|
+
const savedPct = Math.round((savedTokens / event.tokensBefore) * 100);
|
|
2589
|
+
this.ui.print({
|
|
2590
|
+
type: 'info',
|
|
2591
|
+
message: `Context compacted: ${event.tokensBefore.toLocaleString()} → ${event.tokensAfter.toLocaleString()} tokens (${String(savedPct)}% reduction)`,
|
|
2592
|
+
});
|
|
2593
|
+
if (hints) {
|
|
2594
|
+
// Display file restoration hints
|
|
2595
|
+
this.ui.print({
|
|
2596
|
+
type: 'info',
|
|
2597
|
+
message: hints,
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
// Hint about Ctrl+O
|
|
2601
|
+
this.ui.print({
|
|
2602
|
+
type: 'info',
|
|
2603
|
+
message: 'Tip: Use Ctrl+O to view full conversation history',
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
else if (event.type === 'context_summarized') {
|
|
2607
|
+
// Context was summarized (LLM-based) - more aggressive
|
|
2608
|
+
const savedTokens = event.tokensBefore - event.tokensAfter;
|
|
2609
|
+
const savedPct = Math.round((savedTokens / event.tokensBefore) * 100);
|
|
2610
|
+
this.ui.print({
|
|
2611
|
+
type: 'info',
|
|
2612
|
+
message: `Context summarized (${String(event.rounds)} round${event.rounds > 1 ? 's' : ''}): ${event.tokensBefore.toLocaleString()} → ${event.tokensAfter.toLocaleString()} tokens (${String(savedPct)}% reduction)`,
|
|
2613
|
+
});
|
|
2614
|
+
const hints = agent.formatRestorationHints();
|
|
2615
|
+
if (hints) {
|
|
2616
|
+
this.ui.print({
|
|
2617
|
+
type: 'info',
|
|
2618
|
+
message: hints,
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
else if (event.type === 'llm_retry') {
|
|
2623
|
+
// LLM call failed, retrying with exponential backoff
|
|
2624
|
+
const delaySec = (event.delayMs / 1000).toFixed(1);
|
|
2625
|
+
this.ui.print({
|
|
2626
|
+
type: 'warning',
|
|
2627
|
+
message: `Connection failed (attempt ${String(event.attempt)}/${String(event.maxAttempts)}): ${event.error}. Retrying in ${delaySec}s...`,
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
else if (event.type === 'custom' && typeof event.name === 'string' && event.name.startsWith('delegation:')) {
|
|
2631
|
+
// Tool result delegation events
|
|
2632
|
+
const data = event.data;
|
|
2633
|
+
if (event.name === 'delegation:completed') {
|
|
2634
|
+
const originalTokens = data.originalTokens;
|
|
2635
|
+
const summaryTokens = data.summaryTokens;
|
|
2636
|
+
const toolName = data.toolName;
|
|
2637
|
+
this.ui.setSpinnerText(`Delegated ${toolName} result: ${String(originalTokens)} → ${String(summaryTokens)} tokens`);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
else if (event.type === 'llm_retry_exhausted') {
|
|
2641
|
+
// All retry attempts failed
|
|
2642
|
+
this.ui.print({
|
|
2643
|
+
type: 'error',
|
|
2644
|
+
message: `All ${String(event.attempts)} retry attempts failed: ${event.error}`,
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
601
2647
|
}
|
|
602
|
-
// Flush remaining text
|
|
2648
|
+
// Flush remaining text and check for PLAN_READY marker
|
|
603
2649
|
if (this.textAccumulator.trim()) {
|
|
604
|
-
|
|
2650
|
+
const text = this.textAccumulator.trim();
|
|
2651
|
+
// Check for PLAN_READY marker
|
|
2652
|
+
const planReadyInfo = this.detectPlanReady(text);
|
|
2653
|
+
// Print the text (remove the marker from display if present)
|
|
2654
|
+
const displayText = planReadyInfo
|
|
2655
|
+
? text.replace(ReplV2.PLAN_READY_REGEX, '').trim()
|
|
2656
|
+
: text;
|
|
2657
|
+
if (displayText) {
|
|
2658
|
+
this.ui.print({ type: 'agent-text', text: displayText, expression: this.getActiveAgentMascot(), agentId: activeAgentId });
|
|
2659
|
+
}
|
|
2660
|
+
// If PLAN_READY marker found, show approval overlay
|
|
2661
|
+
if (planReadyInfo) {
|
|
2662
|
+
await this.handlePlanApproval(planReadyInfo.planId, planReadyInfo.planName);
|
|
2663
|
+
}
|
|
605
2664
|
}
|
|
2665
|
+
// Print per-turn summary before stopping agent
|
|
2666
|
+
this.printTurnSummary();
|
|
606
2667
|
this.ui.setAgentRunning(false);
|
|
2668
|
+
this.ui.setThinking(false); // Ensure thinking indicator is cleared
|
|
607
2669
|
// NOTE: Don't clear todos here - they should persist after agent finishes
|
|
608
2670
|
// to show the final state. Only clear on explicit user action or new conversation.
|
|
609
2671
|
// Notify that agent has finished (for applying deferred suggestions)
|
|
610
2672
|
this.onAgentFinish?.();
|
|
2673
|
+
// Auto-save session after successful agent response
|
|
2674
|
+
this.autoSaveSession().catch((err) => {
|
|
2675
|
+
// Log but don't interrupt - saving is best-effort
|
|
2676
|
+
console.error('Failed to save session:', err);
|
|
2677
|
+
});
|
|
611
2678
|
}
|
|
612
2679
|
catch (err) {
|
|
613
2680
|
if (!signal.aborted) {
|
|
614
2681
|
this.ui.print({ type: 'error', message: String(err) });
|
|
615
2682
|
}
|
|
616
2683
|
this.ui.setAgentRunning(false);
|
|
2684
|
+
this.ui.setThinking(false); // Ensure thinking indicator is cleared
|
|
617
2685
|
// Still notify on error so suggestions can be applied
|
|
618
2686
|
this.onAgentFinish?.();
|
|
619
2687
|
}
|
|
620
2688
|
})(),
|
|
621
2689
|
};
|
|
622
|
-
|
|
623
|
-
this.
|
|
2690
|
+
// Register the session (foreground)
|
|
2691
|
+
this.startSession(session);
|
|
2692
|
+
// Wait for session to complete
|
|
2693
|
+
await session.promise;
|
|
2694
|
+
this.endSession(activeAgentId);
|
|
2695
|
+
// Check for pending delegation and handle it
|
|
2696
|
+
await this.handlePendingDelegation();
|
|
2697
|
+
}
|
|
2698
|
+
// ===========================================================================
|
|
2699
|
+
// Delegation Completion (Phase 2 - Coordinator Mode)
|
|
2700
|
+
// ===========================================================================
|
|
2701
|
+
/**
|
|
2702
|
+
* Format completion events into a structured message for the coordinator.
|
|
2703
|
+
* Groups events by agent and includes task, result summary, and artifact info.
|
|
2704
|
+
*/
|
|
2705
|
+
formatCompletionMessage(events) {
|
|
2706
|
+
const lines = ['[DELEGATION COMPLETIONS]', ''];
|
|
2707
|
+
for (const event of events) {
|
|
2708
|
+
const delegation = getDelegationTracker().get(event.delegationId);
|
|
2709
|
+
const agentLabel = this.team?.get(event.agentId)?.displayName ?? event.agentId;
|
|
2710
|
+
const status = event.status === 'completed' ? 'COMPLETED' : 'FAILED';
|
|
2711
|
+
lines.push(`## $${event.agentId} (${agentLabel}) — ${status}`);
|
|
2712
|
+
if (delegation) {
|
|
2713
|
+
lines.push(`Task: ${delegation.task}`);
|
|
2714
|
+
}
|
|
2715
|
+
if (event.result.success) {
|
|
2716
|
+
lines.push(`Result: ${event.result.summary}`);
|
|
2717
|
+
}
|
|
2718
|
+
else {
|
|
2719
|
+
lines.push(`Error: ${event.result.error ?? event.result.summary}`);
|
|
2720
|
+
}
|
|
2721
|
+
if (event.result.artifactIds.length > 0) {
|
|
2722
|
+
lines.push(`Artifacts: ${event.result.artifactIds.join(', ')}`);
|
|
2723
|
+
}
|
|
2724
|
+
lines.push('');
|
|
2725
|
+
}
|
|
2726
|
+
lines.push('Review the results above and decide next steps.');
|
|
2727
|
+
return lines.join('\n');
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
2730
|
+
* Check and process delegation completions for an agent that just finished.
|
|
2731
|
+
* Marks all running delegations for this agent as completed or failed,
|
|
2732
|
+
* then attempts to auto-resume the coordinator if idle.
|
|
2733
|
+
*
|
|
2734
|
+
* @param agentId - The agent that just finished
|
|
2735
|
+
* @param error - If present, the agent failed with this error
|
|
2736
|
+
*/
|
|
2737
|
+
checkDelegationCompletion(agentId, error) {
|
|
2738
|
+
const tracker = getDelegationTracker();
|
|
2739
|
+
const running = tracker.getByAgent(agentId).filter(d => d.status === 'running');
|
|
2740
|
+
if (running.length === 0)
|
|
2741
|
+
return;
|
|
2742
|
+
for (const delegation of running) {
|
|
2743
|
+
if (error) {
|
|
2744
|
+
tracker.fail(delegation.id, error);
|
|
2745
|
+
}
|
|
2746
|
+
else {
|
|
2747
|
+
// Gather artifacts created during this delegation
|
|
2748
|
+
const artifactIds = [];
|
|
2749
|
+
if (this.team) {
|
|
2750
|
+
const currentProject = getCurrentProject();
|
|
2751
|
+
const projectId = currentProject?.id ?? null;
|
|
2752
|
+
const artifactStore = getTeamCheckpointer().getArtifactStore(projectId);
|
|
2753
|
+
const agentArtifacts = artifactStore.listByAgent(agentId);
|
|
2754
|
+
for (const artifact of agentArtifacts) {
|
|
2755
|
+
if (artifact.createdAt >= delegation.createdAt) {
|
|
2756
|
+
artifactIds.push(artifact.id);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
tracker.complete(delegation.id, {
|
|
2761
|
+
success: true,
|
|
2762
|
+
summary: `Task completed successfully${artifactIds.length > 0 ? ` with ${String(artifactIds.length)} artifact(s)` : ''}`,
|
|
2763
|
+
artifactIds,
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
// Try to auto-resume the coordinator (fire-and-forget — async switch must complete before runAgentReal)
|
|
2768
|
+
void this.maybeResumeCoordinator();
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Auto-resume the coordinator if it's idle and there are pending completion events.
|
|
2772
|
+
* If the coordinator is busy, events stay in queue for deferred delivery.
|
|
2773
|
+
*
|
|
2774
|
+
* Must be async to properly await switchAgent before running the coordinator.
|
|
2775
|
+
* Called fire-and-forget from checkDelegationCompletion — this ensures the
|
|
2776
|
+
* background agent's endSession() runs independently (no session conflicts).
|
|
2777
|
+
*/
|
|
2778
|
+
async maybeResumeCoordinator() {
|
|
2779
|
+
const tracker = getDelegationTracker();
|
|
2780
|
+
if (!tracker.hasCompletionEvents())
|
|
2781
|
+
return;
|
|
2782
|
+
// Check if coordinator (default agent) is idle
|
|
2783
|
+
if (this.sessions.has('default'))
|
|
2784
|
+
return; // Coordinator is busy
|
|
2785
|
+
// Drain events and format the completion message
|
|
2786
|
+
const events = tracker.drainCompletionEvents();
|
|
2787
|
+
const completionMessage = this.formatCompletionMessage(events);
|
|
2788
|
+
// Switch to coordinator if not already active — MUST await before runAgentReal
|
|
2789
|
+
// so that this.team.getActive().id returns 'default' and the foreground session
|
|
2790
|
+
// is registered under the correct agent ID
|
|
2791
|
+
if (this.team && this.team.getActive().id !== 'default') {
|
|
2792
|
+
try {
|
|
2793
|
+
await this.switchAgent('default', {
|
|
2794
|
+
demoteCurrentIfBusy: false,
|
|
2795
|
+
trackSession: false,
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
catch (err) {
|
|
2799
|
+
this.ui.print({
|
|
2800
|
+
type: 'error',
|
|
2801
|
+
message: `Failed to switch to coordinator: ${err instanceof Error ? err.message : String(err)}`,
|
|
2802
|
+
});
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
// Run coordinator foreground with the completion message
|
|
2807
|
+
this.ui.print({
|
|
2808
|
+
type: 'info',
|
|
2809
|
+
message: 'Delegation completed — resuming coordinator...',
|
|
2810
|
+
});
|
|
2811
|
+
void this.runAgentReal(completionMessage);
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Run an agent in background (Phase 3 - Background Agents).
|
|
2815
|
+
* Similar to runAgentReal but:
|
|
2816
|
+
* - Creates a background session (isBackground: true)
|
|
2817
|
+
* - Labels all output with agent mascot
|
|
2818
|
+
* - Doesn't block the caller
|
|
2819
|
+
* - Shows completion notification when done
|
|
2820
|
+
*
|
|
2821
|
+
* @param agentId - The agent ID to run
|
|
2822
|
+
* @param userMessage - The message to send to the agent
|
|
2823
|
+
*/
|
|
2824
|
+
async runAgentBackground(agentId, userMessage) {
|
|
2825
|
+
if (!this.agent)
|
|
2826
|
+
return;
|
|
2827
|
+
const abortController = new AbortController();
|
|
2828
|
+
const signal = abortController.signal;
|
|
2829
|
+
const label = this.getAgentMascotLabel(agentId);
|
|
2830
|
+
const mascot = this.team?.get(agentId)?.mascot ?? '[•_•]';
|
|
2831
|
+
// Create background session
|
|
2832
|
+
const session = {
|
|
2833
|
+
agentId,
|
|
2834
|
+
isBackground: true,
|
|
2835
|
+
abortController,
|
|
2836
|
+
currentAction: null,
|
|
2837
|
+
taskDescription: truncate(userMessage, 50),
|
|
2838
|
+
promise: (async () => {
|
|
2839
|
+
try {
|
|
2840
|
+
// Note: Don't set UI to "running" state - that's for foreground only
|
|
2841
|
+
let textAccumulator = '';
|
|
2842
|
+
// Stream agent events
|
|
2843
|
+
const agent = this.agent;
|
|
2844
|
+
if (!agent)
|
|
2845
|
+
return; // Type guard
|
|
2846
|
+
// Phase 3b: Set the executing context so permission handler knows which agent is calling
|
|
2847
|
+
this.currentExecutingBackgroundAgentId = agentId;
|
|
2848
|
+
for await (const event of agent.stream(userMessage, {
|
|
2849
|
+
signal,
|
|
2850
|
+
chatOptions: { model: this.model },
|
|
2851
|
+
})) {
|
|
2852
|
+
if (signal.aborted)
|
|
2853
|
+
break;
|
|
2854
|
+
if (event.type === 'llm_chunk') {
|
|
2855
|
+
if (event.chunk.type === 'text' && event.chunk.text) {
|
|
2856
|
+
textAccumulator += event.chunk.text;
|
|
2857
|
+
}
|
|
2858
|
+
else if (event.chunk.type === 'done' && event.chunk.usage) {
|
|
2859
|
+
// Update session token tracking
|
|
2860
|
+
this.sessionInputTokens += event.chunk.usage.inputTokens;
|
|
2861
|
+
this.sessionOutputTokens += event.chunk.usage.outputTokens;
|
|
2862
|
+
this.sessionRequests++;
|
|
2863
|
+
// Record message for telemetry heartbeat
|
|
2864
|
+
getAuthManager().recordMessage(event.chunk.usage.inputTokens, event.chunk.usage.outputTokens);
|
|
2865
|
+
// Update team agent's token tracking
|
|
2866
|
+
if (this.team) {
|
|
2867
|
+
const teamAgent = this.team.get(agentId);
|
|
2868
|
+
teamAgent?.updateTokenUsage(event.chunk.usage.inputTokens, event.chunk.usage.outputTokens);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
else if (event.type === 'tool_start') {
|
|
2873
|
+
// Record tool usage for telemetry
|
|
2874
|
+
if (event.name) {
|
|
2875
|
+
getAuthManager().recordTool(event.name);
|
|
2876
|
+
}
|
|
2877
|
+
// Flush accumulated text before tool (with label)
|
|
2878
|
+
if (textAccumulator.trim()) {
|
|
2879
|
+
// Use 'background-agent' type for proper labeling without double mascot
|
|
2880
|
+
this.ui.print({
|
|
2881
|
+
type: 'background-agent',
|
|
2882
|
+
agentId,
|
|
2883
|
+
mascot,
|
|
2884
|
+
text: textAccumulator.trim(),
|
|
2885
|
+
});
|
|
2886
|
+
textAccumulator = '';
|
|
2887
|
+
}
|
|
2888
|
+
// Show tool start with label
|
|
2889
|
+
this.ui.print({
|
|
2890
|
+
type: 'background-agent',
|
|
2891
|
+
agentId,
|
|
2892
|
+
mascot,
|
|
2893
|
+
text: `Running: ${event.name}`,
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
// Flush any remaining text
|
|
2898
|
+
if (textAccumulator.trim()) {
|
|
2899
|
+
this.ui.print({
|
|
2900
|
+
type: 'background-agent',
|
|
2901
|
+
agentId,
|
|
2902
|
+
mascot,
|
|
2903
|
+
text: textAccumulator.trim(),
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
// Show completion notification
|
|
2907
|
+
this.ui.print({
|
|
2908
|
+
type: 'info',
|
|
2909
|
+
message: `${label} Finished ✓`,
|
|
2910
|
+
});
|
|
2911
|
+
// Notify other terminals about completion
|
|
2912
|
+
try {
|
|
2913
|
+
const sid = getActiveTerminalSessionId();
|
|
2914
|
+
const proj = getActiveProject();
|
|
2915
|
+
if (sid && proj) {
|
|
2916
|
+
getNotificationManager()?.insert({
|
|
2917
|
+
projectId: proj.id,
|
|
2918
|
+
fromSessionId: sid,
|
|
2919
|
+
type: 'agent_finished',
|
|
2920
|
+
title: `${agentId} finished`,
|
|
2921
|
+
message: 'Task completed successfully.',
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
catch {
|
|
2926
|
+
// Best effort
|
|
2927
|
+
}
|
|
2928
|
+
// Check delegation completion (success path)
|
|
2929
|
+
this.checkDelegationCompletion(agentId);
|
|
2930
|
+
}
|
|
2931
|
+
catch (err) {
|
|
2932
|
+
if (!signal.aborted) {
|
|
2933
|
+
this.ui.print({ type: 'error', message: `${label} Error: ${String(err)}` });
|
|
2934
|
+
// Notify other terminals about failure
|
|
2935
|
+
try {
|
|
2936
|
+
const sid = getActiveTerminalSessionId();
|
|
2937
|
+
const proj = getActiveProject();
|
|
2938
|
+
if (sid && proj) {
|
|
2939
|
+
getNotificationManager()?.insert({
|
|
2940
|
+
projectId: proj.id,
|
|
2941
|
+
fromSessionId: sid,
|
|
2942
|
+
type: 'agent_finished',
|
|
2943
|
+
title: `${agentId} failed`,
|
|
2944
|
+
message: String(err).slice(0, 200),
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
catch {
|
|
2949
|
+
// Best effort
|
|
2950
|
+
}
|
|
2951
|
+
// Check delegation completion (failure path)
|
|
2952
|
+
this.checkDelegationCompletion(agentId, String(err));
|
|
2953
|
+
}
|
|
2954
|
+
else {
|
|
2955
|
+
// Killed by user — cancel delegations (no completion event)
|
|
2956
|
+
getDelegationTracker().cancelAllForAgent(agentId);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
finally {
|
|
2960
|
+
// Phase 3b: Clear the executing context
|
|
2961
|
+
if (this.currentExecutingBackgroundAgentId === agentId) {
|
|
2962
|
+
this.currentExecutingBackgroundAgentId = null;
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
})(),
|
|
2966
|
+
};
|
|
2967
|
+
// Register the background session
|
|
2968
|
+
this.startSession(session);
|
|
2969
|
+
this.ui.setBackgroundAgentCount(this.getBackgroundSessions().length);
|
|
2970
|
+
// Wait for completion (but caller doesn't await this method)
|
|
2971
|
+
await session.promise;
|
|
2972
|
+
this.endSession(agentId);
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* Handle pending delegation after agent finishes.
|
|
2976
|
+
* If a delegation was approved during the agent run, switch to the
|
|
2977
|
+
* target agent and inject the task as a user message.
|
|
2978
|
+
*/
|
|
2979
|
+
async handlePendingDelegation() {
|
|
2980
|
+
if (!this.onPendingDelegation || !this.team)
|
|
2981
|
+
return;
|
|
2982
|
+
const pending = this.onPendingDelegation();
|
|
2983
|
+
if (!pending)
|
|
2984
|
+
return;
|
|
2985
|
+
const { agentId, task } = pending;
|
|
2986
|
+
// Validate target agent still exists
|
|
2987
|
+
if (!this.team.has(agentId)) {
|
|
2988
|
+
this.ui.print({
|
|
2989
|
+
type: 'error',
|
|
2990
|
+
message: `Delegation failed: Agent '${agentId}' no longer exists in team.`,
|
|
2991
|
+
});
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
// Switch to target agent (no demotion — delegating agent just finished, no session tracking needed)
|
|
2995
|
+
try {
|
|
2996
|
+
const { teamAgent } = await this.switchAgent(agentId, {
|
|
2997
|
+
demoteCurrentIfBusy: false,
|
|
2998
|
+
trackSession: false,
|
|
2999
|
+
});
|
|
3000
|
+
this.ui.print({
|
|
3001
|
+
type: 'info',
|
|
3002
|
+
message: `Delegating to $${agentId} (${teamAgent.displayName})...`,
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
catch (err) {
|
|
3006
|
+
this.ui.print({
|
|
3007
|
+
type: 'error',
|
|
3008
|
+
message: `Failed to switch to $${agentId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
3009
|
+
});
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
if (!this.agent) {
|
|
3013
|
+
this.ui.print({
|
|
3014
|
+
type: 'error',
|
|
3015
|
+
message: `Agent $${agentId} is not initialized.`,
|
|
3016
|
+
});
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
// Inject the task as a user message and run the agent
|
|
3020
|
+
this.ui.print({
|
|
3021
|
+
type: 'user-message',
|
|
3022
|
+
text: task,
|
|
3023
|
+
});
|
|
3024
|
+
// Run the target agent with the delegated task
|
|
3025
|
+
await this.runAgentReal(task);
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Auto-save the current session and team state.
|
|
3029
|
+
* Called after each successful agent response.
|
|
3030
|
+
*
|
|
3031
|
+
* Saves two things:
|
|
3032
|
+
* 1. Default agent's session state (for /resume - conversation history)
|
|
3033
|
+
* 2. Team state (for team persistence - all agent states including roles)
|
|
3034
|
+
*/
|
|
3035
|
+
async autoSaveSession() {
|
|
3036
|
+
if (!this.team)
|
|
3037
|
+
return;
|
|
3038
|
+
try {
|
|
3039
|
+
// Save default agent's session for /resume functionality
|
|
3040
|
+
const defaultAgent = this.team.get('default')?.agent;
|
|
3041
|
+
if (defaultAgent) {
|
|
3042
|
+
const state = defaultAgent.serialize();
|
|
3043
|
+
await saveCurrentSession(state);
|
|
3044
|
+
}
|
|
3045
|
+
// Save team state for team persistence (all agents including custom ones)
|
|
3046
|
+
// Only save if team has more than just the default agent, or if any agent has history
|
|
3047
|
+
const teamAgents = this.team.getAll();
|
|
3048
|
+
const hasNonDefaultAgents = teamAgents.some(ta => ta.id !== 'default');
|
|
3049
|
+
const hasAnyHistory = teamAgents.some(ta => {
|
|
3050
|
+
const agent = ta.agent;
|
|
3051
|
+
if (!agent)
|
|
3052
|
+
return false;
|
|
3053
|
+
const state = agent.serialize();
|
|
3054
|
+
return state.messages.length > 0;
|
|
3055
|
+
});
|
|
3056
|
+
if (hasNonDefaultAgents || hasAnyHistory) {
|
|
3057
|
+
saveCurrentTeam(this.team);
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
catch {
|
|
3061
|
+
// Silently fail - session/team saving is best-effort
|
|
3062
|
+
}
|
|
624
3063
|
}
|
|
625
3064
|
formatToolParams(input) {
|
|
626
3065
|
if (!input)
|
|
@@ -633,12 +3072,12 @@ export class ReplV2 {
|
|
|
633
3072
|
if ('pattern' in input)
|
|
634
3073
|
return String(input.pattern);
|
|
635
3074
|
if ('command' in input)
|
|
636
|
-
return String(input.command)
|
|
3075
|
+
return truncate(String(input.command), 50);
|
|
637
3076
|
// Default: show first param
|
|
638
3077
|
const keys = Object.keys(input);
|
|
639
3078
|
if (keys.length > 0) {
|
|
640
3079
|
const val = String(input[keys[0]]);
|
|
641
|
-
return val
|
|
3080
|
+
return truncate(val, 50);
|
|
642
3081
|
}
|
|
643
3082
|
return '';
|
|
644
3083
|
}
|
|
@@ -660,7 +3099,12 @@ export class ReplV2 {
|
|
|
660
3099
|
this.currentSession.currentAction = action;
|
|
661
3100
|
}
|
|
662
3101
|
};
|
|
663
|
-
|
|
3102
|
+
// Get active agent ID for session tracking
|
|
3103
|
+
const activeAgentId = this.team?.getActive().id ?? 'default';
|
|
3104
|
+
// Create session object
|
|
3105
|
+
const session = {
|
|
3106
|
+
agentId: activeAgentId,
|
|
3107
|
+
isBackground: false, // Foreground execution
|
|
664
3108
|
abortController,
|
|
665
3109
|
currentAction: null,
|
|
666
3110
|
promise: (async () => {
|
|
@@ -673,7 +3117,7 @@ export class ReplV2 {
|
|
|
673
3117
|
if (signal.aborted)
|
|
674
3118
|
return;
|
|
675
3119
|
const response = `I understand you want me to help with "${userMessage}". Let me analyze this.`;
|
|
676
|
-
this.ui.print({ type: 'agent-text', text: response });
|
|
3120
|
+
this.ui.print({ type: 'agent-text', text: response, expression: this.getActiveAgentMascot() });
|
|
677
3121
|
// 2. Add todos
|
|
678
3122
|
this.ui.setTodos([
|
|
679
3123
|
{ content: 'Analyze the request', status: 'completed' },
|
|
@@ -719,6 +3163,7 @@ export class ReplV2 {
|
|
|
719
3163
|
expression: 'success',
|
|
720
3164
|
});
|
|
721
3165
|
this.ui.setAgentRunning(false);
|
|
3166
|
+
this.ui.setThinking(false); // Ensure thinking indicator is cleared
|
|
722
3167
|
// NOTE: Don't clear todos - let them persist to show final state
|
|
723
3168
|
// Notify that agent has finished (for applying deferred suggestions)
|
|
724
3169
|
this.onAgentFinish?.();
|
|
@@ -728,12 +3173,16 @@ export class ReplV2 {
|
|
|
728
3173
|
this.ui.print({ type: 'error', message: String(err) });
|
|
729
3174
|
}
|
|
730
3175
|
this.ui.setAgentRunning(false);
|
|
3176
|
+
this.ui.setThinking(false); // Ensure thinking indicator is cleared
|
|
731
3177
|
this.onAgentFinish?.();
|
|
732
3178
|
}
|
|
733
3179
|
})(),
|
|
734
3180
|
};
|
|
735
|
-
|
|
736
|
-
this.
|
|
3181
|
+
// Register the session (foreground)
|
|
3182
|
+
this.startSession(session);
|
|
3183
|
+
// Wait for session to complete
|
|
3184
|
+
await session.promise;
|
|
3185
|
+
this.endSession(activeAgentId);
|
|
737
3186
|
}
|
|
738
3187
|
async sleep(ms, signal) {
|
|
739
3188
|
return new Promise((resolve) => {
|
|
@@ -745,15 +3194,83 @@ export class ReplV2 {
|
|
|
745
3194
|
});
|
|
746
3195
|
}
|
|
747
3196
|
// ===========================================================================
|
|
3197
|
+
// Turn Summary
|
|
3198
|
+
// ===========================================================================
|
|
3199
|
+
/**
|
|
3200
|
+
* Print per-turn summary (tokens, duration, tool calls)
|
|
3201
|
+
*/
|
|
3202
|
+
printTurnSummary() {
|
|
3203
|
+
const metrics = this.ui.getTurnMetrics();
|
|
3204
|
+
// Only print summary if there were tokens used
|
|
3205
|
+
if (metrics.inputTokens === 0 && metrics.outputTokens === 0) {
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
const formatDuration = (ms) => {
|
|
3209
|
+
const seconds = Math.floor(ms / 1000);
|
|
3210
|
+
if (seconds < 60)
|
|
3211
|
+
return `${String(seconds)}s`;
|
|
3212
|
+
const minutes = Math.floor(seconds / 60);
|
|
3213
|
+
const remainingSeconds = seconds % 60;
|
|
3214
|
+
return `${String(minutes)}m ${String(remainingSeconds)}s`;
|
|
3215
|
+
};
|
|
3216
|
+
// Build summary parts
|
|
3217
|
+
const parts = [];
|
|
3218
|
+
parts.push(formatDuration(metrics.durationMs));
|
|
3219
|
+
// Token display with optional thinking/cache tokens
|
|
3220
|
+
let tokenDisplay = `↓ ${formatTokens(metrics.inputTokens)} ↑ ${formatTokens(metrics.outputTokens)}`;
|
|
3221
|
+
const extras = [];
|
|
3222
|
+
if (metrics.thinkingTokens > 0) {
|
|
3223
|
+
extras.push(`think: ${formatTokens(metrics.thinkingTokens)}`);
|
|
3224
|
+
}
|
|
3225
|
+
if (metrics.cacheReadTokens > 0) {
|
|
3226
|
+
extras.push(`cache: ${formatTokens(metrics.cacheReadTokens)}`);
|
|
3227
|
+
}
|
|
3228
|
+
if (extras.length > 0) {
|
|
3229
|
+
tokenDisplay += ` (${extras.join(', ')})`;
|
|
3230
|
+
}
|
|
3231
|
+
parts.push(tokenDisplay);
|
|
3232
|
+
// Add token breakdown
|
|
3233
|
+
const cacheInfo = formatCacheInfo();
|
|
3234
|
+
if (cacheInfo) {
|
|
3235
|
+
// Show cache info if available
|
|
3236
|
+
parts.push(cacheInfo);
|
|
3237
|
+
}
|
|
3238
|
+
else {
|
|
3239
|
+
// Show component breakdown
|
|
3240
|
+
const breakdown = formatBreakdownCompact();
|
|
3241
|
+
if (breakdown) {
|
|
3242
|
+
parts.push(breakdown);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
if (metrics.toolCalls > 0) {
|
|
3246
|
+
parts.push(`${String(metrics.toolCalls)} tool${metrics.toolCalls > 1 ? 's' : ''}`);
|
|
3247
|
+
}
|
|
3248
|
+
// DEBUG: Show payload debug info (chars sent to provider)
|
|
3249
|
+
if (metrics.debugPayload) {
|
|
3250
|
+
const { systemChars, contentsChars, toolsChars } = metrics.debugPayload;
|
|
3251
|
+
const totalChars = systemChars + contentsChars + toolsChars;
|
|
3252
|
+
const estTokens = Math.ceil(totalChars / 4);
|
|
3253
|
+
// Format: [dbg 2calls sent:123k (~31k est) sys:10k cont:100k tools:13k]
|
|
3254
|
+
parts.push(`[dbg ${String(metrics.apiCalls)}calls sent:${formatTokens(totalChars)} (~${formatTokens(estTokens)} est) sys:${formatTokens(systemChars)} cont:${formatTokens(contentsChars)} tools:${formatTokens(toolsChars)}]`);
|
|
3255
|
+
}
|
|
3256
|
+
// Print as muted info line
|
|
3257
|
+
this.ui.print({ type: 'turn-summary', parts });
|
|
3258
|
+
}
|
|
3259
|
+
// ===========================================================================
|
|
748
3260
|
// Session Management
|
|
749
3261
|
// ===========================================================================
|
|
3262
|
+
/**
|
|
3263
|
+
* Cancel the foreground session.
|
|
3264
|
+
* Background agents continue running.
|
|
3265
|
+
*/
|
|
750
3266
|
cancelSession() {
|
|
751
3267
|
const action = this.currentSession?.currentAction ?? undefined;
|
|
752
|
-
if (this.currentSession) {
|
|
3268
|
+
if (this.currentSession && this.foregroundAgentId) {
|
|
753
3269
|
this.currentSession.abortController.abort();
|
|
754
|
-
this.
|
|
3270
|
+
this.endSession(this.foregroundAgentId);
|
|
755
3271
|
}
|
|
756
3272
|
this.ui.setAgentRunning(false);
|
|
3273
|
+
this.ui.setThinking(false); // Ensure thinking indicator is cleared
|
|
757
3274
|
this.ui.setTodos([]);
|
|
758
3275
|
this.ui.clearQueue();
|
|
759
3276
|
this.ui.print({ type: 'interrupted', action, suggestion: 'what should I do instead?' });
|
|
@@ -765,10 +3282,16 @@ export class ReplV2 {
|
|
|
765
3282
|
// Only run main() when executed directly (not when imported)
|
|
766
3283
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
767
3284
|
const repl = new ReplV2();
|
|
768
|
-
repl.start();
|
|
769
3285
|
// Handle SIGINT
|
|
770
3286
|
process.on('SIGINT', () => {
|
|
771
3287
|
console.log('\n\nInterrupted\n');
|
|
772
3288
|
process.exit(0);
|
|
773
3289
|
});
|
|
3290
|
+
try {
|
|
3291
|
+
repl.start();
|
|
3292
|
+
}
|
|
3293
|
+
catch (err) {
|
|
3294
|
+
console.error('Failed to start REPL:', err);
|
|
3295
|
+
process.exit(1);
|
|
3296
|
+
}
|
|
774
3297
|
}
|