@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/ui/terminal-ui.js
CHANGED
|
@@ -14,93 +14,23 @@
|
|
|
14
14
|
* - All output goes through TerminalUI (no external console.log)
|
|
15
15
|
*/
|
|
16
16
|
import { EventEmitter } from 'events';
|
|
17
|
-
import
|
|
18
|
-
import * as terminal from './terminal.js';
|
|
19
|
-
import { getPhysicalLineCount, getVisibleLength } from './line-utils.js';
|
|
17
|
+
import { getVisibleLength } from './line-utils.js';
|
|
20
18
|
import { getStyles } from '../themes/index.js';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Mascot Expressions
|
|
37
|
-
// =============================================================================
|
|
38
|
-
/**
|
|
39
|
-
* Inline mascot expressions for agent identity and state feedback.
|
|
40
|
-
* Used to prefix agent messages and in spinner animations.
|
|
41
|
-
*/
|
|
42
|
-
export const MASCOT = {
|
|
43
|
-
// Core expressions
|
|
44
|
-
neutral: '[•_•]', // Default state - regular messages
|
|
45
|
-
thinking: '[°_°]', // Processing/thinking
|
|
46
|
-
searching: '[◐_◐]', // Searching/scanning files
|
|
47
|
-
success: '[^_^]', // Task completed successfully
|
|
48
|
-
error: '[×_×]', // Error occurred
|
|
49
|
-
confused: '[?_?]', // Needs clarification
|
|
50
|
-
working: '[•̀_•́]', // Actively working on task
|
|
51
|
-
// CRT monitor animation frames (subtle scanner effect)
|
|
52
|
-
crt: ['[░░░]', '[▒░░]', '[░▒░]', '[░░▒]', '[░▒░]', '[▒░░]'],
|
|
53
|
-
};
|
|
54
|
-
// =============================================================================
|
|
55
|
-
// Fuzzy Matching
|
|
56
|
-
// =============================================================================
|
|
57
|
-
/**
|
|
58
|
-
* Calculate fuzzy match score for a query against a target string.
|
|
59
|
-
* Higher score = better match. Returns -1 if no match.
|
|
60
|
-
*/
|
|
61
|
-
function fuzzyMatchScore(query, target) {
|
|
62
|
-
const queryLower = query.toLowerCase();
|
|
63
|
-
const targetLower = target.toLowerCase();
|
|
64
|
-
// Exact prefix match - highest priority (score 1000+)
|
|
65
|
-
if (targetLower.startsWith(queryLower)) {
|
|
66
|
-
return 1000 + (100 - target.length); // Shorter commands rank higher
|
|
67
|
-
}
|
|
68
|
-
// Contiguous substring match - high priority (score 500+)
|
|
69
|
-
if (targetLower.includes(queryLower)) {
|
|
70
|
-
const index = targetLower.indexOf(queryLower);
|
|
71
|
-
return 500 + (100 - index); // Earlier matches rank higher
|
|
72
|
-
}
|
|
73
|
-
// Fuzzy match - characters appear in order (score 100+)
|
|
74
|
-
let queryIdx = 0;
|
|
75
|
-
let consecutiveBonus = 0;
|
|
76
|
-
let lastMatchIdx = -1;
|
|
77
|
-
for (let i = 0; i < targetLower.length && queryIdx < queryLower.length; i++) {
|
|
78
|
-
if (targetLower[i] === queryLower[queryIdx]) {
|
|
79
|
-
if (lastMatchIdx === i - 1) {
|
|
80
|
-
consecutiveBonus += 10;
|
|
81
|
-
}
|
|
82
|
-
lastMatchIdx = i;
|
|
83
|
-
queryIdx++;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (queryIdx === queryLower.length) {
|
|
87
|
-
return 100 + consecutiveBonus + (100 - target.length);
|
|
88
|
-
}
|
|
89
|
-
return -1;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Filter and rank commands matching input using fuzzy matching
|
|
93
|
-
*/
|
|
94
|
-
function filterCommands(input, commands) {
|
|
95
|
-
const scored = commands
|
|
96
|
-
.map((cmd) => ({
|
|
97
|
-
cmd,
|
|
98
|
-
score: fuzzyMatchScore(input, cmd.command),
|
|
99
|
-
}))
|
|
100
|
-
.filter((item) => item.score >= 0);
|
|
101
|
-
scored.sort((a, b) => b.score - a.score);
|
|
102
|
-
return scored.map((item) => item.cmd);
|
|
103
|
-
}
|
|
19
|
+
import { renderItem } from './terminal-render-item.js';
|
|
20
|
+
import { TurnMetrics } from './turn-metrics.js';
|
|
21
|
+
import { StatusBarController } from './status-bar-controller.js';
|
|
22
|
+
import { ConversationStore } from './conversation-store.js';
|
|
23
|
+
import { AutocompleteController } from './autocomplete-controller.js';
|
|
24
|
+
import { InputController } from './input-controller.js';
|
|
25
|
+
import { LiveRegionFacade } from './live-region-facade.js';
|
|
26
|
+
import { OverlayManager } from './overlay-manager.js';
|
|
27
|
+
import { FooterRenderer } from './footer-renderer.js';
|
|
28
|
+
import { KeyboardHandler } from './keyboard-handler.js';
|
|
29
|
+
import { DEFAULT_TERMINAL_UI_CONFIG, } from './terminal-types.js';
|
|
30
|
+
export { DEFAULT_TERMINAL_UI_CONFIG, MASCOT } from './terminal-types.js';
|
|
31
|
+
import { getPendingRequestsManager } from '../multi-agent/index.js';
|
|
32
|
+
import { getSessionRegistry } from '../multi-agent/session-registry.js';
|
|
33
|
+
import { getNotificationManager } from '../multi-agent/notification-manager.js';
|
|
104
34
|
// =============================================================================
|
|
105
35
|
// Footer V2 Class
|
|
106
36
|
// =============================================================================
|
|
@@ -109,68 +39,120 @@ export class TerminalUI extends EventEmitter {
|
|
|
109
39
|
promptPrefix;
|
|
110
40
|
promptPrefixLen;
|
|
111
41
|
config;
|
|
112
|
-
// Conversation history
|
|
113
|
-
|
|
114
|
-
// Input state (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
42
|
+
// Conversation history, restored history, and filtering
|
|
43
|
+
conversation = new ConversationStore();
|
|
44
|
+
// Input state (delegated to InputController)
|
|
45
|
+
input = new InputController();
|
|
46
|
+
get lines() { return this.input.lines; }
|
|
47
|
+
set lines(v) { this.input.lines = v; }
|
|
48
|
+
get currentLine() { return this.input.currentLine; }
|
|
49
|
+
set currentLine(v) { this.input.currentLine = v; }
|
|
50
|
+
get cursorPos() { return this.input.cursorPos; }
|
|
51
|
+
set cursorPos(v) { this.input.cursorPos = v; }
|
|
52
|
+
get suggestion() { return this.input.suggestion; }
|
|
53
|
+
set suggestion(v) { this.input.suggestion = v; }
|
|
54
|
+
get queuedInputs() { return this.input.queuedInputs; }
|
|
55
|
+
set queuedInputs(v) { this.input.queuedInputs = v; }
|
|
118
56
|
mode;
|
|
119
57
|
projectName = null;
|
|
120
58
|
todos = [];
|
|
59
|
+
// Plan mode state
|
|
60
|
+
activePlanId = null;
|
|
61
|
+
activePlanName = null;
|
|
121
62
|
spinnerText = null;
|
|
122
|
-
spinnerFrame = 0;
|
|
123
63
|
agentRunning = false;
|
|
124
|
-
queuedInputs = [];
|
|
125
64
|
agentMessageQueue = [];
|
|
126
65
|
currentTool = null;
|
|
66
|
+
// Per-turn token and tool call tracking
|
|
67
|
+
turnMetrics = new TurnMetrics();
|
|
127
68
|
// LiveRegion for running tools (bash, subagents)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// File autocomplete state (for @paths)
|
|
137
|
-
fileAutocomplete = {
|
|
138
|
-
active: false,
|
|
139
|
-
matches: [],
|
|
140
|
-
selectedIndex: 0,
|
|
141
|
-
scrollOffset: 0,
|
|
142
|
-
partial: '',
|
|
143
|
-
};
|
|
144
|
-
// History state
|
|
145
|
-
history = [];
|
|
146
|
-
historyIndex = -1;
|
|
147
|
-
savedInput = '';
|
|
69
|
+
live = new LiveRegionFacade({
|
|
70
|
+
requestRender: () => { this.needsRender = true; },
|
|
71
|
+
print: (item) => { this.print(item); },
|
|
72
|
+
emit: (event, ...args) => { this.emit(event, ...args); },
|
|
73
|
+
setSpinnerText: (text) => { this.spinnerText = text; },
|
|
74
|
+
});
|
|
75
|
+
// Autocomplete controller (command, file, agent)
|
|
76
|
+
ac = new AutocompleteController();
|
|
148
77
|
// Render tracking
|
|
149
|
-
lastRenderHeight = 0;
|
|
150
|
-
cursorLineFromBottom = 0;
|
|
151
78
|
isRunning = false;
|
|
152
79
|
isPaused = false;
|
|
153
80
|
renderTimer = null;
|
|
154
81
|
needsRender = false;
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
// Ghost text suggestion
|
|
158
|
-
suggestion = null;
|
|
82
|
+
// Active team agent (for multi-agent footer display)
|
|
83
|
+
activeTeamAgent = null;
|
|
159
84
|
// Todo visibility (Ctrl+T to toggle)
|
|
160
85
|
showTodos = true;
|
|
161
86
|
// View mode (Ctrl+O to toggle verbose view)
|
|
162
87
|
// normal: compact output
|
|
163
88
|
// verbose-temp: temporarily show last N items verbose (any key returns to normal)
|
|
164
89
|
viewMode = 'normal';
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
90
|
+
// Status bar navigation (down arrow into footer indicators)
|
|
91
|
+
statusBar = new StatusBarController({
|
|
92
|
+
requestRender: () => { this.needsRender = true; },
|
|
93
|
+
emit: (event, ...args) => { this.emit(event, ...args); },
|
|
94
|
+
});
|
|
95
|
+
// Overlay lifecycle (stack, resolvers, render state, print buffer)
|
|
96
|
+
overlay = new OverlayManager({
|
|
97
|
+
clearFooter: () => { this.footer.clear(); },
|
|
98
|
+
renderFooter: () => { this.footer.render(); },
|
|
99
|
+
print: (item) => { this.print(item); },
|
|
100
|
+
renderItemToConsole: (item) => { this.renderItemToConsole(item); },
|
|
101
|
+
});
|
|
102
|
+
// Footer renderer (clear, render, spinner animation, line builders)
|
|
103
|
+
footer = new FooterRenderer({
|
|
104
|
+
isFooterRenderAllowed: () => this.isRunning && !this.isPaused && !this.overlay.hasActiveOverlay(),
|
|
105
|
+
getRenderData: () => ({
|
|
106
|
+
liveRegion: this.live.region,
|
|
107
|
+
liveVerbose: this.config.verbose,
|
|
108
|
+
agentRunning: this.agentRunning,
|
|
109
|
+
currentTool: this.currentTool,
|
|
110
|
+
spinnerText: this.spinnerText,
|
|
111
|
+
activeBashInfo: this.live.getActiveBashInfo(),
|
|
112
|
+
turnMetrics: this.turnMetrics,
|
|
113
|
+
todos: this.todos,
|
|
114
|
+
showTodos: this.showTodos,
|
|
115
|
+
lines: this.lines,
|
|
116
|
+
currentLine: this.currentLine,
|
|
117
|
+
cursorPos: this.cursorPos,
|
|
118
|
+
queuedInputs: this.queuedInputs,
|
|
119
|
+
suggestion: this.suggestion,
|
|
120
|
+
promptPrefix: this.promptPrefix,
|
|
121
|
+
promptPrefixLen: this.promptPrefixLen,
|
|
122
|
+
ac: this.ac,
|
|
123
|
+
statusBar: this.statusBar,
|
|
124
|
+
mode: this.mode,
|
|
125
|
+
activePlanName: this.activePlanName,
|
|
126
|
+
activeTeamAgent: this.activeTeamAgent,
|
|
127
|
+
projectName: this.projectName,
|
|
128
|
+
conversationFilterActive: this.conversation.isFilterActive,
|
|
129
|
+
hasRestoredHistory: this.conversation.hasRestoredHistory(),
|
|
130
|
+
config: this.config,
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
// Keyboard input handler (capture, escape, overlay routing, keypress dispatch)
|
|
134
|
+
keyboard = new KeyboardHandler({
|
|
135
|
+
isGuardActive: () => !this.isRunning || this.isPaused,
|
|
136
|
+
isAgentRunning: () => this.agentRunning,
|
|
137
|
+
getViewMode: () => this.viewMode,
|
|
138
|
+
setViewMode: (mode) => { this.viewMode = mode; },
|
|
139
|
+
requestRender: () => { this.needsRender = true; },
|
|
140
|
+
emit: (event, ...args) => { this.emit(event, ...args); },
|
|
141
|
+
input: this.input,
|
|
142
|
+
ac: this.ac,
|
|
143
|
+
overlay: this.overlay,
|
|
144
|
+
statusBar: this.statusBar,
|
|
145
|
+
getActiveOverlay: () => this.overlay.getActiveOverlay(),
|
|
146
|
+
hasActiveOverlay: () => this.overlay.hasActiveOverlay(),
|
|
147
|
+
hasActiveBash: () => this.live.hasActiveBash(),
|
|
148
|
+
backgroundBashCommand: () => { this.backgroundBashCommand(); },
|
|
149
|
+
hasRestoredHistory: () => this.conversation.hasRestoredHistory(),
|
|
150
|
+
showRestoredHistory: () => { this.showRestoredHistory(); },
|
|
151
|
+
toggleVerboseView: () => { this.toggleVerboseView(); },
|
|
152
|
+
toggleLiveRegionExpanded: () => { this.toggleLiveRegionExpanded(); },
|
|
153
|
+
toggleTodos: () => { this.toggleTodos(); },
|
|
154
|
+
reRenderConversationVerbose: (verbose) => { this.reRenderConversationVerbose(verbose); },
|
|
155
|
+
});
|
|
174
156
|
constructor(options = {}) {
|
|
175
157
|
super();
|
|
176
158
|
const s = getStyles();
|
|
@@ -180,38 +162,53 @@ export class TerminalUI extends EventEmitter {
|
|
|
180
162
|
this.config = { ...DEFAULT_TERMINAL_UI_CONFIG, ...options.config };
|
|
181
163
|
}
|
|
182
164
|
// ===========================================================================
|
|
183
|
-
//
|
|
165
|
+
// Status bar navigation (delegated to StatusBarController)
|
|
184
166
|
// ===========================================================================
|
|
185
|
-
/**
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
/** Get the current line content */
|
|
190
|
-
getCurrentLineContent() {
|
|
191
|
-
return this.lines[this.currentLine];
|
|
167
|
+
/** Set background agent count (called by REPL to keep UI in sync). */
|
|
168
|
+
setBackgroundAgentCount(count) {
|
|
169
|
+
this.statusBar.setBackgroundAgentCount(count);
|
|
192
170
|
}
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
this.
|
|
196
|
-
this.currentLine = 0;
|
|
197
|
-
this.cursorPos = 0;
|
|
171
|
+
/** Set MCP loading state for status bar indicator. */
|
|
172
|
+
setMCPLoading(loading, toolCount) {
|
|
173
|
+
this.statusBar.setMCPLoading(loading, toolCount);
|
|
198
174
|
}
|
|
199
175
|
// ===========================================================================
|
|
200
176
|
// Lifecycle
|
|
201
177
|
// ===========================================================================
|
|
202
|
-
start() {
|
|
178
|
+
start(skipInitialRender) {
|
|
203
179
|
if (this.isRunning)
|
|
204
180
|
return;
|
|
205
181
|
this.isRunning = true;
|
|
206
182
|
this.isPaused = false;
|
|
207
183
|
// Start keyboard input handling
|
|
208
|
-
this.
|
|
209
|
-
// Initial render
|
|
210
|
-
|
|
184
|
+
this.keyboard.startCapture();
|
|
185
|
+
// Initial render (skip when a startup overlay will be shown immediately)
|
|
186
|
+
if (!skipInitialRender) {
|
|
187
|
+
this.footer.render();
|
|
188
|
+
}
|
|
189
|
+
// Phase 3b: Listen for pending requests changes to trigger re-render
|
|
190
|
+
const pendingManager = getPendingRequestsManager();
|
|
191
|
+
pendingManager.on('count-changed', () => {
|
|
192
|
+
this.needsRender = true;
|
|
193
|
+
});
|
|
194
|
+
// Multi-terminal: Listen for session count changes (other terminals joining/leaving)
|
|
195
|
+
const registry = getSessionRegistry();
|
|
196
|
+
if (registry) {
|
|
197
|
+
registry.onCountChange(() => {
|
|
198
|
+
this.needsRender = true;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// Cross-session notifications: re-render when count changes
|
|
202
|
+
const notifMgr = getNotificationManager();
|
|
203
|
+
if (notifMgr) {
|
|
204
|
+
notifMgr.on('count-changed', () => {
|
|
205
|
+
this.needsRender = true;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
211
208
|
// Start render loop (60ms = ~16fps)
|
|
212
209
|
this.renderTimer = setInterval(() => {
|
|
213
210
|
if (this.needsRender && !this.isPaused) {
|
|
214
|
-
this.render();
|
|
211
|
+
this.footer.render();
|
|
215
212
|
this.needsRender = false;
|
|
216
213
|
}
|
|
217
214
|
}, 60);
|
|
@@ -226,11 +223,11 @@ export class TerminalUI extends EventEmitter {
|
|
|
226
223
|
this.renderTimer = null;
|
|
227
224
|
}
|
|
228
225
|
// Stop spinner
|
|
229
|
-
this.stopSpinnerAnimation();
|
|
226
|
+
this.footer.stopSpinnerAnimation();
|
|
230
227
|
// Stop keyboard capture
|
|
231
|
-
this.
|
|
228
|
+
this.keyboard.stopCapture();
|
|
232
229
|
// Clear footer
|
|
233
|
-
this.clear();
|
|
230
|
+
this.footer.clear();
|
|
234
231
|
}
|
|
235
232
|
// ===========================================================================
|
|
236
233
|
// Public API - Output (THE KEY METHODS)
|
|
@@ -245,222 +242,38 @@ export class TerminalUI extends EventEmitter {
|
|
|
245
242
|
*/
|
|
246
243
|
print(item) {
|
|
247
244
|
// Store in history for re-render capability
|
|
248
|
-
this.
|
|
245
|
+
this.conversation.addItem(item);
|
|
246
|
+
// Check if item should be shown based on current filter
|
|
247
|
+
if (!this.conversation.shouldShowItem(item)) {
|
|
248
|
+
return; // Filtered out - stored but not displayed
|
|
249
|
+
}
|
|
249
250
|
// If overlay is active, buffer the item to render later
|
|
250
251
|
// This prevents output from corrupting overlay cursor tracking
|
|
251
|
-
if (this.hasActiveOverlay()) {
|
|
252
|
-
this.
|
|
252
|
+
if (this.overlay.hasActiveOverlay()) {
|
|
253
|
+
this.overlay.bufferItem(item);
|
|
253
254
|
return;
|
|
254
255
|
}
|
|
255
256
|
// IMPORTANT: If needsRender is true, render FIRST to update lastRenderHeight.
|
|
256
257
|
// This prevents ghost lines when footer height changed (e.g., spinner started)
|
|
257
258
|
// but render loop hasn't fired yet. Without this, clear() uses stale height.
|
|
258
259
|
if (this.needsRender) {
|
|
259
|
-
this.render();
|
|
260
|
+
this.footer.render();
|
|
260
261
|
this.needsRender = false;
|
|
261
262
|
}
|
|
262
263
|
// Clear footer, render item, re-render footer
|
|
263
|
-
this.clear();
|
|
264
|
-
this.
|
|
265
|
-
this.render();
|
|
264
|
+
this.footer.clear();
|
|
265
|
+
this.renderItemToConsole(item);
|
|
266
|
+
this.footer.render();
|
|
266
267
|
}
|
|
267
268
|
/**
|
|
268
269
|
* Render a single item to the console.
|
|
269
270
|
* Config-aware: respects verbose, showMascot settings.
|
|
271
|
+
* Delegates to standalone renderItem() function.
|
|
270
272
|
*/
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
console.log('');
|
|
276
|
-
console.log(s.primaryBold('> ') + item.text);
|
|
277
|
-
console.log(''); // Trailing blank for separation
|
|
278
|
-
break;
|
|
279
|
-
case 'agent-text': {
|
|
280
|
-
// Render markdown for proper formatting (headers, lists, bold, code, etc.)
|
|
281
|
-
const rendered = renderMarkdown(item.text);
|
|
282
|
-
// Prefix agent messages with mascot expression (if enabled)
|
|
283
|
-
if (this.config.showMascot) {
|
|
284
|
-
const expr = item.expression ? MASCOT[item.expression] : MASCOT.neutral;
|
|
285
|
-
// Only prefix the first line with mascot
|
|
286
|
-
const lines = rendered.split('\n');
|
|
287
|
-
if (lines.length > 0) {
|
|
288
|
-
console.log(s.primary(expr) + s.muted(' > ') + lines[0]);
|
|
289
|
-
for (let i = 1; i < lines.length; i++) {
|
|
290
|
-
console.log(lines[i]);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
console.log(rendered);
|
|
296
|
-
}
|
|
297
|
-
console.log(''); // Trailing blank for separation
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
case 'thinking':
|
|
301
|
-
// Only show thinking in verbose mode
|
|
302
|
-
if (this.config.verbose) {
|
|
303
|
-
console.log(s.muted(`∴ Thinking…`));
|
|
304
|
-
console.log('');
|
|
305
|
-
// Indent thinking text
|
|
306
|
-
const lines = item.text.split('\n');
|
|
307
|
-
for (const line of lines) {
|
|
308
|
-
console.log(s.muted(` ${line}`));
|
|
309
|
-
}
|
|
310
|
-
console.log('');
|
|
311
|
-
}
|
|
312
|
-
// If not verbose, skip entirely (but still stored in history)
|
|
313
|
-
break;
|
|
314
|
-
case 'tool-start': {
|
|
315
|
-
// Truncate long params (e.g., long bash commands)
|
|
316
|
-
const maxLen = Math.min(60, terminal.getTerminalWidth() - 15);
|
|
317
|
-
let params = item.params;
|
|
318
|
-
// Handle multi-line params
|
|
319
|
-
const nlIdx = params.indexOf('\n');
|
|
320
|
-
if (nlIdx !== -1) {
|
|
321
|
-
const first = params.slice(0, nlIdx).trim();
|
|
322
|
-
const count = params.split('\n').length;
|
|
323
|
-
params = first.length > 0 ? `${first}… (${String(count)} lines)` : `(${String(count)} line script)`;
|
|
324
|
-
}
|
|
325
|
-
// Truncate if still too long
|
|
326
|
-
if (params.length > maxLen) {
|
|
327
|
-
params = params.slice(0, maxLen) + '…';
|
|
328
|
-
}
|
|
329
|
-
console.log(s.info(`● ${item.name}`) + s.muted(`(${params})`));
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
case 'tool-result': {
|
|
333
|
-
// Truncate long params (e.g., long bash commands)
|
|
334
|
-
const maxParamsLen = Math.min(60, terminal.getTerminalWidth() - 15);
|
|
335
|
-
let paramsDisplay = item.params;
|
|
336
|
-
// Handle multi-line params (show first line + count)
|
|
337
|
-
const newlineIdx = paramsDisplay.indexOf('\n');
|
|
338
|
-
if (newlineIdx !== -1) {
|
|
339
|
-
const firstLine = paramsDisplay.slice(0, newlineIdx).trim();
|
|
340
|
-
const lineCount = paramsDisplay.split('\n').length;
|
|
341
|
-
paramsDisplay = firstLine.length > 0
|
|
342
|
-
? `${firstLine}… (${String(lineCount)} lines)`
|
|
343
|
-
: `(${String(lineCount)} line script)`;
|
|
344
|
-
}
|
|
345
|
-
// Truncate if still too long
|
|
346
|
-
if (paramsDisplay.length > maxParamsLen) {
|
|
347
|
-
paramsDisplay = paramsDisplay.slice(0, maxParamsLen) + '…';
|
|
348
|
-
}
|
|
349
|
-
console.log(s.info(`● ${item.name}`) + s.muted(`(${paramsDisplay})`));
|
|
350
|
-
if (item.content) {
|
|
351
|
-
const lines = item.content.split('\n').filter((l) => l.length > 0);
|
|
352
|
-
const maxPreviewLines = 3;
|
|
353
|
-
if (this.config.verbose) {
|
|
354
|
-
// Verbose: show all lines
|
|
355
|
-
for (const line of lines) {
|
|
356
|
-
console.log(s.muted(` ⎿ ${line}`));
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
else if (lines.length > 0) {
|
|
360
|
-
// Compact: show first few lines + hidden count
|
|
361
|
-
const previewLines = lines.slice(0, maxPreviewLines);
|
|
362
|
-
const hiddenCount = lines.length - maxPreviewLines;
|
|
363
|
-
for (let i = 0; i < previewLines.length; i++) {
|
|
364
|
-
if (i === 0) {
|
|
365
|
-
console.log(s.muted(` ⎿ ${previewLines[i]}`));
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
console.log(s.muted(` ${previewLines[i]}`));
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
if (hiddenCount > 0) {
|
|
372
|
-
console.log(s.muted(` … +${String(hiddenCount)} lines`) + s.muted(' (ctrl+o to expand)'));
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
// No output lines
|
|
377
|
-
const expandHint = s.muted(' (ctrl+o to expand)');
|
|
378
|
-
if (item.success === false) {
|
|
379
|
-
console.log(s.error(` ⎿ ${item.summary}`) + expandHint);
|
|
380
|
-
}
|
|
381
|
-
else {
|
|
382
|
-
console.log(s.muted(` ⎿ ${item.summary}`) + expandHint);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
// No content, just show summary
|
|
388
|
-
if (item.success === false) {
|
|
389
|
-
console.log(s.error(` ⎿ ${item.summary}`));
|
|
390
|
-
}
|
|
391
|
-
else {
|
|
392
|
-
console.log(s.muted(` ⎿ ${item.summary}`));
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
console.log('');
|
|
396
|
-
break;
|
|
397
|
-
}
|
|
398
|
-
case 'tool-error': {
|
|
399
|
-
// Truncate long params
|
|
400
|
-
const maxErrLen = Math.min(60, terminal.getTerminalWidth() - 15);
|
|
401
|
-
let errParams = item.params;
|
|
402
|
-
const errNlIdx = errParams.indexOf('\n');
|
|
403
|
-
if (errNlIdx !== -1) {
|
|
404
|
-
const errFirst = errParams.slice(0, errNlIdx).trim();
|
|
405
|
-
const errCount = errParams.split('\n').length;
|
|
406
|
-
errParams = errFirst.length > 0 ? `${errFirst}… (${String(errCount)} lines)` : `(${String(errCount)} line script)`;
|
|
407
|
-
}
|
|
408
|
-
if (errParams.length > maxErrLen) {
|
|
409
|
-
errParams = errParams.slice(0, maxErrLen) + '…';
|
|
410
|
-
}
|
|
411
|
-
console.log(s.info(`● ${item.name}`) + s.muted(`(${errParams})`));
|
|
412
|
-
console.log(s.error(` ⎿ Error: ${item.error}`));
|
|
413
|
-
console.log('');
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
|
-
case 'interrupted': {
|
|
417
|
-
// Show what was ongoing (if provided)
|
|
418
|
-
if (item.action) {
|
|
419
|
-
console.log(s.info(`● ${item.action}`));
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
// No action means interrupted right after user message.
|
|
423
|
-
// Move cursor up to consume the trailing blank from user-message.
|
|
424
|
-
//
|
|
425
|
-
// ⚠️ WARNING: This cursor manipulation is fragile!
|
|
426
|
-
// It assumes the previous item was user-message which adds a trailing blank.
|
|
427
|
-
// If we add items that don't add trailing blanks, or if the rendering
|
|
428
|
-
// order changes, this could cause visual artifacts (overwriting content).
|
|
429
|
-
// If issues arise, consider tracking the last printed item type instead.
|
|
430
|
-
process.stdout.write('\x1b[1A'); // Move up one line
|
|
431
|
-
}
|
|
432
|
-
// Show interrupted line with mascot
|
|
433
|
-
const suggestion = item.suggestion ?? 'What should I do instead?';
|
|
434
|
-
console.log(s.warning(` ⎿ ${MASCOT.confused} Interrupted - ${suggestion}`));
|
|
435
|
-
console.log('');
|
|
436
|
-
// Also set as ghost text suggestion
|
|
437
|
-
this.setSuggestion(suggestion);
|
|
438
|
-
break;
|
|
439
|
-
}
|
|
440
|
-
case 'error':
|
|
441
|
-
console.log(s.error(`${MASCOT.error} ${item.message}`));
|
|
442
|
-
console.log('');
|
|
443
|
-
break;
|
|
444
|
-
case 'success':
|
|
445
|
-
console.log(s.success(`${MASCOT.success} ${item.message}`));
|
|
446
|
-
console.log('');
|
|
447
|
-
break;
|
|
448
|
-
case 'info':
|
|
449
|
-
console.log(s.info(item.message));
|
|
450
|
-
console.log('');
|
|
451
|
-
break;
|
|
452
|
-
case 'warning':
|
|
453
|
-
console.log(s.warning(item.message));
|
|
454
|
-
console.log('');
|
|
455
|
-
break;
|
|
456
|
-
case 'raw':
|
|
457
|
-
console.log(item.text);
|
|
458
|
-
break;
|
|
459
|
-
case 'raw-lines':
|
|
460
|
-
for (const line of item.lines) {
|
|
461
|
-
console.log(line);
|
|
462
|
-
}
|
|
463
|
-
break;
|
|
273
|
+
renderItemToConsole(item) {
|
|
274
|
+
const suggestion = renderItem(item, this.config);
|
|
275
|
+
if (suggestion !== null) {
|
|
276
|
+
this.setSuggestion(suggestion);
|
|
464
277
|
}
|
|
465
278
|
}
|
|
466
279
|
/**
|
|
@@ -470,9 +283,9 @@ export class TerminalUI extends EventEmitter {
|
|
|
470
283
|
* @deprecated Prefer using print() with PrintableItem
|
|
471
284
|
*/
|
|
472
285
|
output(callback) {
|
|
473
|
-
this.clear();
|
|
286
|
+
this.footer.clear();
|
|
474
287
|
callback();
|
|
475
|
-
this.render();
|
|
288
|
+
this.footer.render();
|
|
476
289
|
}
|
|
477
290
|
// ===========================================================================
|
|
478
291
|
// Public API - State setters
|
|
@@ -481,11 +294,11 @@ export class TerminalUI extends EventEmitter {
|
|
|
481
294
|
const wasRunning = this.agentRunning;
|
|
482
295
|
this.agentRunning = running;
|
|
483
296
|
if (running && !wasRunning) {
|
|
484
|
-
this.startSpinnerAnimation();
|
|
297
|
+
this.footer.startSpinnerAnimation(() => { this.needsRender = true; });
|
|
485
298
|
// Let render loop handle it
|
|
486
299
|
}
|
|
487
300
|
else if (!running && wasRunning) {
|
|
488
|
-
this.stopSpinnerAnimation();
|
|
301
|
+
this.footer.stopSpinnerAnimation();
|
|
489
302
|
this.currentTool = null;
|
|
490
303
|
}
|
|
491
304
|
this.needsRender = true;
|
|
@@ -511,6 +324,51 @@ export class TerminalUI extends EventEmitter {
|
|
|
511
324
|
}
|
|
512
325
|
this.needsRender = true;
|
|
513
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Set whether extended thinking mode is active
|
|
329
|
+
* Shows "thinking" indicator in status line
|
|
330
|
+
*/
|
|
331
|
+
setThinking(thinking) {
|
|
332
|
+
this.turnMetrics.isThinking = thinking;
|
|
333
|
+
this.needsRender = true;
|
|
334
|
+
}
|
|
335
|
+
// ===========================================================================
|
|
336
|
+
// Token Tracking (per-turn metrics)
|
|
337
|
+
// ===========================================================================
|
|
338
|
+
/**
|
|
339
|
+
* Add input/output tokens for live display
|
|
340
|
+
* @param thinkingTokens - Optional thinking tokens (Gemini 2.5+ models)
|
|
341
|
+
* @param cacheReadTokens - Optional cache read tokens (Anthropic/Gemini)
|
|
342
|
+
* @param debugPayload - Optional debug payload sizes (char counts)
|
|
343
|
+
*/
|
|
344
|
+
addTokens(inputTokens, outputTokens, thinkingTokens, cacheReadTokens, debugPayload) {
|
|
345
|
+
this.turnMetrics.addTokens(inputTokens, outputTokens, thinkingTokens, cacheReadTokens, debugPayload);
|
|
346
|
+
this.needsRender = true;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Increment tool call counter
|
|
350
|
+
*/
|
|
351
|
+
incrementToolCalls() {
|
|
352
|
+
this.turnMetrics.incrementToolCalls();
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Increment API call counter (called on each LLM request completion)
|
|
356
|
+
*/
|
|
357
|
+
incrementApiCalls() {
|
|
358
|
+
this.turnMetrics.incrementApiCalls();
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Reset turn metrics (call when agent starts a new turn)
|
|
362
|
+
*/
|
|
363
|
+
resetTurnMetrics() {
|
|
364
|
+
this.turnMetrics.reset();
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Get current turn metrics
|
|
368
|
+
*/
|
|
369
|
+
getTurnMetrics() {
|
|
370
|
+
return this.turnMetrics.getSnapshot();
|
|
371
|
+
}
|
|
514
372
|
setMode(mode) {
|
|
515
373
|
this.mode = mode;
|
|
516
374
|
this.needsRender = true;
|
|
@@ -526,215 +384,117 @@ export class TerminalUI extends EventEmitter {
|
|
|
526
384
|
return this.projectName;
|
|
527
385
|
}
|
|
528
386
|
/**
|
|
529
|
-
* Set
|
|
387
|
+
* Set the active team agent (displayed in footer)
|
|
388
|
+
* Pass null to clear (default agent or no team)
|
|
530
389
|
*/
|
|
531
|
-
|
|
532
|
-
this.
|
|
533
|
-
this.needsRender = true;
|
|
534
|
-
}
|
|
535
|
-
getSuggestion() {
|
|
536
|
-
return this.suggestion;
|
|
537
|
-
}
|
|
538
|
-
clearSuggestion() {
|
|
539
|
-
this.suggestion = null;
|
|
390
|
+
setActiveTeamAgent(agent) {
|
|
391
|
+
this.activeTeamAgent = agent;
|
|
540
392
|
this.needsRender = true;
|
|
541
393
|
}
|
|
542
|
-
// ===========================================================================
|
|
543
|
-
// Public API - LiveRegion (subagents, bash commands)
|
|
544
|
-
// ===========================================================================
|
|
545
394
|
/**
|
|
546
|
-
*
|
|
395
|
+
* Get the active team agent
|
|
547
396
|
*/
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
id,
|
|
551
|
-
type: 'subagent',
|
|
552
|
-
status: 'running',
|
|
553
|
-
startTime: Date.now(),
|
|
554
|
-
agentType,
|
|
555
|
-
description,
|
|
556
|
-
toolCount: 0,
|
|
557
|
-
tokenCount: 0,
|
|
558
|
-
lastAction: '',
|
|
559
|
-
lastActionDetails: [],
|
|
560
|
-
};
|
|
561
|
-
this.liveRegion.addItem(item);
|
|
562
|
-
// Let render loop handle it (same as addBashCommand)
|
|
563
|
-
this.needsRender = true;
|
|
397
|
+
getActiveTeamAgent() {
|
|
398
|
+
return this.activeTeamAgent;
|
|
564
399
|
}
|
|
565
400
|
/**
|
|
566
|
-
*
|
|
401
|
+
* Set the callback to get available team agents for $ autocomplete
|
|
567
402
|
*/
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (item && item.type === 'subagent') {
|
|
571
|
-
const newAction = `${toolName}: ${summary}`;
|
|
572
|
-
// Add previous action to history (for expanded view)
|
|
573
|
-
const updatedDetails = item.lastAction
|
|
574
|
-
? [...item.lastActionDetails, item.lastAction]
|
|
575
|
-
: [...item.lastActionDetails];
|
|
576
|
-
this.liveRegion.updateItem(id, {
|
|
577
|
-
toolCount: item.toolCount + 1,
|
|
578
|
-
lastAction: newAction,
|
|
579
|
-
lastActionDetails: updatedDetails,
|
|
580
|
-
});
|
|
581
|
-
this.needsRender = true;
|
|
582
|
-
}
|
|
403
|
+
setAgentSuggestionsCallback(callback) {
|
|
404
|
+
this.ac.getAgentSuggestions = callback;
|
|
583
405
|
}
|
|
584
406
|
/**
|
|
585
|
-
*
|
|
407
|
+
* Set the callback to get available commands for / autocomplete
|
|
586
408
|
*/
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (item && item.type === 'subagent') {
|
|
590
|
-
this.liveRegion.updateItem(id, {
|
|
591
|
-
status: success ? 'done' : 'error',
|
|
592
|
-
tokenCount: tokenCount ?? item.tokenCount,
|
|
593
|
-
endTime: Date.now(),
|
|
594
|
-
});
|
|
595
|
-
this.liveRegion.completeItem(id, success ? 'done' : 'error');
|
|
596
|
-
this.needsRender = true;
|
|
597
|
-
}
|
|
409
|
+
setCommandsCallback(callback) {
|
|
410
|
+
this.ac.getCommands = callback;
|
|
598
411
|
}
|
|
599
412
|
/**
|
|
600
|
-
*
|
|
413
|
+
* Set the active plan (for Plan Mode display in footer)
|
|
601
414
|
*/
|
|
602
|
-
|
|
603
|
-
|
|
415
|
+
setActivePlan(planId, planName) {
|
|
416
|
+
this.activePlanId = planId;
|
|
417
|
+
this.activePlanName = planName;
|
|
604
418
|
this.needsRender = true;
|
|
605
|
-
return item;
|
|
606
419
|
}
|
|
607
420
|
/**
|
|
608
|
-
*
|
|
421
|
+
* Get the active plan info
|
|
609
422
|
*/
|
|
610
|
-
|
|
611
|
-
this.
|
|
612
|
-
this.needsRender = true;
|
|
423
|
+
getActivePlan() {
|
|
424
|
+
return { id: this.activePlanId, name: this.activePlanName };
|
|
613
425
|
}
|
|
614
426
|
/**
|
|
615
|
-
*
|
|
427
|
+
* Clear the active plan
|
|
616
428
|
*/
|
|
617
|
-
|
|
618
|
-
this.
|
|
429
|
+
clearActivePlan() {
|
|
430
|
+
this.activePlanId = null;
|
|
431
|
+
this.activePlanName = null;
|
|
619
432
|
this.needsRender = true;
|
|
620
433
|
}
|
|
621
434
|
/**
|
|
622
|
-
*
|
|
435
|
+
* Set ghost text suggestion (shown when input is empty)
|
|
623
436
|
*/
|
|
624
|
-
|
|
625
|
-
|
|
437
|
+
setSuggestion(text) {
|
|
438
|
+
this.input.suggestion = text;
|
|
439
|
+
this.needsRender = true;
|
|
626
440
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
441
|
+
getSuggestion() {
|
|
442
|
+
return this.input.suggestion;
|
|
443
|
+
}
|
|
444
|
+
clearSuggestion() {
|
|
445
|
+
this.input.suggestion = null;
|
|
446
|
+
this.needsRender = true;
|
|
632
447
|
}
|
|
633
448
|
// ===========================================================================
|
|
634
|
-
//
|
|
449
|
+
// Public API - LiveRegion (subagents, bash commands)
|
|
635
450
|
// ===========================================================================
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
451
|
+
addSubagent(id, agentType, description) {
|
|
452
|
+
this.live.addSubagent(id, agentType, description);
|
|
453
|
+
}
|
|
454
|
+
updateSubagentTool(id, toolName, summary) {
|
|
455
|
+
this.live.updateSubagentTool(id, toolName, summary);
|
|
456
|
+
}
|
|
457
|
+
completeSubagent(id, success, tokenCount, _error) {
|
|
458
|
+
this.live.completeSubagent(id, success, tokenCount, _error);
|
|
459
|
+
}
|
|
460
|
+
removeSubagent(id) {
|
|
461
|
+
return this.live.removeSubagent(id);
|
|
462
|
+
}
|
|
463
|
+
clearLiveRegion() {
|
|
464
|
+
this.live.clearLiveRegion();
|
|
465
|
+
}
|
|
466
|
+
toggleLiveRegionExpanded() {
|
|
467
|
+
this.live.toggleLiveRegionExpanded();
|
|
468
|
+
}
|
|
469
|
+
hasLiveItems() {
|
|
470
|
+
return this.live.hasLiveItems();
|
|
471
|
+
}
|
|
472
|
+
getLiveRegion() {
|
|
473
|
+
return this.live.getLiveRegion();
|
|
474
|
+
}
|
|
639
475
|
addBashCommand(id, command) {
|
|
640
|
-
|
|
641
|
-
id,
|
|
642
|
-
type: 'bash',
|
|
643
|
-
status: 'running',
|
|
644
|
-
startTime: Date.now(),
|
|
645
|
-
command,
|
|
646
|
-
output: [],
|
|
647
|
-
};
|
|
648
|
-
this.liveRegion.addItem(item);
|
|
649
|
-
this.needsRender = true;
|
|
476
|
+
this.live.addBashCommand(id, command);
|
|
650
477
|
}
|
|
651
|
-
/**
|
|
652
|
-
* Add output line to a bash command.
|
|
653
|
-
*/
|
|
654
478
|
updateBashOutput(id, line) {
|
|
655
|
-
|
|
656
|
-
if (item && item.type === 'bash') {
|
|
657
|
-
item.output.push(line);
|
|
658
|
-
this.liveRegion.updateItem(id, { output: [...item.output] });
|
|
659
|
-
this.needsRender = true;
|
|
660
|
-
}
|
|
479
|
+
this.live.updateBashOutput(id, line);
|
|
661
480
|
}
|
|
662
|
-
/**
|
|
663
|
-
* Mark a bash command as completed.
|
|
664
|
-
*/
|
|
665
481
|
completeBashCommand(id, exitCode) {
|
|
666
|
-
|
|
667
|
-
if (item && item.type === 'bash') {
|
|
668
|
-
this.liveRegion.updateItem(id, {
|
|
669
|
-
status: exitCode === 0 ? 'done' : 'error',
|
|
670
|
-
exitCode,
|
|
671
|
-
endTime: Date.now(),
|
|
672
|
-
});
|
|
673
|
-
this.liveRegion.completeItem(id, exitCode === 0 ? 'done' : 'error');
|
|
674
|
-
this.needsRender = true;
|
|
675
|
-
}
|
|
482
|
+
this.live.completeBashCommand(id, exitCode);
|
|
676
483
|
}
|
|
677
|
-
/**
|
|
678
|
-
* Remove a bash command and commit it to the scrolling zone.
|
|
679
|
-
*/
|
|
680
484
|
commitBashCommand(id) {
|
|
681
|
-
|
|
682
|
-
if (item && item.type === 'bash') {
|
|
683
|
-
const output = item.output.join('\n') || '(no output)';
|
|
684
|
-
const exitCode = item.exitCode ?? 0;
|
|
685
|
-
// Print the bash output to scrolling zone
|
|
686
|
-
this.print({
|
|
687
|
-
type: 'tool-result',
|
|
688
|
-
name: 'Bash',
|
|
689
|
-
params: item.command,
|
|
690
|
-
summary: exitCode === 0 ? 'Completed' : `Exit code ${String(exitCode)}`,
|
|
691
|
-
content: output,
|
|
692
|
-
success: exitCode === 0,
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
this.needsRender = true;
|
|
485
|
+
this.live.commitBashCommand(id);
|
|
696
486
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
* This is the atomic equivalent of commitBashCommand for subagents.
|
|
700
|
-
* The result content comes from tool_end, not from the LiveRegion item.
|
|
701
|
-
*/
|
|
702
|
-
commitSubagent(id, resultContent, success) {
|
|
703
|
-
// Remove from LiveRegion FIRST (no needsRender yet - that's the key!)
|
|
704
|
-
const item = this.liveRegion.removeItem(id);
|
|
705
|
-
if (item && item.type === 'subagent') {
|
|
706
|
-
// Build summary from item stats
|
|
707
|
-
const toolCount = item.toolCount;
|
|
708
|
-
const tokenCount = item.tokenCount;
|
|
709
|
-
const duration = item.endTime
|
|
710
|
-
? Math.round((item.endTime - item.startTime) / 1000)
|
|
711
|
-
: Math.round((Date.now() - item.startTime) / 1000);
|
|
712
|
-
const tokenStr = tokenCount > 0 ? `, ${this.formatTokens(tokenCount)} tokens` : '';
|
|
713
|
-
const summary = `${String(toolCount)} tool calls${tokenStr}, ${String(duration)}s`;
|
|
714
|
-
// Print atomically (uses current lastRenderHeight before it changes)
|
|
715
|
-
this.print({
|
|
716
|
-
type: 'tool-result',
|
|
717
|
-
name: item.agentType,
|
|
718
|
-
params: item.description,
|
|
719
|
-
summary,
|
|
720
|
-
content: resultContent.length > 200 ? resultContent : undefined,
|
|
721
|
-
success,
|
|
722
|
-
});
|
|
723
|
-
// Reset spinner text
|
|
724
|
-
this.spinnerText = null;
|
|
725
|
-
}
|
|
726
|
-
// Set needsRender AFTER print (same pattern as commitBashCommand)
|
|
727
|
-
this.needsRender = true;
|
|
728
|
-
return item;
|
|
487
|
+
backgroundBashCommand() {
|
|
488
|
+
this.live.backgroundBashCommand();
|
|
729
489
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
return
|
|
490
|
+
hasActiveBash() {
|
|
491
|
+
return this.live.hasActiveBash();
|
|
492
|
+
}
|
|
493
|
+
getActiveBashInfo() {
|
|
494
|
+
return this.live.getActiveBashInfo();
|
|
495
|
+
}
|
|
496
|
+
commitSubagent(id, resultContent, success) {
|
|
497
|
+
return this.live.commitSubagent(id, resultContent, success);
|
|
738
498
|
}
|
|
739
499
|
/**
|
|
740
500
|
* Toggle todo list visibility (Ctrl+T)
|
|
@@ -779,8 +539,7 @@ export class TerminalUI extends EventEmitter {
|
|
|
779
539
|
// Clear screen and scrollback buffer
|
|
780
540
|
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
781
541
|
// Reset footer state
|
|
782
|
-
this.
|
|
783
|
-
this.cursorLineFromBottom = 0;
|
|
542
|
+
this.footer.resetRenderState();
|
|
784
543
|
// Show mode indicator at top
|
|
785
544
|
if (verbose) {
|
|
786
545
|
console.log(s.info(`[Verbose View Mode] Press any key to return to normal view`));
|
|
@@ -789,8 +548,8 @@ export class TerminalUI extends EventEmitter {
|
|
|
789
548
|
// Re-render all items with temporary verbose override
|
|
790
549
|
const originalVerbose = this.config.verbose;
|
|
791
550
|
this.config.verbose = verbose;
|
|
792
|
-
for (const item of this.
|
|
793
|
-
this.
|
|
551
|
+
for (const item of this.conversation.history) {
|
|
552
|
+
this.renderItemToConsole(item);
|
|
794
553
|
}
|
|
795
554
|
// Restore original config
|
|
796
555
|
this.config.verbose = originalVerbose;
|
|
@@ -798,6 +557,46 @@ export class TerminalUI extends EventEmitter {
|
|
|
798
557
|
this.needsRender = true;
|
|
799
558
|
}
|
|
800
559
|
// ===========================================================================
|
|
560
|
+
// Public API - Restored History (Ctrl+R)
|
|
561
|
+
// ===========================================================================
|
|
562
|
+
/**
|
|
563
|
+
* Store restored conversation items for Ctrl+R history view.
|
|
564
|
+
* Called when a session is resumed/restored.
|
|
565
|
+
*/
|
|
566
|
+
setRestoredHistory(items) {
|
|
567
|
+
this.conversation.setRestoredHistory(items);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Check if there is restored history available.
|
|
571
|
+
*/
|
|
572
|
+
hasRestoredHistory() {
|
|
573
|
+
return this.conversation.hasRestoredHistory();
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Show restored conversation history (Ctrl+R).
|
|
577
|
+
* Renders items permanently into the scrollback buffer.
|
|
578
|
+
* Stays in normal mode — the output persists like any other printed content.
|
|
579
|
+
*/
|
|
580
|
+
showRestoredHistory() {
|
|
581
|
+
const s = getStyles();
|
|
582
|
+
// Clear existing footer to make room for output
|
|
583
|
+
this.footer.clear();
|
|
584
|
+
// Print header
|
|
585
|
+
console.log('');
|
|
586
|
+
console.log(s.info(`── Conversation History (${String(this.conversation.restoredHistory.length)} items) ──`));
|
|
587
|
+
console.log('');
|
|
588
|
+
// Render each restored item into the scrollback
|
|
589
|
+
for (const item of this.conversation.restoredHistory) {
|
|
590
|
+
this.renderItemToConsole(item);
|
|
591
|
+
}
|
|
592
|
+
console.log('');
|
|
593
|
+
console.log(s.muted(`── End of history ──`));
|
|
594
|
+
console.log('');
|
|
595
|
+
// Reset footer state so it re-renders cleanly below the output
|
|
596
|
+
this.footer.resetRenderState();
|
|
597
|
+
this.needsRender = true;
|
|
598
|
+
}
|
|
599
|
+
// ===========================================================================
|
|
801
600
|
// Public API - Config
|
|
802
601
|
// ===========================================================================
|
|
803
602
|
/**
|
|
@@ -829,16 +628,21 @@ export class TerminalUI extends EventEmitter {
|
|
|
829
628
|
*/
|
|
830
629
|
reRenderConversation() {
|
|
831
630
|
// Clear screen AND scrollback buffer, then move to home
|
|
832
|
-
// \x1b[2J - clear entire screen
|
|
833
|
-
// \x1b[3J - clear scrollback buffer (prevents accumulation)
|
|
834
|
-
// \x1b[H - move cursor to home position
|
|
835
631
|
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
836
632
|
// Reset footer state since we cleared everything
|
|
837
|
-
this.
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
633
|
+
this.footer.resetRenderState();
|
|
634
|
+
// Show filter indicator if active
|
|
635
|
+
if (this.conversation.isFilterActive) {
|
|
636
|
+
const agentList = [...this.conversation.filterAgentIds].map(id => `$${id}`).join(', ');
|
|
637
|
+
const s = getStyles();
|
|
638
|
+
console.log(s.muted(`── Showing messages from: ${agentList} ──`));
|
|
639
|
+
console.log('');
|
|
640
|
+
}
|
|
641
|
+
// Re-render all items with new config (applying filter)
|
|
642
|
+
for (const item of this.conversation.history) {
|
|
643
|
+
if (this.conversation.shouldShowItem(item)) {
|
|
644
|
+
this.renderItemToConsole(item);
|
|
645
|
+
}
|
|
842
646
|
}
|
|
843
647
|
// Footer will be re-rendered by the render loop
|
|
844
648
|
this.needsRender = true;
|
|
@@ -851,336 +655,66 @@ export class TerminalUI extends EventEmitter {
|
|
|
851
655
|
* Called by /clear command.
|
|
852
656
|
*/
|
|
853
657
|
clearConversationHistory() {
|
|
854
|
-
this.
|
|
658
|
+
this.conversation.clearHistory();
|
|
855
659
|
}
|
|
856
660
|
/**
|
|
857
661
|
* Get conversation history (copy).
|
|
858
662
|
*/
|
|
859
663
|
getConversationHistory() {
|
|
860
|
-
return
|
|
664
|
+
return this.conversation.getHistory();
|
|
861
665
|
}
|
|
862
666
|
// ===========================================================================
|
|
863
|
-
// Public API -
|
|
667
|
+
// Public API - Conversation Filter (delegated to ConversationStore)
|
|
864
668
|
// ===========================================================================
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
hasActiveOverlay() {
|
|
869
|
-
return this.overlayStack.length > 0;
|
|
669
|
+
setConversationFilter(agentIds) {
|
|
670
|
+
this.conversation.setFilter(agentIds);
|
|
671
|
+
this.reRenderConversation();
|
|
870
672
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
getActiveOverlay() {
|
|
875
|
-
return this.overlayStack.length > 0
|
|
876
|
-
? this.overlayStack[this.overlayStack.length - 1]
|
|
877
|
-
: null;
|
|
673
|
+
clearConversationFilter() {
|
|
674
|
+
this.conversation.clearFilter();
|
|
675
|
+
this.reRenderConversation();
|
|
878
676
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
* Returns promise that resolves when overlay closes.
|
|
882
|
-
*/
|
|
883
|
-
async showOverlay(overlay) {
|
|
884
|
-
// Check if we're pushing onto an existing overlay (stack has items before this push)
|
|
885
|
-
const isPushingOntoExisting = this.overlayStack.length > 0;
|
|
886
|
-
// 1. If pushing onto existing overlay, clear its render first
|
|
887
|
-
if (isPushingOntoExisting) {
|
|
888
|
-
this.clearOverlayRender();
|
|
889
|
-
}
|
|
890
|
-
// 2. Push overlay onto stack
|
|
891
|
-
this.overlayStack.push(overlay);
|
|
892
|
-
// 3. Call onMount lifecycle (alternate screen overlays enter alternate screen here)
|
|
893
|
-
await overlay.onMount?.();
|
|
894
|
-
// 4. Switch render mode based on overlay type
|
|
895
|
-
// Skip for alternate screen overlays - they manage their own screen buffer
|
|
896
|
-
if (!overlay.usesAlternateScreen) {
|
|
897
|
-
if (overlay.type === 'fullscreen') {
|
|
898
|
-
this.enterFullscreenOverlayMode();
|
|
899
|
-
}
|
|
900
|
-
else {
|
|
901
|
-
this.enterInlineOverlayMode();
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
// 5. Initial render
|
|
905
|
-
this.renderOverlay();
|
|
906
|
-
// 6. Wait for close
|
|
907
|
-
return new Promise((resolve) => {
|
|
908
|
-
this.overlayResolvers.set(overlay.id, resolve);
|
|
909
|
-
});
|
|
677
|
+
getConversationFilter() {
|
|
678
|
+
return this.conversation.getFilter();
|
|
910
679
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
* Called internally when overlay returns close action.
|
|
914
|
-
*/
|
|
915
|
-
closeCurrentOverlay(result, cancelled) {
|
|
916
|
-
const overlay = this.overlayStack.pop();
|
|
917
|
-
if (!overlay)
|
|
918
|
-
return;
|
|
919
|
-
// Check if overlay uses alternate screen (manages its own screen buffer)
|
|
920
|
-
const usesAlternateScreen = overlay.usesAlternateScreen === true;
|
|
921
|
-
// 1. Call onUnmount lifecycle (this exits alternate screen if used)
|
|
922
|
-
overlay.onUnmount?.();
|
|
923
|
-
// 2. Get close summary before clearing (needs overlay reference)
|
|
924
|
-
const summary = (!cancelled && result !== null)
|
|
925
|
-
? overlay.getCloseSummary?.(result)
|
|
926
|
-
: null;
|
|
927
|
-
// 3. Restore render mode FIRST (clears overlay from screen)
|
|
928
|
-
if (this.overlayStack.length === 0) {
|
|
929
|
-
// Skip exitOverlayMode for alternate screen overlays - exiting alternate
|
|
930
|
-
// screen already restored the main screen, clearing would corrupt it
|
|
931
|
-
if (usesAlternateScreen) {
|
|
932
|
-
// Just reset state, skip clearing AND skip re-rendering footer
|
|
933
|
-
// (alternate screen restore already put the screen back to its previous state)
|
|
934
|
-
this.overlayRenderState = { lineCount: 0, maxLineCount: 0 };
|
|
935
|
-
this.flushOverlayPrintBuffer();
|
|
936
|
-
// Don't call render() - the footer was restored by alternate screen exit
|
|
937
|
-
}
|
|
938
|
-
else {
|
|
939
|
-
this.exitOverlayMode();
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
else {
|
|
943
|
-
// Popping back to parent overlay:
|
|
944
|
-
// 1. Clear the child overlay's render (skip for alternate screen)
|
|
945
|
-
if (!usesAlternateScreen) {
|
|
946
|
-
this.clearOverlayRender();
|
|
947
|
-
}
|
|
948
|
-
// 2. Reset render state for parent
|
|
949
|
-
this.overlayRenderState = { lineCount: 0, maxLineCount: 0 };
|
|
950
|
-
// 3. Re-render parent overlay
|
|
951
|
-
this.renderOverlay();
|
|
952
|
-
}
|
|
953
|
-
// 4. Show close summary AFTER clearing overlay
|
|
954
|
-
if (summary) {
|
|
955
|
-
this.print({ type: 'info', message: summary });
|
|
956
|
-
}
|
|
957
|
-
// 5. Resolve promise
|
|
958
|
-
const resolver = this.overlayResolvers.get(overlay.id);
|
|
959
|
-
resolver?.(result);
|
|
960
|
-
this.overlayResolvers.delete(overlay.id);
|
|
680
|
+
addToConversationFilter(agentId) {
|
|
681
|
+
return this.conversation.addToFilter(agentId);
|
|
961
682
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
* Clears screen and prepares for overlay rendering.
|
|
965
|
-
*/
|
|
966
|
-
enterFullscreenOverlayMode() {
|
|
967
|
-
// Clear footer first
|
|
968
|
-
this.clear();
|
|
969
|
-
// Clear screen for fullscreen overlay
|
|
970
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
971
|
-
// Reset overlay render state (both lineCount and maxLineCount)
|
|
972
|
-
this.overlayRenderState = { lineCount: 0, maxLineCount: 0 };
|
|
973
|
-
}
|
|
974
|
-
/**
|
|
975
|
-
* Enter inline overlay mode.
|
|
976
|
-
* Clears footer but keeps conversation visible.
|
|
977
|
-
*/
|
|
978
|
-
enterInlineOverlayMode() {
|
|
979
|
-
// Clear footer to make room for inline overlay
|
|
980
|
-
this.clear();
|
|
981
|
-
// Reset overlay render state (both lineCount and maxLineCount)
|
|
982
|
-
this.overlayRenderState = { lineCount: 0, maxLineCount: 0 };
|
|
983
|
-
}
|
|
984
|
-
/**
|
|
985
|
-
* Exit overlay mode and restore normal rendering.
|
|
986
|
-
*/
|
|
987
|
-
exitOverlayMode() {
|
|
988
|
-
// Clear overlay content (uses maxLineCount to ensure all lines are cleared)
|
|
989
|
-
this.clearOverlayRender();
|
|
990
|
-
// Reset state
|
|
991
|
-
this.overlayRenderState = { lineCount: 0, maxLineCount: 0 };
|
|
992
|
-
// Flush any items that were buffered while overlay was active
|
|
993
|
-
this.flushOverlayPrintBuffer();
|
|
994
|
-
// Re-render footer
|
|
995
|
-
this.render();
|
|
683
|
+
isAgentInFilter(agentId) {
|
|
684
|
+
return this.conversation.isAgentInFilter(agentId);
|
|
996
685
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
if (this.overlayPrintBuffer.length === 0)
|
|
1003
|
-
return;
|
|
1004
|
-
// Render all buffered items
|
|
1005
|
-
for (const item of this.overlayPrintBuffer) {
|
|
1006
|
-
this.renderItem(item);
|
|
1007
|
-
}
|
|
1008
|
-
// Clear the buffer
|
|
1009
|
-
this.overlayPrintBuffer = [];
|
|
1010
|
-
}
|
|
1011
|
-
/**
|
|
1012
|
-
* Clear previous overlay render.
|
|
1013
|
-
* Uses maxLineCount to ensure all lines are cleared even if content shrank between renders.
|
|
1014
|
-
*/
|
|
1015
|
-
clearOverlayRender() {
|
|
1016
|
-
// Use maxLineCount to handle cases where overlay content varied between renders
|
|
1017
|
-
const linesToClear = this.overlayRenderState.maxLineCount;
|
|
1018
|
-
if (linesToClear > 0) {
|
|
1019
|
-
// Move cursor up to start of overlay
|
|
1020
|
-
process.stdout.write(`\x1b[${String(linesToClear)}A`);
|
|
1021
|
-
// Move to column 1
|
|
1022
|
-
process.stdout.write('\r');
|
|
1023
|
-
// Clear from cursor to end of screen
|
|
1024
|
-
process.stdout.write('\x1b[J');
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
/**
|
|
1028
|
-
* Render the active overlay.
|
|
1029
|
-
*/
|
|
1030
|
-
renderOverlay() {
|
|
1031
|
-
const overlay = this.getActiveOverlay();
|
|
1032
|
-
if (!overlay)
|
|
1033
|
-
return;
|
|
1034
|
-
const termWidth = terminal.getTerminalWidth();
|
|
1035
|
-
const termHeight = terminal.getTerminalHeight();
|
|
1036
|
-
const s = getStyles();
|
|
1037
|
-
const context = {
|
|
1038
|
-
width: termWidth,
|
|
1039
|
-
height: termHeight,
|
|
1040
|
-
styles: s,
|
|
1041
|
-
};
|
|
1042
|
-
const content = overlay.render(context);
|
|
1043
|
-
if (overlay.type === 'fullscreen') {
|
|
1044
|
-
this.renderFullscreenOverlay(content, termWidth);
|
|
1045
|
-
}
|
|
1046
|
-
else {
|
|
1047
|
-
this.renderInlineOverlay(content, termWidth);
|
|
1048
|
-
}
|
|
686
|
+
// ===========================================================================
|
|
687
|
+
// Public API - Overlays (delegated to OverlayManager)
|
|
688
|
+
// ===========================================================================
|
|
689
|
+
hasActiveOverlay() {
|
|
690
|
+
return this.overlay.hasActiveOverlay();
|
|
1049
691
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
*
|
|
1053
|
-
* CRITICAL: Cursor positioning must be consistent between renders.
|
|
1054
|
-
* Same logic as renderInlineOverlay - pad to maxLineCount for consistency.
|
|
1055
|
-
*/
|
|
1056
|
-
renderFullscreenOverlay(content, termWidth) {
|
|
1057
|
-
// Clear previous render
|
|
1058
|
-
this.clearOverlayRender();
|
|
1059
|
-
// Calculate physical lines
|
|
1060
|
-
let physicalLines = 0;
|
|
1061
|
-
for (const line of content.lines) {
|
|
1062
|
-
const visibleLen = getVisibleLength(line);
|
|
1063
|
-
physicalLines += Math.max(1, Math.ceil(visibleLen / termWidth));
|
|
1064
|
-
}
|
|
1065
|
-
// Update maxLineCount BEFORE padding (track the natural maximum)
|
|
1066
|
-
const contentMinHeight = content.minHeight ?? 0;
|
|
1067
|
-
const naturalHeight = Math.max(physicalLines, contentMinHeight);
|
|
1068
|
-
this.overlayRenderState.maxLineCount = Math.max(this.overlayRenderState.maxLineCount, naturalHeight);
|
|
1069
|
-
// CRITICAL: Pad to maxLineCount for consistent cursor positioning
|
|
1070
|
-
const targetHeight = this.overlayRenderState.maxLineCount;
|
|
1071
|
-
const paddedLines = [...content.lines];
|
|
1072
|
-
while (physicalLines < targetHeight) {
|
|
1073
|
-
paddedLines.push('');
|
|
1074
|
-
physicalLines++;
|
|
1075
|
-
}
|
|
1076
|
-
// Update lineCount to actual rendered height
|
|
1077
|
-
this.overlayRenderState.lineCount = physicalLines;
|
|
1078
|
-
// Render lines
|
|
1079
|
-
for (let i = 0; i < paddedLines.length; i++) {
|
|
1080
|
-
process.stdout.write(paddedLines[i]);
|
|
1081
|
-
if (i < paddedLines.length - 1) {
|
|
1082
|
-
process.stdout.write('\n');
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
// Position cursor if specified
|
|
1086
|
-
if (content.cursorPosition) {
|
|
1087
|
-
const { line, column } = content.cursorPosition;
|
|
1088
|
-
// Move to the correct line (relative from end)
|
|
1089
|
-
const linesFromEnd = paddedLines.length - 1 - line;
|
|
1090
|
-
if (linesFromEnd > 0) {
|
|
1091
|
-
process.stdout.write(`\x1b[${String(linesFromEnd)}A`);
|
|
1092
|
-
}
|
|
1093
|
-
process.stdout.write(`\x1b[${String(column)}G`);
|
|
1094
|
-
}
|
|
1095
|
-
terminal.showCursor();
|
|
692
|
+
getActiveOverlay() {
|
|
693
|
+
return this.overlay.getActiveOverlay();
|
|
1096
694
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
*
|
|
1100
|
-
* CRITICAL: Cursor positioning must be consistent between renders.
|
|
1101
|
-
* After rendering, cursor MUST be at maxLineCount position, not at
|
|
1102
|
-
* actual content height. Otherwise, the next clear will move cursor
|
|
1103
|
-
* up by maxLineCount from the wrong position, causing "ghost lines".
|
|
1104
|
-
*/
|
|
1105
|
-
renderInlineOverlay(content, termWidth) {
|
|
1106
|
-
// Clear previous render
|
|
1107
|
-
this.clearOverlayRender();
|
|
1108
|
-
// Calculate physical lines
|
|
1109
|
-
let physicalLines = 0;
|
|
1110
|
-
for (const line of content.lines) {
|
|
1111
|
-
const visibleLen = getVisibleLength(line);
|
|
1112
|
-
physicalLines += Math.max(1, Math.ceil(visibleLen / termWidth));
|
|
1113
|
-
}
|
|
1114
|
-
// Update maxLineCount BEFORE padding (track the natural maximum)
|
|
1115
|
-
const contentMinHeight = content.minHeight ?? 0;
|
|
1116
|
-
const naturalHeight = Math.max(physicalLines, contentMinHeight);
|
|
1117
|
-
this.overlayRenderState.maxLineCount = Math.max(this.overlayRenderState.maxLineCount, naturalHeight);
|
|
1118
|
-
// CRITICAL: Pad to maxLineCount, not just minHeight!
|
|
1119
|
-
// This ensures cursor position is consistent between renders.
|
|
1120
|
-
// Without this, going from tall content (e.g., detail screen with 25 lines)
|
|
1121
|
-
// to short content (e.g., main list with 20 lines) leaves cursor at line 20,
|
|
1122
|
-
// but next clear moves up 25 lines → cursor goes to line -5 → ghost lines!
|
|
1123
|
-
const targetHeight = this.overlayRenderState.maxLineCount;
|
|
1124
|
-
const paddedLines = [...content.lines];
|
|
1125
|
-
while (physicalLines < targetHeight) {
|
|
1126
|
-
paddedLines.push('');
|
|
1127
|
-
physicalLines++;
|
|
1128
|
-
}
|
|
1129
|
-
// Update lineCount to actual rendered height
|
|
1130
|
-
this.overlayRenderState.lineCount = physicalLines;
|
|
1131
|
-
// Render lines (each line ends with \n for consistent cursor positioning)
|
|
1132
|
-
for (const line of paddedLines) {
|
|
1133
|
-
process.stdout.write(line + '\n');
|
|
1134
|
-
}
|
|
1135
|
-
terminal.showCursor();
|
|
695
|
+
async showOverlay(overlay) {
|
|
696
|
+
return this.overlay.showOverlay(overlay);
|
|
1136
697
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
*/
|
|
1140
|
-
processOverlayAction(action) {
|
|
1141
|
-
switch (action.type) {
|
|
1142
|
-
case 'none':
|
|
1143
|
-
// Do nothing
|
|
1144
|
-
break;
|
|
1145
|
-
case 'render':
|
|
1146
|
-
this.renderOverlay();
|
|
1147
|
-
break;
|
|
1148
|
-
case 'close':
|
|
1149
|
-
if (action.cancelled) {
|
|
1150
|
-
this.closeCurrentOverlay(null, true);
|
|
1151
|
-
}
|
|
1152
|
-
else {
|
|
1153
|
-
this.closeCurrentOverlay(action.result ?? null, false);
|
|
1154
|
-
}
|
|
1155
|
-
break;
|
|
1156
|
-
case 'push':
|
|
1157
|
-
if (action.overlay) {
|
|
1158
|
-
void this.showOverlay(action.overlay);
|
|
1159
|
-
}
|
|
1160
|
-
break;
|
|
1161
|
-
case 'pop':
|
|
1162
|
-
this.closeCurrentOverlay(null, true);
|
|
1163
|
-
break;
|
|
1164
|
-
}
|
|
698
|
+
requestOverlayRender() {
|
|
699
|
+
this.overlay.requestOverlayRender();
|
|
1165
700
|
}
|
|
1166
701
|
// ===========================================================================
|
|
1167
702
|
// Public API - Input queue
|
|
1168
703
|
// ===========================================================================
|
|
1169
704
|
getQueuedInputs() {
|
|
1170
|
-
return
|
|
705
|
+
return this.input.getQueuedInputs();
|
|
1171
706
|
}
|
|
1172
707
|
popQueuedInput() {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
return input ?? null;
|
|
708
|
+
const result = this.input.popQueuedInput();
|
|
709
|
+
if (result !== null)
|
|
710
|
+
this.needsRender = true;
|
|
711
|
+
return result;
|
|
1178
712
|
}
|
|
1179
713
|
hasQueuedInput() {
|
|
1180
|
-
return this.
|
|
714
|
+
return this.input.hasQueuedInput();
|
|
1181
715
|
}
|
|
1182
716
|
clearQueue() {
|
|
1183
|
-
this.
|
|
717
|
+
this.input.clearQueue();
|
|
1184
718
|
this.needsRender = true;
|
|
1185
719
|
}
|
|
1186
720
|
// ===========================================================================
|
|
@@ -1225,11 +759,11 @@ export class TerminalUI extends EventEmitter {
|
|
|
1225
759
|
this.renderTimer = null;
|
|
1226
760
|
}
|
|
1227
761
|
// Stop spinner
|
|
1228
|
-
this.stopSpinnerAnimation();
|
|
762
|
+
this.footer.stopSpinnerAnimation();
|
|
1229
763
|
// Stop keyboard capture
|
|
1230
|
-
this.
|
|
764
|
+
this.keyboard.stopCapture();
|
|
1231
765
|
// Clear footer from screen
|
|
1232
|
-
this.clear();
|
|
766
|
+
this.footer.clear();
|
|
1233
767
|
}
|
|
1234
768
|
/**
|
|
1235
769
|
* Resume footer after pause
|
|
@@ -1238,15 +772,15 @@ export class TerminalUI extends EventEmitter {
|
|
|
1238
772
|
this.isPaused = false;
|
|
1239
773
|
// Resume spinner if agent running
|
|
1240
774
|
if (this.agentRunning) {
|
|
1241
|
-
this.startSpinnerAnimation();
|
|
775
|
+
this.footer.startSpinnerAnimation(() => { this.needsRender = true; });
|
|
1242
776
|
}
|
|
1243
777
|
// Restart keyboard capture
|
|
1244
|
-
this.
|
|
778
|
+
this.keyboard.startCapture();
|
|
1245
779
|
// Restart render loop
|
|
1246
|
-
this.render();
|
|
780
|
+
this.footer.render();
|
|
1247
781
|
this.renderTimer = setInterval(() => {
|
|
1248
782
|
if (this.needsRender && !this.isPaused) {
|
|
1249
|
-
this.render();
|
|
783
|
+
this.footer.render();
|
|
1250
784
|
this.needsRender = false;
|
|
1251
785
|
}
|
|
1252
786
|
}, 60);
|
|
@@ -1255,7 +789,7 @@ export class TerminalUI extends EventEmitter {
|
|
|
1255
789
|
* Restart input after command
|
|
1256
790
|
*/
|
|
1257
791
|
restartInput() {
|
|
1258
|
-
this.render();
|
|
792
|
+
this.footer.render();
|
|
1259
793
|
}
|
|
1260
794
|
/**
|
|
1261
795
|
* Refresh prompt with theme colors
|
|
@@ -1273,1024 +807,13 @@ export class TerminalUI extends EventEmitter {
|
|
|
1273
807
|
* @deprecated Use print() or output() instead
|
|
1274
808
|
*/
|
|
1275
809
|
clearForOutput() {
|
|
1276
|
-
this.clear();
|
|
810
|
+
this.footer.clear();
|
|
1277
811
|
}
|
|
1278
812
|
/**
|
|
1279
813
|
* @deprecated Use print() or output() instead
|
|
1280
814
|
*/
|
|
1281
815
|
forceRender() {
|
|
1282
|
-
this.render();
|
|
816
|
+
this.footer.render();
|
|
1283
817
|
this.needsRender = false;
|
|
1284
818
|
}
|
|
1285
|
-
// ===========================================================================
|
|
1286
|
-
// Private - Rendering
|
|
1287
|
-
// ===========================================================================
|
|
1288
|
-
clear() {
|
|
1289
|
-
if (this.lastRenderHeight === 0) {
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
terminal.moveCursorToLineStart();
|
|
1293
|
-
// Move cursor to TOP of footer
|
|
1294
|
-
// cursorRowFromTop = which row the cursor is on (1-indexed from top of footer)
|
|
1295
|
-
const cursorRowFromTop = this.lastRenderHeight - this.cursorLineFromBottom;
|
|
1296
|
-
const rowsToMoveUp = cursorRowFromTop - 1;
|
|
1297
|
-
if (rowsToMoveUp > 0) {
|
|
1298
|
-
terminal.moveCursorUp(rowsToMoveUp);
|
|
1299
|
-
}
|
|
1300
|
-
terminal.clearToEndOfScreen();
|
|
1301
|
-
this.lastRenderHeight = 0;
|
|
1302
|
-
this.cursorLineFromBottom = 0;
|
|
1303
|
-
}
|
|
1304
|
-
render() {
|
|
1305
|
-
if (!this.isRunning || this.isPaused) {
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
// Don't render footer when overlay is active
|
|
1309
|
-
if (this.hasActiveOverlay()) {
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
const termWidth = terminal.getTerminalWidth();
|
|
1313
|
-
const s = getStyles();
|
|
1314
|
-
// Build all lines in order
|
|
1315
|
-
const lines = [];
|
|
1316
|
-
// 0. LiveRegion (if has items) - rendered at top of footer
|
|
1317
|
-
if (this.liveRegion.hasItems()) {
|
|
1318
|
-
const liveLines = this.liveRegion.render({
|
|
1319
|
-
width: termWidth,
|
|
1320
|
-
verbose: this.config.verbose,
|
|
1321
|
-
showTokens: true,
|
|
1322
|
-
});
|
|
1323
|
-
for (const line of liveLines) {
|
|
1324
|
-
lines.push({
|
|
1325
|
-
content: line,
|
|
1326
|
-
physicalLines: getPhysicalLineCount(line, termWidth),
|
|
1327
|
-
});
|
|
1328
|
-
}
|
|
1329
|
-
// Blank line after live region for spacing
|
|
1330
|
-
if (liveLines.length > 0) {
|
|
1331
|
-
lines.push({ content: '', physicalLines: 1 });
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
// 1. Header: Spinner (when running) or "Todos" (when idle with todos)
|
|
1335
|
-
if (this.agentRunning) {
|
|
1336
|
-
const spinnerLine = this.buildSpinnerLine();
|
|
1337
|
-
lines.push({
|
|
1338
|
-
content: spinnerLine,
|
|
1339
|
-
physicalLines: getPhysicalLineCount(spinnerLine, termWidth),
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1342
|
-
else if (this.todos.length > 0) {
|
|
1343
|
-
const headerLine = s.muted('Todos');
|
|
1344
|
-
lines.push({
|
|
1345
|
-
content: headerLine,
|
|
1346
|
-
physicalLines: 1,
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1349
|
-
// 2. Todo list (show if there are todos AND showTodos is true)
|
|
1350
|
-
if (this.todos.length > 0 && this.showTodos) {
|
|
1351
|
-
for (const todo of this.todos) {
|
|
1352
|
-
const todoLine = this.buildTodoLine(todo);
|
|
1353
|
-
lines.push({
|
|
1354
|
-
content: todoLine,
|
|
1355
|
-
physicalLines: getPhysicalLineCount(todoLine, termWidth),
|
|
1356
|
-
});
|
|
1357
|
-
}
|
|
1358
|
-
// Blank line after todos for spacing
|
|
1359
|
-
lines.push({ content: '', physicalLines: 1 });
|
|
1360
|
-
}
|
|
1361
|
-
// 3. Queued inputs (show multiline as single line with ↵ indicator)
|
|
1362
|
-
for (const queued of this.queuedInputs) {
|
|
1363
|
-
const displayText = queued.replace(/\n/g, ' ↵ ');
|
|
1364
|
-
const queuedLine = s.muted(`queued: "${displayText}"`);
|
|
1365
|
-
lines.push({
|
|
1366
|
-
content: queuedLine,
|
|
1367
|
-
physicalLines: getPhysicalLineCount(queuedLine, termWidth),
|
|
1368
|
-
});
|
|
1369
|
-
}
|
|
1370
|
-
// 4. Top separator
|
|
1371
|
-
const separator = '─'.repeat(termWidth);
|
|
1372
|
-
lines.push({ content: separator, physicalLines: 1 });
|
|
1373
|
-
// 5. Input prompt (multiline support)
|
|
1374
|
-
const continuationPrompt = s.muted(' \\ ');
|
|
1375
|
-
const continuationPromptLen = getVisibleLength(continuationPrompt);
|
|
1376
|
-
const cursorLineIndex = lines.length + this.currentLine; // Index where cursor is
|
|
1377
|
-
for (let i = 0; i < this.lines.length; i++) {
|
|
1378
|
-
const linePrompt = i === 0 ? this.promptPrefix : continuationPrompt;
|
|
1379
|
-
const linePromptLen = i === 0 ? this.promptPrefixLen : continuationPromptLen;
|
|
1380
|
-
const lineContent = this.lines[i];
|
|
1381
|
-
// Show ghost text on first line when input is empty and no autocomplete
|
|
1382
|
-
let fullLine;
|
|
1383
|
-
let totalLen;
|
|
1384
|
-
if (i === 0 &&
|
|
1385
|
-
lineContent === '' &&
|
|
1386
|
-
this.lines.length === 1 &&
|
|
1387
|
-
this.suggestion &&
|
|
1388
|
-
!this.autocomplete.active &&
|
|
1389
|
-
!this.fileAutocomplete.active) {
|
|
1390
|
-
// Build: [prompt][suggestion]...[↵ send]
|
|
1391
|
-
const rightHint = '↵ send';
|
|
1392
|
-
const leftContent = linePrompt + s.muted(this.suggestion);
|
|
1393
|
-
const leftLen = linePromptLen + this.suggestion.length;
|
|
1394
|
-
const rightLen = rightHint.length;
|
|
1395
|
-
const padding = Math.max(1, termWidth - leftLen - rightLen);
|
|
1396
|
-
fullLine = leftContent + ' '.repeat(padding) + s.muted(rightHint);
|
|
1397
|
-
totalLen = termWidth; // Full width
|
|
1398
|
-
}
|
|
1399
|
-
else {
|
|
1400
|
-
fullLine = linePrompt + lineContent;
|
|
1401
|
-
totalLen = linePromptLen + getVisibleLength(lineContent);
|
|
1402
|
-
}
|
|
1403
|
-
const physicalLines = totalLen === 0 ? 1 : Math.ceil(totalLen / termWidth) || 1;
|
|
1404
|
-
lines.push({
|
|
1405
|
-
content: fullLine,
|
|
1406
|
-
physicalLines,
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
// 6. Bottom separator
|
|
1410
|
-
lines.push({ content: separator, physicalLines: 1 });
|
|
1411
|
-
// 7. Mode indicator
|
|
1412
|
-
const modeLine = this.buildModeLine(termWidth);
|
|
1413
|
-
lines.push({
|
|
1414
|
-
content: modeLine,
|
|
1415
|
-
physicalLines: getPhysicalLineCount(modeLine, termWidth),
|
|
1416
|
-
});
|
|
1417
|
-
// 8. Autocomplete dropdown (if active) - file autocomplete takes priority
|
|
1418
|
-
const fileAutocompleteLines = this.buildFileAutocompleteLines(termWidth);
|
|
1419
|
-
const autocompleteLines = fileAutocompleteLines.length > 0
|
|
1420
|
-
? fileAutocompleteLines
|
|
1421
|
-
: this.buildAutocompleteLines(termWidth);
|
|
1422
|
-
for (const line of autocompleteLines) {
|
|
1423
|
-
lines.push({
|
|
1424
|
-
content: line,
|
|
1425
|
-
physicalLines: getPhysicalLineCount(line, termWidth),
|
|
1426
|
-
});
|
|
1427
|
-
}
|
|
1428
|
-
// Calculate total physical height
|
|
1429
|
-
let totalPhysicalLines = 0;
|
|
1430
|
-
for (const line of lines) {
|
|
1431
|
-
totalPhysicalLines += line.physicalLines;
|
|
1432
|
-
}
|
|
1433
|
-
// Clear existing footer
|
|
1434
|
-
this.clear();
|
|
1435
|
-
// Write all lines
|
|
1436
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1437
|
-
terminal.write(lines[i].content);
|
|
1438
|
-
if (i < lines.length - 1) {
|
|
1439
|
-
terminal.write('\n');
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
// Calculate cursor position (for multiline, position on the line where cursor is)
|
|
1443
|
-
let rowsAfterCursor = 0;
|
|
1444
|
-
for (let i = cursorLineIndex + 1; i < lines.length; i++) {
|
|
1445
|
-
rowsAfterCursor += lines[i].physicalLines;
|
|
1446
|
-
}
|
|
1447
|
-
if (rowsAfterCursor > 0) {
|
|
1448
|
-
terminal.moveCursorUp(rowsAfterCursor);
|
|
1449
|
-
}
|
|
1450
|
-
// Calculate column within current line
|
|
1451
|
-
const currentLinePromptLen = this.currentLine === 0 ? this.promptPrefixLen : continuationPromptLen;
|
|
1452
|
-
const totalPos = currentLinePromptLen + this.cursorPos;
|
|
1453
|
-
const cursorCol = (totalPos % termWidth) + 1;
|
|
1454
|
-
terminal.moveCursorToColumn(cursorCol);
|
|
1455
|
-
terminal.showCursor();
|
|
1456
|
-
this.lastRenderHeight = totalPhysicalLines;
|
|
1457
|
-
this.cursorLineFromBottom = rowsAfterCursor;
|
|
1458
|
-
}
|
|
1459
|
-
// ===========================================================================
|
|
1460
|
-
// Private - Line builders
|
|
1461
|
-
// ===========================================================================
|
|
1462
|
-
buildSpinnerLine() {
|
|
1463
|
-
const s = getStyles();
|
|
1464
|
-
const frame = this.spinnerFrames[this.spinnerFrame % this.spinnerFrames.length];
|
|
1465
|
-
const text = this.currentTool ?? this.spinnerText ?? 'Thinking...';
|
|
1466
|
-
// Build hints
|
|
1467
|
-
const hints = ['esc to interrupt'];
|
|
1468
|
-
if (this.todos.length > 0 && this.showTodos) {
|
|
1469
|
-
hints.push('ctrl+t to hide todos');
|
|
1470
|
-
}
|
|
1471
|
-
else if (this.todos.length > 0 && !this.showTodos) {
|
|
1472
|
-
hints.push('ctrl+t to show todos');
|
|
1473
|
-
}
|
|
1474
|
-
const hintsText = hints.length > 0 ? ` (${hints.join(' · ')})` : '';
|
|
1475
|
-
// Format: [CRT scanner] text (hints)
|
|
1476
|
-
return s.muted(`${frame} ${text}${hintsText}`);
|
|
1477
|
-
}
|
|
1478
|
-
buildTodoLine(todo) {
|
|
1479
|
-
const s = getStyles();
|
|
1480
|
-
const icon = todo.status === 'completed' ? '✓' :
|
|
1481
|
-
todo.status === 'in_progress' ? '→' : '☐';
|
|
1482
|
-
const style = todo.status === 'completed' ? s.muted :
|
|
1483
|
-
todo.status === 'in_progress' ? s.info : s.muted;
|
|
1484
|
-
return style(`${icon} ${todo.content}`);
|
|
1485
|
-
}
|
|
1486
|
-
buildModeLine(termWidth) {
|
|
1487
|
-
const s = getStyles();
|
|
1488
|
-
const modeInfo = MODE_INFO[this.mode];
|
|
1489
|
-
let leftPart;
|
|
1490
|
-
switch (this.mode) {
|
|
1491
|
-
case 'normal':
|
|
1492
|
-
leftPart = s.muted(`mode: ${modeInfo.label} (Shift+Tab to change)`);
|
|
1493
|
-
break;
|
|
1494
|
-
case 'auto-accept':
|
|
1495
|
-
leftPart = s.warning(`mode: ${modeInfo.label}`) + s.muted(' (Shift+Tab to change)');
|
|
1496
|
-
break;
|
|
1497
|
-
case 'plan':
|
|
1498
|
-
leftPart = s.info(`mode: ${modeInfo.label}`) + s.muted(' (Shift+Tab to change)');
|
|
1499
|
-
break;
|
|
1500
|
-
default:
|
|
1501
|
-
leftPart = s.muted(`mode: ${modeInfo.label} (Shift+Tab to change)`);
|
|
1502
|
-
}
|
|
1503
|
-
if (!this.projectName) {
|
|
1504
|
-
return leftPart;
|
|
1505
|
-
}
|
|
1506
|
-
const projectText = `Project: ${this.projectName}`;
|
|
1507
|
-
const rightPart = s.muted(projectText);
|
|
1508
|
-
const leftVisible = getVisibleLength(leftPart);
|
|
1509
|
-
const padding = Math.max(2, termWidth - leftVisible - projectText.length);
|
|
1510
|
-
return leftPart + ' '.repeat(padding) + rightPart;
|
|
1511
|
-
}
|
|
1512
|
-
buildAutocompleteLines(termWidth) {
|
|
1513
|
-
if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
|
|
1514
|
-
return [];
|
|
1515
|
-
}
|
|
1516
|
-
const s = getStyles();
|
|
1517
|
-
const lines = [];
|
|
1518
|
-
const { matches, selectedIndex, scrollOffset } = this.autocomplete;
|
|
1519
|
-
const visible = matches.slice(scrollOffset, scrollOffset + MAX_VISIBLE_COMMANDS);
|
|
1520
|
-
const total = matches.length;
|
|
1521
|
-
// Show scroll indicator if there are more items above
|
|
1522
|
-
if (scrollOffset > 0) {
|
|
1523
|
-
lines.push(s.muted(` ↑ ${String(scrollOffset)} more above`));
|
|
1524
|
-
}
|
|
1525
|
-
for (let i = 0; i < visible.length; i++) {
|
|
1526
|
-
const cmd = visible[i];
|
|
1527
|
-
const actualIndex = scrollOffset + i;
|
|
1528
|
-
const isSelected = actualIndex === selectedIndex;
|
|
1529
|
-
const prefix = isSelected ? s.primary('❯ ') : ' ';
|
|
1530
|
-
const name = isSelected ? s.primaryBold(cmd.command) : cmd.command;
|
|
1531
|
-
// Truncate if too long
|
|
1532
|
-
const baseLen = getVisibleLength(prefix) + getVisibleLength(name) + 3; // 3 for ' - '
|
|
1533
|
-
const maxDescLen = termWidth - baseLen - 2;
|
|
1534
|
-
const truncatedDesc = cmd.description.length > maxDescLen
|
|
1535
|
-
? cmd.description.slice(0, maxDescLen - 1) + '…'
|
|
1536
|
-
: cmd.description;
|
|
1537
|
-
lines.push(`${prefix}${name} ${s.muted('- ' + truncatedDesc)}`);
|
|
1538
|
-
}
|
|
1539
|
-
// Show scroll indicator if there are more items below
|
|
1540
|
-
const belowCount = total - scrollOffset - visible.length;
|
|
1541
|
-
if (belowCount > 0) {
|
|
1542
|
-
lines.push(s.muted(` ↓ ${String(belowCount)} more below`));
|
|
1543
|
-
}
|
|
1544
|
-
return lines;
|
|
1545
|
-
}
|
|
1546
|
-
buildFileAutocompleteLines(termWidth) {
|
|
1547
|
-
if (!this.fileAutocomplete.active || this.fileAutocomplete.matches.length === 0) {
|
|
1548
|
-
return [];
|
|
1549
|
-
}
|
|
1550
|
-
const s = getStyles();
|
|
1551
|
-
const lines = [];
|
|
1552
|
-
const { matches, selectedIndex, scrollOffset } = this.fileAutocomplete;
|
|
1553
|
-
const visible = matches.slice(scrollOffset, scrollOffset + MAX_VISIBLE_COMMANDS);
|
|
1554
|
-
const total = matches.length;
|
|
1555
|
-
// Show scroll indicator if there are more items above
|
|
1556
|
-
if (scrollOffset > 0) {
|
|
1557
|
-
lines.push(s.muted(` ↑ ${String(scrollOffset)} more above`));
|
|
1558
|
-
}
|
|
1559
|
-
for (let i = 0; i < visible.length; i++) {
|
|
1560
|
-
const file = visible[i];
|
|
1561
|
-
const actualIndex = scrollOffset + i;
|
|
1562
|
-
const isSelected = actualIndex === selectedIndex;
|
|
1563
|
-
const prefix = isSelected ? s.primary('❯ ') : ' ';
|
|
1564
|
-
const icon = file.isDirectory ? '📁 ' : '📄 ';
|
|
1565
|
-
const name = isSelected ? s.primaryBold(file.path) : file.path;
|
|
1566
|
-
// Truncate if too long
|
|
1567
|
-
const baseLen = getVisibleLength(prefix) + 3 + getVisibleLength(file.path); // 3 for icon
|
|
1568
|
-
if (baseLen > termWidth - 2) {
|
|
1569
|
-
const maxLen = termWidth - getVisibleLength(prefix) - 5; // 3 for icon, 2 for padding
|
|
1570
|
-
const truncatedPath = file.path.length > maxLen
|
|
1571
|
-
? '…' + file.path.slice(-(maxLen - 1))
|
|
1572
|
-
: file.path;
|
|
1573
|
-
const truncatedName = isSelected ? s.primaryBold(truncatedPath) : truncatedPath;
|
|
1574
|
-
lines.push(`${prefix}${icon}${truncatedName}`);
|
|
1575
|
-
}
|
|
1576
|
-
else {
|
|
1577
|
-
lines.push(`${prefix}${icon}${name}`);
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
// Show scroll indicator if there are more items below
|
|
1581
|
-
const belowCount = total - scrollOffset - visible.length;
|
|
1582
|
-
if (belowCount > 0) {
|
|
1583
|
-
lines.push(s.muted(` ↓ ${String(belowCount)} more below`));
|
|
1584
|
-
}
|
|
1585
|
-
return lines;
|
|
1586
|
-
}
|
|
1587
|
-
// ===========================================================================
|
|
1588
|
-
// Private - Cursor calculations
|
|
1589
|
-
// ===========================================================================
|
|
1590
|
-
// ===========================================================================
|
|
1591
|
-
// Private - Autocomplete
|
|
1592
|
-
// ===========================================================================
|
|
1593
|
-
updateAutocomplete() {
|
|
1594
|
-
const currentLine = this.getCurrentLineContent();
|
|
1595
|
-
// Check for @ file path autocomplete first (works on any line)
|
|
1596
|
-
const atMention = extractAtMention(currentLine, this.cursorPos);
|
|
1597
|
-
if (atMention !== null) {
|
|
1598
|
-
// File autocomplete mode - disable command autocomplete
|
|
1599
|
-
this.autocomplete.active = false;
|
|
1600
|
-
this.autocomplete.matches = [];
|
|
1601
|
-
this.autocomplete.selectedIndex = 0;
|
|
1602
|
-
this.autocomplete.scrollOffset = 0;
|
|
1603
|
-
// Enable file autocomplete
|
|
1604
|
-
this.fileAutocomplete.active = true;
|
|
1605
|
-
this.fileAutocomplete.partial = atMention;
|
|
1606
|
-
this.fileAutocomplete.matches = getFileMatches(atMention);
|
|
1607
|
-
if (this.fileAutocomplete.selectedIndex >= this.fileAutocomplete.matches.length) {
|
|
1608
|
-
this.fileAutocomplete.selectedIndex = Math.max(0, this.fileAutocomplete.matches.length - 1);
|
|
1609
|
-
this.fileAutocomplete.scrollOffset = 0;
|
|
1610
|
-
}
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
// Reset file autocomplete
|
|
1614
|
-
this.fileAutocomplete.active = false;
|
|
1615
|
-
this.fileAutocomplete.matches = [];
|
|
1616
|
-
this.fileAutocomplete.selectedIndex = 0;
|
|
1617
|
-
this.fileAutocomplete.scrollOffset = 0;
|
|
1618
|
-
this.fileAutocomplete.partial = '';
|
|
1619
|
-
// Activate command autocomplete when first line starts with /
|
|
1620
|
-
const firstLine = this.lines[0];
|
|
1621
|
-
if (this.currentLine === 0 && firstLine.startsWith('/')) {
|
|
1622
|
-
this.autocomplete.active = true;
|
|
1623
|
-
const freshCommands = getAutocompleteCommands();
|
|
1624
|
-
this.autocomplete.matches = filterCommands(firstLine, freshCommands);
|
|
1625
|
-
// Reset selection if it's out of bounds
|
|
1626
|
-
if (this.autocomplete.selectedIndex >= this.autocomplete.matches.length) {
|
|
1627
|
-
this.autocomplete.selectedIndex = Math.max(0, this.autocomplete.matches.length - 1);
|
|
1628
|
-
this.autocomplete.scrollOffset = 0;
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
else {
|
|
1632
|
-
this.autocomplete.active = false;
|
|
1633
|
-
this.autocomplete.matches = [];
|
|
1634
|
-
this.autocomplete.selectedIndex = 0;
|
|
1635
|
-
this.autocomplete.scrollOffset = 0;
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
acceptAutocomplete() {
|
|
1639
|
-
// File autocomplete takes priority (works on current line)
|
|
1640
|
-
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
1641
|
-
const selectedFile = this.fileAutocomplete.matches[this.fileAutocomplete.selectedIndex];
|
|
1642
|
-
const currentLine = this.getCurrentLineContent();
|
|
1643
|
-
const result = replaceAtMention(currentLine, this.cursorPos, selectedFile.path);
|
|
1644
|
-
this.lines[this.currentLine] = result.input;
|
|
1645
|
-
this.cursorPos = result.cursorPos;
|
|
1646
|
-
this.fileAutocomplete.active = false;
|
|
1647
|
-
this.fileAutocomplete.matches = [];
|
|
1648
|
-
this.fileAutocomplete.selectedIndex = 0;
|
|
1649
|
-
this.fileAutocomplete.scrollOffset = 0;
|
|
1650
|
-
this.fileAutocomplete.partial = '';
|
|
1651
|
-
// Check if still in @ context (e.g., directory selected, might want to continue)
|
|
1652
|
-
this.updateAutocomplete();
|
|
1653
|
-
return;
|
|
1654
|
-
}
|
|
1655
|
-
// Command autocomplete (works on first line only)
|
|
1656
|
-
if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
|
|
1657
|
-
const selected = this.autocomplete.matches[this.autocomplete.selectedIndex];
|
|
1658
|
-
this.lines[0] = selected.command;
|
|
1659
|
-
this.currentLine = 0;
|
|
1660
|
-
this.cursorPos = this.lines[0].length;
|
|
1661
|
-
this.autocomplete.active = false;
|
|
1662
|
-
this.autocomplete.matches = [];
|
|
1663
|
-
this.autocomplete.selectedIndex = 0;
|
|
1664
|
-
this.autocomplete.scrollOffset = 0;
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
closeAutocomplete() {
|
|
1668
|
-
// Close file autocomplete
|
|
1669
|
-
this.fileAutocomplete.active = false;
|
|
1670
|
-
this.fileAutocomplete.matches = [];
|
|
1671
|
-
this.fileAutocomplete.selectedIndex = 0;
|
|
1672
|
-
this.fileAutocomplete.scrollOffset = 0;
|
|
1673
|
-
this.fileAutocomplete.partial = '';
|
|
1674
|
-
// Close command autocomplete
|
|
1675
|
-
this.autocomplete.active = false;
|
|
1676
|
-
this.autocomplete.matches = [];
|
|
1677
|
-
this.autocomplete.selectedIndex = 0;
|
|
1678
|
-
this.autocomplete.scrollOffset = 0;
|
|
1679
|
-
}
|
|
1680
|
-
navigateAutocompleteUp() {
|
|
1681
|
-
// File autocomplete navigation takes priority
|
|
1682
|
-
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
1683
|
-
if (this.fileAutocomplete.selectedIndex > 0) {
|
|
1684
|
-
this.fileAutocomplete.selectedIndex--;
|
|
1685
|
-
if (this.fileAutocomplete.selectedIndex < this.fileAutocomplete.scrollOffset) {
|
|
1686
|
-
this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex;
|
|
1687
|
-
}
|
|
1688
|
-
return true;
|
|
1689
|
-
}
|
|
1690
|
-
return false;
|
|
1691
|
-
}
|
|
1692
|
-
// Command autocomplete navigation
|
|
1693
|
-
if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
|
|
1694
|
-
return false;
|
|
1695
|
-
}
|
|
1696
|
-
if (this.autocomplete.selectedIndex > 0) {
|
|
1697
|
-
this.autocomplete.selectedIndex--;
|
|
1698
|
-
// Adjust scroll if selection goes above visible area
|
|
1699
|
-
if (this.autocomplete.selectedIndex < this.autocomplete.scrollOffset) {
|
|
1700
|
-
this.autocomplete.scrollOffset = this.autocomplete.selectedIndex;
|
|
1701
|
-
}
|
|
1702
|
-
return true;
|
|
1703
|
-
}
|
|
1704
|
-
return false;
|
|
1705
|
-
}
|
|
1706
|
-
navigateAutocompleteDown() {
|
|
1707
|
-
// File autocomplete navigation takes priority
|
|
1708
|
-
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
1709
|
-
if (this.fileAutocomplete.selectedIndex < this.fileAutocomplete.matches.length - 1) {
|
|
1710
|
-
this.fileAutocomplete.selectedIndex++;
|
|
1711
|
-
const maxVisibleIndex = this.fileAutocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
|
|
1712
|
-
if (this.fileAutocomplete.selectedIndex > maxVisibleIndex) {
|
|
1713
|
-
this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
|
|
1714
|
-
}
|
|
1715
|
-
return true;
|
|
1716
|
-
}
|
|
1717
|
-
return false;
|
|
1718
|
-
}
|
|
1719
|
-
// Command autocomplete navigation
|
|
1720
|
-
if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
|
|
1721
|
-
return false;
|
|
1722
|
-
}
|
|
1723
|
-
if (this.autocomplete.selectedIndex < this.autocomplete.matches.length - 1) {
|
|
1724
|
-
this.autocomplete.selectedIndex++;
|
|
1725
|
-
// Adjust scroll if selection goes below visible area
|
|
1726
|
-
const maxVisibleIndex = this.autocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
|
|
1727
|
-
if (this.autocomplete.selectedIndex > maxVisibleIndex) {
|
|
1728
|
-
this.autocomplete.scrollOffset = this.autocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
|
|
1729
|
-
}
|
|
1730
|
-
return true;
|
|
1731
|
-
}
|
|
1732
|
-
return false;
|
|
1733
|
-
}
|
|
1734
|
-
// ===========================================================================
|
|
1735
|
-
// Private - History
|
|
1736
|
-
// ===========================================================================
|
|
1737
|
-
addToHistory(input) {
|
|
1738
|
-
const trimmed = input.trim();
|
|
1739
|
-
// Don't add empty or duplicate consecutive entries
|
|
1740
|
-
if (trimmed && (this.history.length === 0 || this.history[this.history.length - 1] !== trimmed)) {
|
|
1741
|
-
this.history.push(trimmed);
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
navigateHistoryUp() {
|
|
1745
|
-
if (this.autocomplete.active)
|
|
1746
|
-
return false;
|
|
1747
|
-
if (this.history.length === 0)
|
|
1748
|
-
return false;
|
|
1749
|
-
// Save current input when starting to navigate
|
|
1750
|
-
if (this.historyIndex === -1) {
|
|
1751
|
-
this.savedInput = this.getInputValue();
|
|
1752
|
-
}
|
|
1753
|
-
// Navigate to older entry
|
|
1754
|
-
if (this.historyIndex < this.history.length - 1) {
|
|
1755
|
-
this.historyIndex++;
|
|
1756
|
-
const historyEntry = this.history[this.history.length - 1 - this.historyIndex];
|
|
1757
|
-
// History entries are single-line (newlines stripped on save)
|
|
1758
|
-
this.lines = [historyEntry];
|
|
1759
|
-
this.currentLine = 0;
|
|
1760
|
-
this.cursorPos = historyEntry.length;
|
|
1761
|
-
return true;
|
|
1762
|
-
}
|
|
1763
|
-
return false;
|
|
1764
|
-
}
|
|
1765
|
-
navigateHistoryDown() {
|
|
1766
|
-
if (this.autocomplete.active)
|
|
1767
|
-
return false;
|
|
1768
|
-
if (this.historyIndex < 0)
|
|
1769
|
-
return false;
|
|
1770
|
-
this.historyIndex--;
|
|
1771
|
-
if (this.historyIndex === -1) {
|
|
1772
|
-
// Restore saved input (may be multiline)
|
|
1773
|
-
this.lines = this.savedInput.split('\n');
|
|
1774
|
-
if (this.lines.length === 0)
|
|
1775
|
-
this.lines = [''];
|
|
1776
|
-
this.currentLine = this.lines.length - 1;
|
|
1777
|
-
this.cursorPos = this.lines[this.currentLine].length;
|
|
1778
|
-
}
|
|
1779
|
-
else {
|
|
1780
|
-
// Navigate to newer entry
|
|
1781
|
-
const historyEntry = this.history[this.history.length - 1 - this.historyIndex];
|
|
1782
|
-
this.lines = [historyEntry];
|
|
1783
|
-
this.currentLine = 0;
|
|
1784
|
-
this.cursorPos = historyEntry.length;
|
|
1785
|
-
}
|
|
1786
|
-
return true;
|
|
1787
|
-
}
|
|
1788
|
-
resetHistoryNavigation() {
|
|
1789
|
-
this.historyIndex = -1;
|
|
1790
|
-
this.savedInput = '';
|
|
1791
|
-
}
|
|
1792
|
-
// ===========================================================================
|
|
1793
|
-
// Private - Spinner animation
|
|
1794
|
-
// ===========================================================================
|
|
1795
|
-
startSpinnerAnimation() {
|
|
1796
|
-
if (this.spinnerTimer)
|
|
1797
|
-
return;
|
|
1798
|
-
this.spinnerTimer = setInterval(() => {
|
|
1799
|
-
this.spinnerFrame++;
|
|
1800
|
-
this.needsRender = true;
|
|
1801
|
-
}, 200); // Slower, more relaxed animation
|
|
1802
|
-
}
|
|
1803
|
-
stopSpinnerAnimation() {
|
|
1804
|
-
if (this.spinnerTimer) {
|
|
1805
|
-
clearInterval(this.spinnerTimer);
|
|
1806
|
-
this.spinnerTimer = null;
|
|
1807
|
-
}
|
|
1808
|
-
this.spinnerFrame = 0;
|
|
1809
|
-
}
|
|
1810
|
-
// ===========================================================================
|
|
1811
|
-
// Private - Keyboard handling
|
|
1812
|
-
// ===========================================================================
|
|
1813
|
-
keyHandler = null;
|
|
1814
|
-
dataHandler = null;
|
|
1815
|
-
startKeyboardCapture() {
|
|
1816
|
-
if (this.keyHandler)
|
|
1817
|
-
return; // Already capturing
|
|
1818
|
-
readline.emitKeypressEvents(process.stdin);
|
|
1819
|
-
if (process.stdin.isTTY) {
|
|
1820
|
-
process.stdin.setRawMode(true);
|
|
1821
|
-
}
|
|
1822
|
-
// Handle raw data for reliable Escape and Option+Arrow detection
|
|
1823
|
-
this.dataHandler = (data) => {
|
|
1824
|
-
if (!this.isRunning || this.isPaused)
|
|
1825
|
-
return;
|
|
1826
|
-
// Pure Escape key is a single byte 0x1B
|
|
1827
|
-
if (data.length === 1 && data[0] === 0x1b) {
|
|
1828
|
-
this.handleEscape();
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
// Mac Option+Left (word left): ESC b or ESC [ 1 ; 9 D
|
|
1832
|
-
const isOptionLeft = (data.length === 2 && data[0] === 0x1b && data[1] === 0x62) ||
|
|
1833
|
-
(data.length === 6 &&
|
|
1834
|
-
data[0] === 0x1b &&
|
|
1835
|
-
data[1] === 0x5b &&
|
|
1836
|
-
data[2] === 0x31 &&
|
|
1837
|
-
data[3] === 0x3b &&
|
|
1838
|
-
data[4] === 0x39 &&
|
|
1839
|
-
data[5] === 0x44);
|
|
1840
|
-
// Mac Option+Right (word right): ESC f or ESC [ 1 ; 9 C
|
|
1841
|
-
const isOptionRight = (data.length === 2 && data[0] === 0x1b && data[1] === 0x66) ||
|
|
1842
|
-
(data.length === 6 &&
|
|
1843
|
-
data[0] === 0x1b &&
|
|
1844
|
-
data[1] === 0x5b &&
|
|
1845
|
-
data[2] === 0x31 &&
|
|
1846
|
-
data[3] === 0x3b &&
|
|
1847
|
-
data[4] === 0x39 &&
|
|
1848
|
-
data[5] === 0x43);
|
|
1849
|
-
if (isOptionLeft) {
|
|
1850
|
-
this.handleWordLeft();
|
|
1851
|
-
return;
|
|
1852
|
-
}
|
|
1853
|
-
if (isOptionRight) {
|
|
1854
|
-
this.handleWordRight();
|
|
1855
|
-
return;
|
|
1856
|
-
}
|
|
1857
|
-
// Home/Cmd+Left: Ctrl+A (\x01) or ESC [ H
|
|
1858
|
-
const isHome = (data.length === 1 && data[0] === 0x01) ||
|
|
1859
|
-
(data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x48);
|
|
1860
|
-
// End/Cmd+Right: Ctrl+E (\x05) or ESC [ F
|
|
1861
|
-
const isEnd = (data.length === 1 && data[0] === 0x05) ||
|
|
1862
|
-
(data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x46);
|
|
1863
|
-
if (isHome) {
|
|
1864
|
-
this.cursorPos = 0;
|
|
1865
|
-
this.needsRender = true;
|
|
1866
|
-
return;
|
|
1867
|
-
}
|
|
1868
|
-
if (isEnd) {
|
|
1869
|
-
this.cursorPos = this.lines[this.currentLine].length;
|
|
1870
|
-
this.needsRender = true;
|
|
1871
|
-
return;
|
|
1872
|
-
}
|
|
1873
|
-
};
|
|
1874
|
-
this.keyHandler = (str, key) => {
|
|
1875
|
-
if (!this.isRunning || this.isPaused)
|
|
1876
|
-
return;
|
|
1877
|
-
// Skip escape here - handled by dataHandler
|
|
1878
|
-
if (key.name === 'escape')
|
|
1879
|
-
return;
|
|
1880
|
-
this.handleKeypress(str, key);
|
|
1881
|
-
};
|
|
1882
|
-
process.stdin.on('data', this.dataHandler);
|
|
1883
|
-
process.stdin.on('keypress', this.keyHandler);
|
|
1884
|
-
process.stdin.resume();
|
|
1885
|
-
}
|
|
1886
|
-
stopKeyboardCapture() {
|
|
1887
|
-
if (this.dataHandler) {
|
|
1888
|
-
process.stdin.removeListener('data', this.dataHandler);
|
|
1889
|
-
this.dataHandler = null;
|
|
1890
|
-
}
|
|
1891
|
-
if (this.keyHandler) {
|
|
1892
|
-
process.stdin.removeListener('keypress', this.keyHandler);
|
|
1893
|
-
this.keyHandler = null;
|
|
1894
|
-
}
|
|
1895
|
-
if (process.stdin.isTTY) {
|
|
1896
|
-
process.stdin.setRawMode(false);
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
/**
|
|
1900
|
-
* Handle word left (Option+Left) - move cursor to previous word boundary
|
|
1901
|
-
*/
|
|
1902
|
-
handleWordLeft() {
|
|
1903
|
-
const line = this.lines[this.currentLine];
|
|
1904
|
-
if (this.cursorPos > 0) {
|
|
1905
|
-
let pos = this.cursorPos;
|
|
1906
|
-
// Skip spaces
|
|
1907
|
-
while (pos > 0 && line[pos - 1] === ' ')
|
|
1908
|
-
pos--;
|
|
1909
|
-
// Skip word
|
|
1910
|
-
while (pos > 0 && line[pos - 1] !== ' ')
|
|
1911
|
-
pos--;
|
|
1912
|
-
this.cursorPos = pos;
|
|
1913
|
-
this.needsRender = true;
|
|
1914
|
-
}
|
|
1915
|
-
else if (this.currentLine > 0) {
|
|
1916
|
-
// Move to end of previous line
|
|
1917
|
-
this.currentLine--;
|
|
1918
|
-
this.cursorPos = this.lines[this.currentLine].length;
|
|
1919
|
-
this.needsRender = true;
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
/**
|
|
1923
|
-
* Handle word right (Option+Right) - move cursor to next word boundary
|
|
1924
|
-
*/
|
|
1925
|
-
handleWordRight() {
|
|
1926
|
-
const line = this.lines[this.currentLine];
|
|
1927
|
-
if (this.cursorPos < line.length) {
|
|
1928
|
-
let pos = this.cursorPos;
|
|
1929
|
-
// Skip word
|
|
1930
|
-
while (pos < line.length && line[pos] !== ' ')
|
|
1931
|
-
pos++;
|
|
1932
|
-
// Skip spaces
|
|
1933
|
-
while (pos < line.length && line[pos] === ' ')
|
|
1934
|
-
pos++;
|
|
1935
|
-
this.cursorPos = pos;
|
|
1936
|
-
this.needsRender = true;
|
|
1937
|
-
}
|
|
1938
|
-
else if (this.currentLine < this.lines.length - 1) {
|
|
1939
|
-
// Move to start of next line
|
|
1940
|
-
this.currentLine++;
|
|
1941
|
-
this.cursorPos = 0;
|
|
1942
|
-
this.needsRender = true;
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
/**
|
|
1946
|
-
* Handle Escape key - called from raw data handler for reliable detection
|
|
1947
|
-
*/
|
|
1948
|
-
handleEscape() {
|
|
1949
|
-
// In verbose-temp mode, escape returns to normal
|
|
1950
|
-
if (this.viewMode === 'verbose-temp') {
|
|
1951
|
-
this.viewMode = 'normal';
|
|
1952
|
-
this.reRenderConversationVerbose(false);
|
|
1953
|
-
return;
|
|
1954
|
-
}
|
|
1955
|
-
// Route escape to active overlay if present
|
|
1956
|
-
const overlay = this.getActiveOverlay();
|
|
1957
|
-
if (overlay) {
|
|
1958
|
-
const keyEvent = {
|
|
1959
|
-
raw: Buffer.from([0x1b]),
|
|
1960
|
-
name: 'escape',
|
|
1961
|
-
ctrl: false,
|
|
1962
|
-
shift: false,
|
|
1963
|
-
meta: false,
|
|
1964
|
-
};
|
|
1965
|
-
const actionOrPromise = overlay.handleKey(keyEvent);
|
|
1966
|
-
// Handle both sync and async handleKey
|
|
1967
|
-
if (actionOrPromise instanceof Promise) {
|
|
1968
|
-
actionOrPromise
|
|
1969
|
-
.then((action) => { this.processOverlayAction(action); })
|
|
1970
|
-
.catch((err) => { console.error('Overlay handleKey error:', err); });
|
|
1971
|
-
}
|
|
1972
|
-
else {
|
|
1973
|
-
this.processOverlayAction(actionOrPromise);
|
|
1974
|
-
}
|
|
1975
|
-
return;
|
|
1976
|
-
}
|
|
1977
|
-
const now = Date.now();
|
|
1978
|
-
const timeSinceLastEsc = now - this.lastEscapeTime;
|
|
1979
|
-
const isDoubleEsc = timeSinceLastEsc < 500;
|
|
1980
|
-
this.lastEscapeTime = now;
|
|
1981
|
-
if (this.fileAutocomplete.active) {
|
|
1982
|
-
// 1. Close file autocomplete first
|
|
1983
|
-
this.closeAutocomplete();
|
|
1984
|
-
this.needsRender = true;
|
|
1985
|
-
}
|
|
1986
|
-
else if (this.autocomplete.active) {
|
|
1987
|
-
// 2. Close command autocomplete
|
|
1988
|
-
this.closeAutocomplete();
|
|
1989
|
-
this.needsRender = true;
|
|
1990
|
-
}
|
|
1991
|
-
else if (isDoubleEsc && this.getInputValue().length > 0) {
|
|
1992
|
-
// 3. Double Esc clears input (if there's content)
|
|
1993
|
-
this.clearInput();
|
|
1994
|
-
this.resetHistoryNavigation();
|
|
1995
|
-
this.needsRender = true;
|
|
1996
|
-
}
|
|
1997
|
-
else if (this.agentRunning) {
|
|
1998
|
-
// 4. Single Esc cancels agent
|
|
1999
|
-
this.emit('cancel');
|
|
2000
|
-
}
|
|
2001
|
-
else {
|
|
2002
|
-
// 5. Single Esc emits escape
|
|
2003
|
-
this.emit('escape');
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
/**
|
|
2007
|
-
* Handle keypress when overlay is active.
|
|
2008
|
-
* Converts keyboard event to KeyEvent and routes to overlay.
|
|
2009
|
-
*/
|
|
2010
|
-
handleOverlayKeypress(str, key) {
|
|
2011
|
-
const overlay = this.getActiveOverlay();
|
|
2012
|
-
if (!overlay)
|
|
2013
|
-
return;
|
|
2014
|
-
// Build KeyEvent from keypress data
|
|
2015
|
-
const rawData = key.sequence ?? str;
|
|
2016
|
-
const keyName = key.name ?? (str ? str.toLowerCase() : '');
|
|
2017
|
-
const keyEvent = {
|
|
2018
|
-
raw: Buffer.from(rawData),
|
|
2019
|
-
name: keyName,
|
|
2020
|
-
char: str && str.length === 1 && str.charCodeAt(0) >= 32 ? str : undefined,
|
|
2021
|
-
ctrl: key.ctrl ?? false,
|
|
2022
|
-
shift: key.shift ?? false,
|
|
2023
|
-
meta: key.meta ?? false,
|
|
2024
|
-
};
|
|
2025
|
-
const actionOrPromise = overlay.handleKey(keyEvent);
|
|
2026
|
-
// Handle both sync and async handleKey
|
|
2027
|
-
if (actionOrPromise instanceof Promise) {
|
|
2028
|
-
actionOrPromise
|
|
2029
|
-
.then((action) => { this.processOverlayAction(action); })
|
|
2030
|
-
.catch((err) => { console.error('Overlay handleKey error:', err); });
|
|
2031
|
-
}
|
|
2032
|
-
else {
|
|
2033
|
-
this.processOverlayAction(actionOrPromise);
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
handleKeypress(str, key) {
|
|
2037
|
-
// Route to active overlay if present
|
|
2038
|
-
if (this.hasActiveOverlay()) {
|
|
2039
|
-
this.handleOverlayKeypress(str, key);
|
|
2040
|
-
return;
|
|
2041
|
-
}
|
|
2042
|
-
// In verbose-temp mode, any key (except Ctrl+O) returns to normal
|
|
2043
|
-
if (this.viewMode === 'verbose-temp') {
|
|
2044
|
-
// Ctrl+O toggles back
|
|
2045
|
-
if (key.ctrl && key.name === 'o') {
|
|
2046
|
-
this.toggleVerboseView();
|
|
2047
|
-
this.toggleLiveRegionExpanded();
|
|
2048
|
-
return;
|
|
2049
|
-
}
|
|
2050
|
-
// Any other key returns to normal
|
|
2051
|
-
this.viewMode = 'normal';
|
|
2052
|
-
this.reRenderConversationVerbose(false);
|
|
2053
|
-
// Don't consume the key - let it be processed normally
|
|
2054
|
-
}
|
|
2055
|
-
// Ctrl+C - interrupt/exit
|
|
2056
|
-
if (key.ctrl && key.name === 'c') {
|
|
2057
|
-
this.emit('interrupt');
|
|
2058
|
-
return;
|
|
2059
|
-
}
|
|
2060
|
-
// Ctrl+T - toggle todo list visibility
|
|
2061
|
-
if (key.ctrl && key.name === 't') {
|
|
2062
|
-
this.toggleTodos();
|
|
2063
|
-
return;
|
|
2064
|
-
}
|
|
2065
|
-
// Ctrl+O - toggle verbose view mode AND LiveRegion expansion
|
|
2066
|
-
if (key.ctrl && key.name === 'o') {
|
|
2067
|
-
this.toggleVerboseView();
|
|
2068
|
-
this.toggleLiveRegionExpanded();
|
|
2069
|
-
return;
|
|
2070
|
-
}
|
|
2071
|
-
// Tab - accept suggestion, or autocomplete
|
|
2072
|
-
if (key.name === 'tab' && !key.shift) {
|
|
2073
|
-
// Accept ghost text suggestion if input is empty
|
|
2074
|
-
if (this.suggestion && this.getInputValue() === '') {
|
|
2075
|
-
this.lines[0] = this.suggestion;
|
|
2076
|
-
this.cursorPos = this.suggestion.length;
|
|
2077
|
-
this.suggestion = null;
|
|
2078
|
-
this.needsRender = true;
|
|
2079
|
-
return;
|
|
2080
|
-
}
|
|
2081
|
-
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
2082
|
-
this.acceptAutocomplete();
|
|
2083
|
-
this.needsRender = true;
|
|
2084
|
-
return;
|
|
2085
|
-
}
|
|
2086
|
-
if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
|
|
2087
|
-
this.acceptAutocomplete();
|
|
2088
|
-
this.needsRender = true;
|
|
2089
|
-
return;
|
|
2090
|
-
}
|
|
2091
|
-
// Otherwise ignore tab (or could insert spaces)
|
|
2092
|
-
return;
|
|
2093
|
-
}
|
|
2094
|
-
// Enter - handle multiline continuation, autocomplete, then execute
|
|
2095
|
-
if (key.name === 'return') {
|
|
2096
|
-
// Check for backslash continuation at end of current line
|
|
2097
|
-
const currentLine = this.getCurrentLineContent();
|
|
2098
|
-
if (currentLine.endsWith('\\')) {
|
|
2099
|
-
// Remove backslash and add new line
|
|
2100
|
-
this.lines[this.currentLine] = currentLine.slice(0, -1);
|
|
2101
|
-
this.lines.push('');
|
|
2102
|
-
this.currentLine++;
|
|
2103
|
-
this.cursorPos = 0;
|
|
2104
|
-
this.closeAutocomplete();
|
|
2105
|
-
this.resetHistoryNavigation();
|
|
2106
|
-
this.needsRender = true;
|
|
2107
|
-
return;
|
|
2108
|
-
}
|
|
2109
|
-
// If file autocomplete is active, accept the selection and submit
|
|
2110
|
-
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
2111
|
-
this.acceptAutocomplete();
|
|
2112
|
-
// Fall through to submit the message
|
|
2113
|
-
}
|
|
2114
|
-
// If command autocomplete is active, accept the selection first
|
|
2115
|
-
if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
|
|
2116
|
-
this.acceptAutocomplete();
|
|
2117
|
-
// Fall through to execute the command
|
|
2118
|
-
}
|
|
2119
|
-
let input = this.getInputValue().trim();
|
|
2120
|
-
// If input is empty but we have a ghost text suggestion, accept it
|
|
2121
|
-
if (!input && this.suggestion) {
|
|
2122
|
-
input = this.suggestion;
|
|
2123
|
-
this.suggestion = null;
|
|
2124
|
-
}
|
|
2125
|
-
if (input) {
|
|
2126
|
-
// Add to history before processing (store as single line for history)
|
|
2127
|
-
this.addToHistory(input.replace(/\n/g, ' '));
|
|
2128
|
-
this.resetHistoryNavigation();
|
|
2129
|
-
if (this.agentRunning) {
|
|
2130
|
-
this.queuedInputs.push(input);
|
|
2131
|
-
this.needsRender = true;
|
|
2132
|
-
}
|
|
2133
|
-
else if (input.startsWith('/')) {
|
|
2134
|
-
const spaceIndex = input.indexOf(' ');
|
|
2135
|
-
const cmd = spaceIndex > 0 ? input.slice(1, spaceIndex) : input.slice(1);
|
|
2136
|
-
const args = spaceIndex > 0 ? input.slice(spaceIndex + 1) : '';
|
|
2137
|
-
this.closeAutocomplete();
|
|
2138
|
-
this.emit('command', cmd, args);
|
|
2139
|
-
}
|
|
2140
|
-
else {
|
|
2141
|
-
this.emit('submit', input);
|
|
2142
|
-
}
|
|
2143
|
-
this.clearInput();
|
|
2144
|
-
this.closeAutocomplete();
|
|
2145
|
-
this.needsRender = true;
|
|
2146
|
-
}
|
|
2147
|
-
return;
|
|
2148
|
-
}
|
|
2149
|
-
// Arrow Up - autocomplete > multiline navigation > history
|
|
2150
|
-
if (key.name === 'up') {
|
|
2151
|
-
// 1. Autocomplete navigation takes priority
|
|
2152
|
-
if (this.navigateAutocompleteUp()) {
|
|
2153
|
-
this.needsRender = true;
|
|
2154
|
-
return;
|
|
2155
|
-
}
|
|
2156
|
-
// 2. Navigate to previous line in multiline input
|
|
2157
|
-
if (this.currentLine > 0) {
|
|
2158
|
-
this.currentLine--;
|
|
2159
|
-
// Try to keep same cursor position, but clamp to line length
|
|
2160
|
-
this.cursorPos = Math.min(this.cursorPos, this.lines[this.currentLine].length);
|
|
2161
|
-
this.needsRender = true;
|
|
2162
|
-
return;
|
|
2163
|
-
}
|
|
2164
|
-
// 3. History navigation (only when at first line)
|
|
2165
|
-
if (this.navigateHistoryUp()) {
|
|
2166
|
-
this.closeAutocomplete();
|
|
2167
|
-
this.needsRender = true;
|
|
2168
|
-
}
|
|
2169
|
-
return;
|
|
2170
|
-
}
|
|
2171
|
-
// Arrow Down - autocomplete > multiline navigation > history
|
|
2172
|
-
if (key.name === 'down') {
|
|
2173
|
-
// 1. Autocomplete navigation takes priority
|
|
2174
|
-
if (this.navigateAutocompleteDown()) {
|
|
2175
|
-
this.needsRender = true;
|
|
2176
|
-
return;
|
|
2177
|
-
}
|
|
2178
|
-
// 2. Navigate to next line in multiline input
|
|
2179
|
-
if (this.currentLine < this.lines.length - 1) {
|
|
2180
|
-
this.currentLine++;
|
|
2181
|
-
// Try to keep same cursor position, but clamp to line length
|
|
2182
|
-
this.cursorPos = Math.min(this.cursorPos, this.lines[this.currentLine].length);
|
|
2183
|
-
this.needsRender = true;
|
|
2184
|
-
return;
|
|
2185
|
-
}
|
|
2186
|
-
// 3. History forward (only when at last line)
|
|
2187
|
-
if (this.historyIndex >= 0 && this.navigateHistoryDown()) {
|
|
2188
|
-
this.needsRender = true;
|
|
2189
|
-
}
|
|
2190
|
-
return;
|
|
2191
|
-
}
|
|
2192
|
-
// Backspace
|
|
2193
|
-
if (key.name === 'backspace') {
|
|
2194
|
-
if (this.cursorPos > 0) {
|
|
2195
|
-
// Delete character in current line
|
|
2196
|
-
const line = this.lines[this.currentLine];
|
|
2197
|
-
this.lines[this.currentLine] = line.slice(0, this.cursorPos - 1) + line.slice(this.cursorPos);
|
|
2198
|
-
this.cursorPos--;
|
|
2199
|
-
this.resetHistoryNavigation();
|
|
2200
|
-
this.updateAutocomplete();
|
|
2201
|
-
this.needsRender = true;
|
|
2202
|
-
}
|
|
2203
|
-
else if (this.currentLine > 0) {
|
|
2204
|
-
// At start of line - merge with previous line
|
|
2205
|
-
const currentLine = this.lines[this.currentLine];
|
|
2206
|
-
const prevLine = this.lines[this.currentLine - 1];
|
|
2207
|
-
this.lines[this.currentLine - 1] = prevLine + currentLine;
|
|
2208
|
-
this.lines.splice(this.currentLine, 1);
|
|
2209
|
-
this.currentLine--;
|
|
2210
|
-
this.cursorPos = prevLine.length;
|
|
2211
|
-
this.resetHistoryNavigation();
|
|
2212
|
-
this.updateAutocomplete();
|
|
2213
|
-
this.needsRender = true;
|
|
2214
|
-
}
|
|
2215
|
-
return;
|
|
2216
|
-
}
|
|
2217
|
-
// Delete
|
|
2218
|
-
if (key.name === 'delete') {
|
|
2219
|
-
const line = this.lines[this.currentLine];
|
|
2220
|
-
if (this.cursorPos < line.length) {
|
|
2221
|
-
// Delete character in current line
|
|
2222
|
-
this.lines[this.currentLine] = line.slice(0, this.cursorPos) + line.slice(this.cursorPos + 1);
|
|
2223
|
-
this.resetHistoryNavigation();
|
|
2224
|
-
this.updateAutocomplete();
|
|
2225
|
-
this.needsRender = true;
|
|
2226
|
-
}
|
|
2227
|
-
else if (this.currentLine < this.lines.length - 1) {
|
|
2228
|
-
// At end of line - merge with next line
|
|
2229
|
-
const nextLine = this.lines[this.currentLine + 1];
|
|
2230
|
-
this.lines[this.currentLine] = line + nextLine;
|
|
2231
|
-
this.lines.splice(this.currentLine + 1, 1);
|
|
2232
|
-
this.resetHistoryNavigation();
|
|
2233
|
-
this.updateAutocomplete();
|
|
2234
|
-
this.needsRender = true;
|
|
2235
|
-
}
|
|
2236
|
-
return;
|
|
2237
|
-
}
|
|
2238
|
-
// Arrow keys (left/right for cursor movement with multiline support)
|
|
2239
|
-
if (key.name === 'left') {
|
|
2240
|
-
if (this.cursorPos > 0) {
|
|
2241
|
-
this.cursorPos--;
|
|
2242
|
-
this.needsRender = true;
|
|
2243
|
-
}
|
|
2244
|
-
else if (this.currentLine > 0) {
|
|
2245
|
-
// Move to end of previous line
|
|
2246
|
-
this.currentLine--;
|
|
2247
|
-
this.cursorPos = this.lines[this.currentLine].length;
|
|
2248
|
-
this.needsRender = true;
|
|
2249
|
-
}
|
|
2250
|
-
return;
|
|
2251
|
-
}
|
|
2252
|
-
if (key.name === 'right') {
|
|
2253
|
-
const lineLen = this.lines[this.currentLine].length;
|
|
2254
|
-
if (this.cursorPos < lineLen) {
|
|
2255
|
-
this.cursorPos++;
|
|
2256
|
-
this.needsRender = true;
|
|
2257
|
-
}
|
|
2258
|
-
else if (this.currentLine < this.lines.length - 1) {
|
|
2259
|
-
// Move to start of next line
|
|
2260
|
-
this.currentLine++;
|
|
2261
|
-
this.cursorPos = 0;
|
|
2262
|
-
this.needsRender = true;
|
|
2263
|
-
}
|
|
2264
|
-
return;
|
|
2265
|
-
}
|
|
2266
|
-
if (key.name === 'home') {
|
|
2267
|
-
this.cursorPos = 0;
|
|
2268
|
-
this.needsRender = true;
|
|
2269
|
-
return;
|
|
2270
|
-
}
|
|
2271
|
-
if (key.name === 'end') {
|
|
2272
|
-
this.cursorPos = this.lines[this.currentLine].length;
|
|
2273
|
-
this.needsRender = true;
|
|
2274
|
-
return;
|
|
2275
|
-
}
|
|
2276
|
-
// Shift+Tab - mode change
|
|
2277
|
-
if (key.shift && key.name === 'tab') {
|
|
2278
|
-
this.emit('modeChange');
|
|
2279
|
-
return;
|
|
2280
|
-
}
|
|
2281
|
-
// Regular character
|
|
2282
|
-
if (str && str.length === 1 && str.charCodeAt(0) >= 32) {
|
|
2283
|
-
// Clear ghost text suggestion when user starts typing
|
|
2284
|
-
if (this.suggestion) {
|
|
2285
|
-
this.suggestion = null;
|
|
2286
|
-
}
|
|
2287
|
-
const line = this.lines[this.currentLine];
|
|
2288
|
-
this.lines[this.currentLine] =
|
|
2289
|
-
line.slice(0, this.cursorPos) + str + line.slice(this.cursorPos);
|
|
2290
|
-
this.cursorPos++;
|
|
2291
|
-
this.resetHistoryNavigation();
|
|
2292
|
-
this.updateAutocomplete();
|
|
2293
|
-
this.needsRender = true;
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
2296
819
|
}
|