@gajae-code/coding-agent 0.11.1 → 0.11.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/CHANGELOG.md +54 -0
- package/dist/types/cli/config-cli.d.ts +11 -1
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/cli/session-picker.d.ts +2 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/mcp-serve.d.ts +33 -0
- package/dist/types/commands/mcp.d.ts +1 -1
- package/dist/types/commands/notify.d.ts +0 -2
- package/dist/types/commands/sdk.d.ts +1 -1
- package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
- package/dist/types/config/file-lock.d.ts +14 -4
- package/dist/types/config/keybindings.d.ts +42 -1
- package/dist/types/config/model-auth.d.ts +3 -0
- package/dist/types/config/model-bindings-applier.d.ts +13 -0
- package/dist/types/config/model-discovery-manager.d.ts +49 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-registry.d.ts +16 -15
- package/dist/types/config/model-resolver.d.ts +28 -18
- package/dist/types/config/models-config-schema.d.ts +11 -0
- package/dist/types/config/settings-schema.d.ts +56 -1
- package/dist/types/config/settings.d.ts +14 -11
- package/dist/types/coordinator-mcp/policy.d.ts +5 -0
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +0 -6
- package/dist/types/edit/index.d.ts +3 -0
- package/dist/types/eval/py/env.d.ts +9 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +34 -3
- package/dist/types/extensibility/hooks/runner.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
- package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
- package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
- package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
- package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
- package/dist/types/gjc-runtime/team-store.d.ts +160 -0
- package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
- package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
- package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/goals/state.d.ts +8 -0
- package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
- package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/state.d.ts +11 -4
- package/dist/types/lsp/lspmux.d.ts +5 -3
- package/dist/types/lsp/path-trust.d.ts +1 -0
- package/dist/types/main.d.ts +3 -2
- package/dist/types/memories/storage.d.ts +1 -0
- package/dist/types/modes/action-registry.d.ts +29 -0
- package/dist/types/modes/components/chrome.d.ts +14 -0
- package/dist/types/modes/components/command-palette.d.ts +27 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
- package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
- package/dist/types/modes/components/queue-pane.d.ts +12 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
- package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/show-images-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +1 -94
- package/dist/types/modes/components/tasks-pane.d.ts +15 -0
- package/dist/types/modes/components/theme-selector.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/tool-status-header.d.ts +111 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -1
- package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
- package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
- package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
- package/dist/types/modes/interactive-mode.d.ts +19 -17
- package/dist/types/modes/jobs-observer.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
- package/dist/types/modes/tasks-aggregator.d.ts +39 -0
- package/dist/types/modes/transcript-item-registry.d.ts +60 -0
- package/dist/types/modes/types.d.ts +20 -13
- package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +78 -12
- package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
- package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
- package/dist/types/sdk/broker/session-index.d.ts +0 -1
- package/dist/types/sdk/broker/state-version.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
- package/dist/types/sdk/bus/config-commands.d.ts +1 -1
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/html-format.d.ts +3 -2
- package/dist/types/sdk/bus/index.d.ts +27 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/notification-service.d.ts +20 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
- package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
- package/dist/types/sdk/bus/rich-document.d.ts +7 -0
- package/dist/types/sdk/bus/rich-render.d.ts +18 -41
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
- package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/session-directory.d.ts +56 -0
- package/dist/types/session/agent-session.d.ts +82 -47
- package/dist/types/session/agent-storage.d.ts +7 -0
- package/dist/types/session/fallback-chain-controller.d.ts +10 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
- package/dist/types/session/messages.d.ts +16 -0
- package/dist/types/session/session-manager-internal.d.ts +14 -0
- package/dist/types/session/session-manager.d.ts +82 -12
- package/dist/types/session/session-storage.d.ts +43 -3
- package/dist/types/session/volatile-context-pruning.d.ts +15 -0
- package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
- package/dist/types/system-prompt.d.ts +2 -11
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/thinking.d.ts +7 -0
- package/dist/types/tool-discovery/tool-index.d.ts +1 -50
- package/dist/types/tools/ask.d.ts +64 -2
- package/dist/types/tools/bash.d.ts +3 -0
- package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
- package/dist/types/tools/cron.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -0
- package/dist/types/tools/index.d.ts +30 -15
- package/dist/types/tools/output-meta.d.ts +1 -0
- package/dist/types/tools/read.d.ts +3 -0
- package/dist/types/tools/search-tool-bm25.d.ts +3 -4
- package/dist/types/utils/event-bus.d.ts +1 -1
- package/dist/types/web/search/index.d.ts +3 -1
- package/package.json +21 -9
- package/scripts/build-sdk-package-smoke.ts +34 -1
- package/scripts/generate-hotkeys-docs.ts +74 -0
- package/scripts/generate-sdk-operation-inventory.ts +64 -20
- package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
- package/src/async/job-manager.ts +49 -6
- package/src/cli/config-cli.ts +75 -2
- package/src/cli/notify-cli.ts +92 -26
- package/src/cli/session-picker.ts +20 -3
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/commands/harness.ts +34 -106
- package/src/commands/launch.ts +6 -4
- package/src/commands/mcp-serve.ts +85 -8
- package/src/commands/mcp.ts +2 -2
- package/src/commands/notify.ts +8 -4
- package/src/commands/sdk.ts +61 -40
- package/src/commands/ultragoal.ts +4 -1
- package/src/config/atomic-yaml-patch.ts +80 -13
- package/src/config/file-lock.ts +105 -43
- package/src/config/keybindings.ts +64 -20
- package/src/config/model-auth.ts +9 -0
- package/src/config/model-bindings-applier.ts +139 -0
- package/src/config/model-discovery-manager.ts +224 -0
- package/src/config/model-equivalence.ts +66 -0
- package/src/config/model-registry.ts +169 -302
- package/src/config/model-resolver.ts +361 -369
- package/src/config/models-config-schema.ts +5 -4
- package/src/config/settings-schema.ts +159 -2
- package/src/config/settings.ts +458 -482
- package/src/coordinator-mcp/policy.ts +76 -11
- package/src/coordinator-mcp/question-gate-codec.ts +274 -0
- package/src/coordinator-mcp/question-state.ts +778 -0
- package/src/coordinator-mcp/server.ts +1702 -238
- package/src/debug/index.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
- package/src/defaults/gjc/skills/team/SKILL.md +16 -0
- package/src/discovery/helpers.ts +1 -50
- package/src/edit/index.ts +64 -0
- package/src/eval/py/env.ts +26 -0
- package/src/eval/py/kernel.ts +5 -3
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +12 -3
- package/src/extensibility/extensions/types.ts +39 -1
- package/src/extensibility/gjc-plugins/installer.ts +16 -16
- package/src/extensibility/hooks/runner.ts +2 -2
- package/src/extensibility/runtime-skill-discovery.ts +33 -13
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
- package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
- package/src/gjc-runtime/deep-interview-state.ts +317 -5
- package/src/gjc-runtime/goal-mode-request.ts +55 -8
- package/src/gjc-runtime/launch-tmux.ts +2 -5
- package/src/gjc-runtime/launch-worktree.ts +5 -5
- package/src/gjc-runtime/linux-proc.ts +71 -0
- package/src/gjc-runtime/ralplan-runtime.ts +74 -90
- package/src/gjc-runtime/session-resolution.ts +18 -3
- package/src/gjc-runtime/session-state-sidecar.ts +6 -107
- package/src/gjc-runtime/state-migrations.ts +1 -1
- package/src/gjc-runtime/state-runtime.ts +320 -249
- package/src/gjc-runtime/state-writer.ts +36 -40
- package/src/gjc-runtime/team-launch.ts +319 -0
- package/src/gjc-runtime/team-notify.ts +326 -0
- package/src/gjc-runtime/team-runtime.ts +1099 -1482
- package/src/gjc-runtime/team-store.ts +828 -0
- package/src/gjc-runtime/team-workers.ts +962 -0
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
- package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
- package/src/gjc-runtime/tmux-sessions.ts +4 -21
- package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
- package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
- package/src/gjc-runtime/ultragoal-guard.ts +29 -5
- package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
- package/src/gjc-runtime/workflow-cli-common.ts +30 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +18 -0
- package/src/goals/continuation-timeout-guard.ts +79 -0
- package/src/goals/runtime.ts +7 -5
- package/src/goals/state.ts +18 -0
- package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
- package/src/harness-control-plane/phase-rollup.ts +4 -7
- package/src/harness-control-plane/receipt-ingest.ts +5 -0
- package/src/harness-control-plane/receipt-spool.ts +51 -7
- package/src/harness-control-plane/session-lease.ts +12 -130
- package/src/hindsight/backend.ts +26 -27
- package/src/hindsight/state.ts +101 -32
- package/src/hooks/skill-state.ts +17 -11
- package/src/internal-urls/docs-index.generated.ts +21 -18
- package/src/lsp/client.ts +1 -1
- package/src/lsp/config.ts +145 -26
- package/src/lsp/index.ts +1 -1
- package/src/lsp/lspmux.ts +39 -10
- package/src/lsp/path-trust.ts +81 -0
- package/src/main.ts +28 -8
- package/src/memories/index.ts +1 -0
- package/src/memories/storage.ts +6 -2
- package/src/modes/action-registry.ts +165 -0
- package/src/modes/components/chrome.ts +40 -0
- package/src/modes/components/command-palette.ts +168 -0
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/custom-provider-wizard.ts +51 -14
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/pet-selector.ts +11 -17
- package/src/modes/components/plan-preview-overlay.ts +295 -0
- package/src/modes/components/queue-mode-selector.ts +10 -27
- package/src/modes/components/queue-pane.ts +79 -0
- package/src/modes/components/read-tool-group.ts +13 -0
- package/src/modes/components/session-observer-overlay.ts +207 -772
- package/src/modes/components/sessions-dashboard.ts +181 -0
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/show-images-selector.ts +10 -24
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +1 -969
- package/src/modes/components/tasks-pane.ts +73 -0
- package/src/modes/components/theme-selector.ts +11 -31
- package/src/modes/components/tool-execution.ts +22 -0
- package/src/modes/components/tool-status-header.ts +1056 -0
- package/src/modes/components/transcript-viewer-overlay.ts +384 -0
- package/src/modes/controllers/btw-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +24 -1
- package/src/modes/controllers/goal-mode-controller.ts +474 -0
- package/src/modes/controllers/input-controller.ts +764 -229
- package/src/modes/controllers/mode-gate.ts +24 -0
- package/src/modes/controllers/plan-mode-controller.ts +517 -0
- package/src/modes/controllers/selector-controller.ts +225 -10
- package/src/modes/controllers/stt-controller.ts +125 -0
- package/src/modes/interactive-mode.ts +289 -1150
- package/src/modes/jobs-observer.ts +3 -0
- package/src/modes/runtime-init.ts +8 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
- package/src/modes/tasks-aggregator.ts +199 -0
- package/src/modes/transcript-item-registry.ts +229 -0
- package/src/modes/types.ts +45 -18
- package/src/modes/utils/ui-helpers.ts +17 -9
- package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -0
- package/src/prompts/agents/architect.md +2 -8
- package/src/prompts/agents/critic.md +2 -8
- package/src/prompts/agents/executor.md +3 -0
- package/src/prompts/agents/planner.md +4 -12
- package/src/prompts/system/custom-system-prompt.md +1 -23
- package/src/prompts/system/plan-mode-approved.md +8 -0
- package/src/prompts/system/system-prompt.md +17 -96
- package/src/prompts/tools/bash.md +4 -4
- package/src/prompts/tools/browser.md +15 -52
- package/src/prompts/tools/find.md +2 -2
- package/src/prompts/tools/hashline.md +13 -55
- package/src/prompts/tools/read.md +3 -6
- package/src/prompts/tools/search-tool-bm25.md +0 -2
- package/src/prompts/tools/search.md +2 -4
- package/src/prompts/tools/skill-discovery.md +1 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-mcp/manager.ts +1 -1
- package/src/sdk/broker/broker.ts +206 -78
- package/src/sdk/broker/discovery.ts +1 -1
- package/src/sdk/broker/ensure.ts +3 -1
- package/src/sdk/broker/lifecycle-codec.ts +11 -0
- package/src/sdk/broker/lifecycle-ledger.ts +371 -74
- package/src/sdk/broker/lifecycle.ts +1399 -142
- package/src/sdk/broker/process-incarnation.ts +5 -13
- package/src/sdk/broker/session-index.ts +145 -56
- package/src/sdk/broker/state-version.ts +21 -0
- package/src/sdk/broker/transport.ts +1 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -0
- package/src/sdk/bus/chat-daemon-control.ts +8 -28
- package/src/sdk/bus/chat-effect-journal.ts +10 -2
- package/src/sdk/bus/config-commands.ts +5 -4
- package/src/sdk/bus/config.ts +98 -0
- package/src/sdk/bus/conversation-store.ts +5 -29
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/html-format.ts +20 -3
- package/src/sdk/bus/index.ts +778 -93
- package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
- package/src/sdk/bus/natives-augment.d.ts +26 -1
- package/src/sdk/bus/notification-service.ts +168 -20
- package/src/sdk/bus/rate-limit-pool.ts +58 -14
- package/src/sdk/bus/recent-activity.ts +188 -74
- package/src/sdk/bus/rich-document.ts +258 -0
- package/src/sdk/bus/rich-render.ts +62 -70
- package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
- package/src/sdk/bus/telegram-daemon.ts +1389 -237
- package/src/sdk/bus/threaded-inbound.ts +8 -3
- package/src/sdk/bus/threaded-render.ts +57 -1
- package/src/sdk/bus/topic-registry.ts +120 -13
- package/src/sdk/host/control/dispatch.ts +14 -2
- package/src/sdk/host/control/operations.ts +6 -1
- package/src/sdk/host/host.ts +31 -1
- package/src/sdk/host/query/handlers.ts +20 -22
- package/src/sdk/host/query/revision-store.ts +80 -57
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
- package/src/sdk/session-directory.ts +141 -0
- package/src/sdk/session.ts +458 -479
- package/src/session/agent-session.ts +1700 -583
- package/src/session/agent-storage.ts +18 -0
- package/src/session/cache-economics.ts +25 -8
- package/src/session/fallback-chain-controller.ts +53 -3
- package/src/session/internal/managed-session-scope.ts +2226 -0
- package/src/session/internal/managed-session-storage.ts +453 -0
- package/src/session/messages.ts +91 -2
- package/src/session/session-manager-internal.ts +33 -0
- package/src/session/session-manager.ts +1112 -333
- package/src/session/session-storage.ts +444 -39
- package/src/session/volatile-context-pruning.ts +78 -0
- package/src/setup/credential-auto-import.ts +2 -1
- package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
- package/src/setup/provider-onboarding.ts +36 -61
- package/src/setup/provider-presets.json +5 -0
- package/src/skill-state/active-state.ts +3 -13
- package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
- package/src/skill-state/workflow-state-contract.ts +6 -41
- package/src/slash-commands/builtin-registry.ts +46 -11
- package/src/system-prompt.ts +37 -44
- package/src/task/agents.ts +19 -1
- package/src/task/executor.ts +90 -6
- package/src/task/index.ts +36 -21
- package/src/thinking.ts +21 -6
- package/src/tool-discovery/tool-index.ts +2 -134
- package/src/tools/ask.ts +157 -21
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/bash.ts +38 -0
- package/src/tools/browser/dead-tab-recovery.ts +78 -0
- package/src/tools/browser/tab-supervisor.ts +282 -36
- package/src/tools/computer.ts +10 -3
- package/src/tools/cron.ts +30 -16
- package/src/tools/eval.ts +1 -2
- package/src/tools/fetch.ts +11 -4
- package/src/tools/index.ts +76 -24
- package/src/tools/output-meta.ts +13 -5
- package/src/tools/read.ts +37 -1
- package/src/tools/resource-gc.ts +2 -2
- package/src/tools/search-tool-bm25.ts +11 -74
- package/src/utils/event-bus.ts +9 -4
- package/src/web/search/index.ts +3 -3
- package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
- package/src/discovery/plugin-dir-roots.ts +0 -28
- package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
|
@@ -1,838 +1,273 @@
|
|
|
1
|
-
|
|
2
|
-
* Session observer overlay component.
|
|
3
|
-
*
|
|
4
|
-
* Picker mode: lists main + active subagent sessions with live status.
|
|
5
|
-
* Viewer mode: renders a scrollable, interactive transcript of the selected subagent's session
|
|
6
|
-
* by reading its JSONL session file — shows thinking, text, tool calls, results
|
|
7
|
-
* with expand/collapse per entry and breadcrumb navigation for nested sub-agents.
|
|
8
|
-
*
|
|
9
|
-
* Lifecycle:
|
|
10
|
-
* - shortcut opens picker
|
|
11
|
-
* - Enter on a subagent -> viewer
|
|
12
|
-
* - shortcut while in viewer -> back to picker (or pop breadcrumb)
|
|
13
|
-
* - Esc from viewer -> back to picker (or pop breadcrumb)
|
|
14
|
-
* - Esc from picker -> close overlay
|
|
15
|
-
* - Enter on main session -> close overlay (jump back)
|
|
16
|
-
*/
|
|
1
|
+
import * as fs from "node:fs";
|
|
17
2
|
import type { ToolResultMessage } from "@gajae-code/ai";
|
|
18
|
-
import {
|
|
19
|
-
import { formatDuration, formatNumber
|
|
3
|
+
import { matchesKey } from "@gajae-code/tui";
|
|
4
|
+
import { formatDuration, formatNumber } from "@gajae-code/utils";
|
|
20
5
|
import type { KeyId } from "../../config/keybindings";
|
|
21
6
|
import { isSilentAbort } from "../../session/messages";
|
|
22
7
|
import type { SessionMessageEntry } from "../../session/session-manager";
|
|
23
8
|
import { parseSessionEntries } from "../../session/session-manager";
|
|
24
|
-
import { PREVIEW_LIMITS, replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
|
|
25
9
|
import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
10
|
+
import { theme } from "../theme/theme";
|
|
11
|
+
import { type TranscriptViewerEntry, TranscriptViewerOverlay } from "./transcript-viewer-overlay";
|
|
28
12
|
|
|
29
|
-
/** Max thinking characters in collapsed state */
|
|
30
|
-
const MAX_THINKING_CHARS_COLLAPSED = 200;
|
|
31
|
-
/** Max thinking characters in expanded state */
|
|
32
|
-
const MAX_THINKING_CHARS_EXPANDED = 4000;
|
|
33
|
-
/** Max tool args characters to display */
|
|
34
13
|
const MAX_TOOL_ARGS_CHARS = 500;
|
|
35
|
-
/** Lines per page for PageUp/PageDown */
|
|
36
|
-
const PAGE_SIZE = 15;
|
|
37
|
-
/** Left indent for content under entry headers */
|
|
38
|
-
const INDENT = " ";
|
|
39
|
-
|
|
40
|
-
/** Compute the max content width for the current terminal, accounting for indent and chrome. */
|
|
41
|
-
function contentWidth(indent = INDENT): number {
|
|
42
|
-
return Math.max(TRUNCATE_LENGTHS.SHORT, resolveTerminalColumns() - indent.length - 2);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Sanitize a line for TUI display: replace tabs, then truncate to viewport width. */
|
|
46
|
-
function sanitizeLine(text: string, maxWidth?: number): string {
|
|
47
|
-
return truncateToWidth(replaceTabs(text), maxWidth ?? contentWidth());
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Represents a rendered entry in the viewer for selection/expand tracking */
|
|
51
|
-
interface ViewerEntry {
|
|
52
|
-
lineStart: number;
|
|
53
|
-
lineCount: number;
|
|
54
|
-
kind: "thinking" | "text" | "toolCall" | "user";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Breadcrumb item for nested session navigation */
|
|
58
|
-
interface BreadcrumbItem {
|
|
59
|
-
sessionId: string;
|
|
60
|
-
label: string;
|
|
61
|
-
sessionFile: string;
|
|
62
|
-
}
|
|
63
14
|
|
|
64
|
-
|
|
15
|
+
/** Session-observer adapter. The shared viewer owns navigation and fold state. */
|
|
16
|
+
export class SessionObserverOverlayComponent extends TranscriptViewerOverlay {
|
|
65
17
|
#registry: SessionObserverRegistry;
|
|
66
18
|
#onDone: () => void;
|
|
19
|
+
#observeKeys: readonly KeyId[];
|
|
67
20
|
#selectedSessionId?: string;
|
|
68
|
-
#
|
|
69
|
-
#transcriptCache?: { path: string; bytesRead: number; entries: SessionMessageEntry[]; model?: string };
|
|
70
|
-
|
|
71
|
-
// Scroll state
|
|
72
|
-
#scrollOffset = 0;
|
|
73
|
-
#renderedLines: string[] = [];
|
|
74
|
-
#viewportHeight = 20;
|
|
75
|
-
#wasAtBottom = true;
|
|
76
|
-
|
|
77
|
-
// Entry selection & expand/collapse
|
|
78
|
-
#viewerEntries: ViewerEntry[] = [];
|
|
79
|
-
#selectedEntryIndex = 0;
|
|
80
|
-
#expandedEntries = new Set<number>();
|
|
81
|
-
|
|
82
|
-
// Breadcrumb navigation
|
|
83
|
-
#navigationStack: BreadcrumbItem[] = [];
|
|
84
|
-
|
|
85
|
-
// Cached header/footer for viewer (rebuilt on refresh)
|
|
86
|
-
#viewerHeaderLines: string[] = [];
|
|
87
|
-
#viewerFooterLines: string[] = [];
|
|
88
|
-
// Markdown rendering
|
|
89
|
-
#mdTheme: MarkdownTheme = getMarkdownTheme();
|
|
21
|
+
#cache?: { path: string; bytesRead: number; entries: SessionMessageEntry[]; model?: string };
|
|
90
22
|
|
|
91
23
|
constructor(registry: SessionObserverRegistry, onDone: () => void, observeKeys: KeyId[]) {
|
|
92
|
-
|
|
24
|
+
// The option closures run during the base constructor's initial refresh,
|
|
25
|
+
// where `this` is still in its temporal dead zone; route through a box
|
|
26
|
+
// that is populated immediately after super() returns.
|
|
27
|
+
const box: { component?: SessionObserverOverlayComponent } = {};
|
|
28
|
+
super({
|
|
29
|
+
title: "Session Observer",
|
|
30
|
+
getEntries: () => (box.component ? box.component.#entries() : []),
|
|
31
|
+
onClose: onDone,
|
|
32
|
+
requestRender: () => {},
|
|
33
|
+
enterExpands: true,
|
|
34
|
+
initialSelection: "latest",
|
|
35
|
+
followTail: true,
|
|
36
|
+
maxExpandedLines: 100,
|
|
37
|
+
getHeaderLines: () => (box.component ? box.component.#headerLines() : []),
|
|
38
|
+
getFooterLines: () => (box.component ? box.component.#footerLines() : []),
|
|
39
|
+
footerControls:
|
|
40
|
+
"j/k:select Enter:expand PgUp/PgDn:page [/]/←→:cycle agents Esc/Ctrl+S:close g/G:top/bottom",
|
|
41
|
+
});
|
|
42
|
+
box.component = this;
|
|
93
43
|
this.#registry = registry;
|
|
94
44
|
this.#onDone = onDone;
|
|
95
45
|
this.#observeKeys = observeKeys;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (mostRecent) {
|
|
100
|
-
this.#selectedSessionId = mostRecent.id;
|
|
101
|
-
this.#setupViewer();
|
|
102
|
-
} else {
|
|
103
|
-
// No sub-agents — close immediately
|
|
104
|
-
queueMicrotask(() => this.#onDone());
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Find the most recently updated sub-agent session (prefer active ones) */
|
|
109
|
-
#getMostRecentSubagent(): ObservableSession | undefined {
|
|
110
|
-
const sessions = this.#registry.getSessions().filter(s => s.kind === "subagent");
|
|
111
|
-
if (sessions.length === 0) return undefined;
|
|
112
|
-
// Prefer active sessions, then sort by lastUpdate descending
|
|
113
|
-
const active = sessions.filter(s => s.status === "active");
|
|
114
|
-
const pool = active.length > 0 ? active : sessions;
|
|
115
|
-
return pool.sort((a, b) => b.lastUpdate - a.lastUpdate)[0];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
override render(width: number): string[] {
|
|
119
|
-
return this.#renderViewer(width);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
#setupViewer(): void {
|
|
123
|
-
this.children = [];
|
|
124
|
-
this.#scrollOffset = 0;
|
|
125
|
-
this.#selectedEntryIndex = 0;
|
|
126
|
-
this.#expandedEntries.clear();
|
|
127
|
-
this.#wasAtBottom = true;
|
|
128
|
-
this.#rebuildViewerContent();
|
|
129
|
-
// Auto-scroll to bottom and select last entry on init
|
|
130
|
-
if (this.#viewerEntries.length > 0) {
|
|
131
|
-
this.#selectedEntryIndex = this.#viewerEntries.length - 1;
|
|
132
|
-
this.#wasAtBottom = true;
|
|
133
|
-
this.#rebuildViewerContent();
|
|
134
|
-
}
|
|
46
|
+
this.#selectedSessionId = this.#mostRecent()?.id;
|
|
47
|
+
if (!this.#selectedSessionId) queueMicrotask(onDone);
|
|
48
|
+
this.refresh();
|
|
135
49
|
}
|
|
136
50
|
|
|
137
|
-
/** Rebuild content from live registry data */
|
|
138
51
|
refreshFromRegistry(): void {
|
|
139
|
-
|
|
140
|
-
// Keep auto-scrolling to bottom unless the user navigated away from the last entry
|
|
141
|
-
this.#wasAtBottom = this.#selectedEntryIndex >= this.#viewerEntries.length - 1;
|
|
142
|
-
this.#rebuildViewerContent();
|
|
143
|
-
}
|
|
52
|
+
this.refresh();
|
|
144
53
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const session = sessions.find(s => s.id === this.#selectedSessionId);
|
|
150
|
-
|
|
151
|
-
// Load transcript first so model info is available for header
|
|
152
|
-
let messageEntries: SessionMessageEntry[] | null = null;
|
|
153
|
-
if (session?.sessionFile) {
|
|
154
|
-
messageEntries = this.#loadTranscript(session.sessionFile);
|
|
54
|
+
override handleInput(keyData: string): void {
|
|
55
|
+
if (this.#observeKeys.some(key => matchesKey(keyData, key))) {
|
|
56
|
+
this.#onDone();
|
|
57
|
+
return;
|
|
155
58
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const breadcrumb = this.#buildBreadcrumb(session);
|
|
160
|
-
this.#viewerHeaderLines.push(theme.fg("accent", breadcrumb));
|
|
161
|
-
if (session) {
|
|
162
|
-
const statusColor = session.status === "active" ? "success" : session.status === "failed" ? "error" : "dim";
|
|
163
|
-
const statusText = theme.fg(statusColor, `[${session.status}]`);
|
|
164
|
-
const agentTag = session.agent ? theme.fg("dim", ` ${session.agent}`) : "";
|
|
165
|
-
const subagentIds = this.#getSubagentSessionIds();
|
|
166
|
-
const posIdx = subagentIds.indexOf(this.#selectedSessionId ?? "");
|
|
167
|
-
const posLabel =
|
|
168
|
-
subagentIds.length > 1 && posIdx >= 0 ? theme.fg("dim", ` (${posIdx + 1}/${subagentIds.length})`) : "";
|
|
169
|
-
const modelName = this.#transcriptCache?.model;
|
|
170
|
-
const modelLabel = modelName ? theme.fg("muted", ` · ${modelName}`) : "";
|
|
171
|
-
this.#viewerHeaderLines.push(`${theme.bold(session.label)} ${statusText}${agentTag}${posLabel}${modelLabel}`);
|
|
59
|
+
if (keyData === "]" || matchesKey(keyData, "right") || matchesKey(keyData, "tab")) {
|
|
60
|
+
this.#cycle(1);
|
|
61
|
+
return;
|
|
172
62
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
this.#viewerEntries = [];
|
|
177
|
-
|
|
178
|
-
if (!session) {
|
|
179
|
-
contentLines.push(theme.fg("dim", "Session no longer available."));
|
|
180
|
-
} else if (!session.sessionFile) {
|
|
181
|
-
contentLines.push(theme.fg("dim", "No session file available yet."));
|
|
182
|
-
} else if (!messageEntries) {
|
|
183
|
-
contentLines.push(theme.fg("dim", "Unable to read session file."));
|
|
184
|
-
} else if (messageEntries.length === 0) {
|
|
185
|
-
contentLines.push(theme.fg("dim", "No messages yet."));
|
|
186
|
-
} else {
|
|
187
|
-
this.#buildTranscriptLines(messageEntries, contentLines);
|
|
63
|
+
if (keyData === "[" || matchesKey(keyData, "left") || matchesKey(keyData, "shift+tab")) {
|
|
64
|
+
this.#cycle(-1);
|
|
65
|
+
return;
|
|
188
66
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
this.#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
theme.fg("dim", "j/k:scroll Enter:expand [/]/←→:cycle agents Esc/Ctrl+S:close g/G:top/bottom"),
|
|
67
|
+
super.handleInput(keyData);
|
|
68
|
+
}
|
|
69
|
+
#mostRecent(): ObservableSession | undefined {
|
|
70
|
+
const all = this.#registry.getSessions().filter(session => session.kind === "subagent");
|
|
71
|
+
return (
|
|
72
|
+
all.filter(session => session.status === "active").sort((a, b) => b.lastUpdate - a.lastUpdate)[0] ??
|
|
73
|
+
all.sort((a, b) => b.lastUpdate - a.lastUpdate)[0]
|
|
197
74
|
);
|
|
198
|
-
|
|
199
|
-
// Auto-scroll to bottom if we were at bottom
|
|
200
|
-
if (this.#wasAtBottom) {
|
|
201
|
-
this.#scrollOffset = Math.max(0, contentLines.length - this.#viewportHeight);
|
|
202
|
-
}
|
|
203
75
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
this
|
|
215
|
-
|
|
216
|
-
// Clamp scroll offset
|
|
217
|
-
const maxScroll = Math.max(0, this.#renderedLines.length - this.#viewportHeight);
|
|
218
|
-
this.#scrollOffset = Math.max(0, Math.min(this.#scrollOffset, maxScroll));
|
|
219
|
-
|
|
220
|
-
const lines: string[] = [];
|
|
221
|
-
|
|
222
|
-
// --- Header ---
|
|
223
|
-
lines.push(...new DynamicBorder().render(width));
|
|
224
|
-
for (const hl of this.#viewerHeaderLines) {
|
|
225
|
-
lines.push(` ${hl}`);
|
|
226
|
-
}
|
|
227
|
-
lines.push(...new DynamicBorder().render(width));
|
|
228
|
-
|
|
229
|
-
// --- Scrolled content viewport ---
|
|
230
|
-
const visibleLines = this.#renderedLines.slice(this.#scrollOffset, this.#scrollOffset + this.#viewportHeight);
|
|
231
|
-
for (const vl of visibleLines) {
|
|
232
|
-
lines.push(` ${vl}`);
|
|
233
|
-
}
|
|
234
|
-
// Pad to fill viewport if content is shorter
|
|
235
|
-
const pad = this.#viewportHeight - visibleLines.length;
|
|
236
|
-
for (let i = 0; i < pad; i++) {
|
|
237
|
-
lines.push("");
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// --- Footer ---
|
|
241
|
-
const scrollInfo =
|
|
242
|
-
this.#renderedLines.length > this.#viewportHeight
|
|
243
|
-
? ` ${theme.fg("dim", `[${this.#scrollOffset + 1}-${Math.min(this.#scrollOffset + this.#viewportHeight, this.#renderedLines.length)}/${this.#renderedLines.length}]`)}`
|
|
244
|
-
: "";
|
|
245
|
-
lines.push("");
|
|
246
|
-
lines.push(` ${this.#viewerFooterLines[0] ?? ""}${scrollInfo}`);
|
|
247
|
-
for (let i = 1; i < this.#viewerFooterLines.length; i++) {
|
|
248
|
-
lines.push(` ${this.#viewerFooterLines[i]}`);
|
|
249
|
-
}
|
|
250
|
-
lines.push(...new DynamicBorder().render(width));
|
|
251
|
-
|
|
252
|
-
return lines;
|
|
76
|
+
#cycle(direction: 1 | -1): void {
|
|
77
|
+
const ids = this.#registry
|
|
78
|
+
.getSessions()
|
|
79
|
+
.filter(session => session.kind === "subagent")
|
|
80
|
+
.map(session => session.id);
|
|
81
|
+
if (ids.length < 2) return;
|
|
82
|
+
const current = ids.indexOf(this.#selectedSessionId ?? "");
|
|
83
|
+
this.#selectedSessionId = ids[(current + direction + ids.length) % ids.length];
|
|
84
|
+
this.#cache = undefined;
|
|
85
|
+
this.resetSourceState();
|
|
86
|
+
this.refresh();
|
|
253
87
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
parts.push(item.label);
|
|
259
|
-
}
|
|
260
|
-
if (session) parts.push(session.label);
|
|
261
|
-
return parts.join(" > ");
|
|
88
|
+
#entries(): readonly TranscriptViewerEntry[] {
|
|
89
|
+
const session = this.#registry.getSessions().find(candidate => candidate.id === this.#selectedSessionId);
|
|
90
|
+
if (!session?.sessionFile) return [];
|
|
91
|
+
return entriesFromMessages(this.#load(session.sessionFile));
|
|
262
92
|
}
|
|
263
|
-
|
|
264
|
-
|
|
93
|
+
#headerLines(): string[] {
|
|
94
|
+
const session = this.#registry.getSessions().find(candidate => candidate.id === this.#selectedSessionId);
|
|
95
|
+
if (!session) return [theme.fg("dim", "Session no longer available.")];
|
|
96
|
+
const ids = this.#registry
|
|
97
|
+
.getSessions()
|
|
98
|
+
.filter(candidate => candidate.kind === "subagent")
|
|
99
|
+
.map(candidate => candidate.id);
|
|
100
|
+
const position = ids.length > 1 ? theme.fg("dim", ` (${ids.indexOf(session.id) + 1}/${ids.length})`) : "";
|
|
101
|
+
const color = session.status === "active" ? "success" : session.status === "failed" ? "error" : "dim";
|
|
102
|
+
const model = this.#cache?.model ? theme.fg("muted", ` · ${this.#cache.model}`) : "";
|
|
103
|
+
return [
|
|
104
|
+
`${theme.bold(session.label)} ${theme.fg(color, `[${session.status}]`)}${session.agent ? theme.fg("dim", ` ${session.agent}`) : ""}${position}${model}`,
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
#footerLines(): string[] {
|
|
108
|
+
const session = this.#registry.getSessions().find(candidate => candidate.id === this.#selectedSessionId);
|
|
265
109
|
const progress = session?.progress;
|
|
266
|
-
if (!progress) return
|
|
110
|
+
if (!progress) return [];
|
|
267
111
|
const stats: string[] = [];
|
|
268
112
|
if (progress.toolCount > 0) stats.push(`${formatNumber(progress.toolCount)} tools`);
|
|
269
|
-
// Current per-turn context — what the user reads as "how full is the context".
|
|
270
|
-
// Falls back to cumulative billing volume (Σ-prefixed) when context size is unknown.
|
|
271
113
|
if (progress.contextTokens && progress.contextTokens > 0) {
|
|
272
|
-
|
|
114
|
+
stats.push(
|
|
273
115
|
progress.contextWindow && progress.contextWindow > 0
|
|
274
116
|
? `${formatNumber(progress.contextTokens)}/${formatNumber(progress.contextWindow)} ctx`
|
|
275
|
-
: `${formatNumber(progress.contextTokens)} ctx
|
|
276
|
-
|
|
117
|
+
: `${formatNumber(progress.contextTokens)} ctx`,
|
|
118
|
+
);
|
|
277
119
|
if (progress.tokens > 0) stats.push(`Σ${formatNumber(progress.tokens)}`);
|
|
278
|
-
} else if (progress.tokens > 0) {
|
|
279
|
-
stats.push(`Σ${formatNumber(progress.tokens)}`);
|
|
280
|
-
}
|
|
120
|
+
} else if (progress.tokens > 0) stats.push(`Σ${formatNumber(progress.tokens)}`);
|
|
281
121
|
if (progress.durationMs > 0) stats.push(formatDuration(progress.durationMs));
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (progress.cost > 0) parts.push(theme.fg("statusLineCost", `$${progress.cost.toFixed(2)}`));
|
|
285
|
-
return parts.join(theme.sep.dot);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
#buildTranscriptLines(messageEntries: SessionMessageEntry[], lines: string[]): void {
|
|
289
|
-
// Build a tool call ID -> tool result map
|
|
290
|
-
const toolResults = new Map<string, ToolResultMessage>();
|
|
291
|
-
for (const entry of messageEntries) {
|
|
292
|
-
if (entry.message.role === "toolResult") {
|
|
293
|
-
toolResults.set(entry.message.toolCallId, entry.message);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
let entryIndex = 0;
|
|
298
|
-
for (const entry of messageEntries) {
|
|
299
|
-
const msg = entry.message;
|
|
300
|
-
|
|
301
|
-
if (msg.role === "assistant") {
|
|
302
|
-
// Handle error messages with empty content
|
|
303
|
-
if (msg.content.length === 0 && msg.errorMessage && !isSilentAbort(msg.errorMessage)) {
|
|
304
|
-
const startLine = lines.length;
|
|
305
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
306
|
-
const cursor = isSelected ? theme.fg("accent", "▶") : " ";
|
|
307
|
-
lines.push("");
|
|
308
|
-
const errorLines = msg.errorMessage.split("\n");
|
|
309
|
-
const maxWidth = contentWidth();
|
|
310
|
-
lines.push(`${cursor} ${theme.fg("error", `✗ Error: ${sanitizeLine(errorLines[0], maxWidth)}`)}`);
|
|
311
|
-
for (let i = 1; i < errorLines.length; i++) {
|
|
312
|
-
lines.push(`${INDENT}${theme.fg("error", sanitizeLine(errorLines[i], maxWidth))}`);
|
|
313
|
-
}
|
|
314
|
-
this.#viewerEntries.push({ lineStart: startLine, lineCount: lines.length - startLine, kind: "text" });
|
|
315
|
-
entryIndex++;
|
|
316
|
-
} else {
|
|
317
|
-
for (const content of msg.content) {
|
|
318
|
-
if (content.type === "thinking" && content.thinking.trim()) {
|
|
319
|
-
const startLine = lines.length;
|
|
320
|
-
const isExpanded = this.#expandedEntries.has(entryIndex);
|
|
321
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
322
|
-
this.#renderThinkingLines(lines, content.thinking.trim(), isExpanded, isSelected);
|
|
323
|
-
this.#viewerEntries.push({
|
|
324
|
-
lineStart: startLine,
|
|
325
|
-
lineCount: lines.length - startLine,
|
|
326
|
-
kind: "thinking",
|
|
327
|
-
});
|
|
328
|
-
entryIndex++;
|
|
329
|
-
} else if (content.type === "text" && content.text.trim()) {
|
|
330
|
-
const startLine = lines.length;
|
|
331
|
-
const isExpanded = this.#expandedEntries.has(entryIndex);
|
|
332
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
333
|
-
this.#renderTextLines(lines, content.text.trim(), isExpanded, isSelected);
|
|
334
|
-
this.#viewerEntries.push({
|
|
335
|
-
lineStart: startLine,
|
|
336
|
-
lineCount: lines.length - startLine,
|
|
337
|
-
kind: "text",
|
|
338
|
-
});
|
|
339
|
-
entryIndex++;
|
|
340
|
-
} else if (content.type === "toolCall") {
|
|
341
|
-
const startLine = lines.length;
|
|
342
|
-
const isExpanded = this.#expandedEntries.has(entryIndex);
|
|
343
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
344
|
-
const result = toolResults.get(content.id);
|
|
345
|
-
this.#renderToolCallLines(lines, content, result, isExpanded, isSelected);
|
|
346
|
-
this.#viewerEntries.push({
|
|
347
|
-
lineStart: startLine,
|
|
348
|
-
lineCount: lines.length - startLine,
|
|
349
|
-
kind: "toolCall",
|
|
350
|
-
});
|
|
351
|
-
entryIndex++;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
} else if (msg.role === "user" || msg.role === "developer") {
|
|
356
|
-
const text =
|
|
357
|
-
typeof msg.content === "string"
|
|
358
|
-
? msg.content
|
|
359
|
-
: msg.content
|
|
360
|
-
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
361
|
-
.map(b => b.text)
|
|
362
|
-
.join("\n");
|
|
363
|
-
if (text.trim()) {
|
|
364
|
-
const startLine = lines.length;
|
|
365
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
366
|
-
const isExpanded = this.#expandedEntries.has(entryIndex);
|
|
367
|
-
const label = msg.role === "developer" ? "System" : "User";
|
|
368
|
-
const cursor = isSelected ? theme.fg("accent", "▶") : " ";
|
|
369
|
-
lines.push("");
|
|
370
|
-
if (isExpanded) {
|
|
371
|
-
lines.push(`${cursor} ${theme.fg("dim", `[${label}]`)}`);
|
|
372
|
-
const mdLines = this.#renderMarkdownToLines(text.trim());
|
|
373
|
-
for (const ml of mdLines) {
|
|
374
|
-
lines.push(ml);
|
|
375
|
-
}
|
|
376
|
-
} else {
|
|
377
|
-
const firstLine = text.trim().split("\n")[0];
|
|
378
|
-
const totalLines = text.trim().split("\n").length;
|
|
379
|
-
const hint = totalLines > 1 ? theme.fg("dim", ` (${totalLines} lines)`) : "";
|
|
380
|
-
lines.push(
|
|
381
|
-
`${cursor} ${theme.fg("dim", `[${label}]`)} ${theme.fg("muted", sanitizeLine(firstLine, TRUNCATE_LENGTHS.TITLE))}${hint}`,
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
this.#viewerEntries.push({ lineStart: startLine, lineCount: lines.length - startLine, kind: "user" });
|
|
385
|
-
entryIndex++;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/** Render markdown text into indented lines using the theme's markdown renderer */
|
|
392
|
-
#renderMarkdownToLines(text: string, indent: string = INDENT): string[] {
|
|
393
|
-
const width = Math.max(40, resolveTerminalColumns() - indent.length - 4);
|
|
394
|
-
const md = new Markdown(text, 0, 0, this.#mdTheme);
|
|
395
|
-
const rendered = md.render(width);
|
|
396
|
-
return rendered.map(line => `${indent}${line.trimEnd()}`);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
#renderThinkingLines(lines: string[], thinking: string, expanded: boolean, selected: boolean): void {
|
|
400
|
-
const cursor = selected ? theme.fg("accent", "▶") : " ";
|
|
401
|
-
const maxChars = expanded ? MAX_THINKING_CHARS_EXPANDED : MAX_THINKING_CHARS_COLLAPSED;
|
|
402
|
-
const truncated = thinking.length > maxChars;
|
|
403
|
-
const expandLabel = !expanded && truncated ? theme.fg("dim", " ↵") : "";
|
|
404
|
-
|
|
405
|
-
lines.push("");
|
|
406
|
-
lines.push(`${cursor} ${theme.fg("dim", "💭 Thinking")}${expandLabel}`);
|
|
407
|
-
|
|
408
|
-
const displayText = truncated ? `${thinking.slice(0, maxChars)}...` : thinking;
|
|
409
|
-
if (expanded) {
|
|
410
|
-
// Expanded thinking: render as markdown for readable formatting
|
|
411
|
-
const mdLines = this.#renderMarkdownToLines(displayText);
|
|
412
|
-
const maxLines = 100;
|
|
413
|
-
for (let i = 0; i < Math.min(mdLines.length, maxLines); i++) {
|
|
414
|
-
lines.push(mdLines[i]);
|
|
415
|
-
}
|
|
416
|
-
if (mdLines.length > maxLines) {
|
|
417
|
-
lines.push(`${INDENT}${theme.fg("dim", `... ${mdLines.length - maxLines} more lines`)}`);
|
|
418
|
-
}
|
|
419
|
-
} else {
|
|
420
|
-
// Collapsed thinking: brief italic preview
|
|
421
|
-
const thinkingLines = displayText.split("\n");
|
|
422
|
-
const maxLines = PREVIEW_LIMITS.COLLAPSED_LINES;
|
|
423
|
-
for (let i = 0; i < Math.min(thinkingLines.length, maxLines); i++) {
|
|
424
|
-
lines.push(`${INDENT}${theme.fg("thinkingText", sanitizeLine(thinkingLines[i]))}`);
|
|
425
|
-
}
|
|
426
|
-
if (thinkingLines.length > maxLines) {
|
|
427
|
-
lines.push(`${INDENT}${theme.fg("dim", `... ${thinkingLines.length - maxLines} more lines`)}`);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
#renderTextLines(lines: string[], text: string, expanded: boolean, selected: boolean): void {
|
|
433
|
-
const cursor = selected ? theme.fg("accent", "▶") : " ";
|
|
434
|
-
|
|
435
|
-
lines.push("");
|
|
436
|
-
lines.push(`${cursor} ${theme.fg("muted", "Response")}`);
|
|
437
|
-
|
|
438
|
-
if (expanded) {
|
|
439
|
-
// Expanded: full markdown rendering
|
|
440
|
-
const mdLines = this.#renderMarkdownToLines(text);
|
|
441
|
-
for (const ml of mdLines) {
|
|
442
|
-
lines.push(ml);
|
|
443
|
-
}
|
|
444
|
-
} else {
|
|
445
|
-
// Collapsed: first few lines as plain text
|
|
446
|
-
const textLines = text.split("\n");
|
|
447
|
-
const maxLines = PREVIEW_LIMITS.COLLAPSED_LINES;
|
|
448
|
-
const maxWidth = contentWidth();
|
|
449
|
-
for (let i = 0; i < Math.min(textLines.length, maxLines); i++) {
|
|
450
|
-
lines.push(`${INDENT}${sanitizeLine(textLines[i], maxWidth)}`);
|
|
451
|
-
}
|
|
452
|
-
if (textLines.length > maxLines) {
|
|
453
|
-
lines.push(`${INDENT}${theme.fg("dim", `... ${textLines.length - maxLines} more lines`)}`);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
#renderToolCallLines(
|
|
459
|
-
lines: string[],
|
|
460
|
-
call: { id: string; name: string; arguments: Record<string, unknown>; intent?: string },
|
|
461
|
-
result: ToolResultMessage | undefined,
|
|
462
|
-
expanded: boolean,
|
|
463
|
-
selected: boolean,
|
|
464
|
-
): void {
|
|
465
|
-
const cursor = selected ? theme.fg("accent", "▶") : " ";
|
|
466
|
-
lines.push("");
|
|
467
|
-
|
|
468
|
-
// Tool call header
|
|
469
|
-
const intentStr = call.intent ? theme.fg("dim", ` ${sanitizeLine(call.intent, TRUNCATE_LENGTHS.SHORT)}`) : "";
|
|
470
|
-
lines.push(`${cursor} ${theme.fg("accent", "▸")} ${theme.bold(theme.fg("muted", call.name))}${intentStr}`);
|
|
471
|
-
|
|
472
|
-
// Key arguments
|
|
473
|
-
const argSummary = this.#formatToolArgs(call.name, call.arguments);
|
|
474
|
-
if (argSummary) {
|
|
475
|
-
lines.push(`${INDENT}${theme.fg("dim", sanitizeLine(argSummary, contentWidth()))}`);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Tool result
|
|
479
|
-
if (result) {
|
|
480
|
-
this.#renderToolResultLines(lines, result, expanded);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
#renderToolResultLines(lines: string[], result: ToolResultMessage, expanded: boolean): void {
|
|
485
|
-
const textParts = result.content
|
|
486
|
-
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
487
|
-
.map(p => p.text);
|
|
488
|
-
const text = textParts.join("\n").trim();
|
|
489
|
-
|
|
490
|
-
if (result.isError) {
|
|
491
|
-
const errorLines = text.split("\n");
|
|
492
|
-
const maxErrorLines = expanded ? PREVIEW_LIMITS.EXPANDED_LINES : PREVIEW_LIMITS.COLLAPSED_LINES;
|
|
493
|
-
const maxWidth = contentWidth();
|
|
494
|
-
lines.push(`${INDENT}${theme.fg("error", `✗ ${sanitizeLine(errorLines[0] || "Error", maxWidth)}`)}`);
|
|
495
|
-
for (let i = 1; i < Math.min(errorLines.length, maxErrorLines); i++) {
|
|
496
|
-
lines.push(`${INDENT} ${theme.fg("error", sanitizeLine(errorLines[i], maxWidth))}`);
|
|
497
|
-
}
|
|
498
|
-
if (errorLines.length > maxErrorLines) {
|
|
499
|
-
lines.push(`${INDENT} ${theme.fg("dim", `... ${errorLines.length - maxErrorLines} more lines`)}`);
|
|
500
|
-
}
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (!text) {
|
|
505
|
-
lines.push(`${INDENT}${theme.fg("dim", "✓ done")}`);
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const resultLines = text.split("\n");
|
|
510
|
-
const maxLines = expanded ? PREVIEW_LIMITS.EXPANDED_LINES : PREVIEW_LIMITS.OUTPUT_COLLAPSED;
|
|
511
|
-
|
|
512
|
-
// Status line
|
|
513
|
-
const statusPrefix = `${INDENT}${theme.fg("success", "✓")}`;
|
|
514
|
-
|
|
515
|
-
if (resultLines.length === 1 && text.length < TRUNCATE_LENGTHS.LONG) {
|
|
516
|
-
lines.push(`${statusPrefix} ${theme.fg("dim", sanitizeLine(text))}`);
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
lines.push(`${statusPrefix} ${theme.fg("dim", `${resultLines.length} lines`)}`);
|
|
521
|
-
const displayLines = resultLines.slice(0, maxLines);
|
|
522
|
-
for (const rl of displayLines) {
|
|
523
|
-
lines.push(`${INDENT} ${theme.fg("dim", sanitizeLine(rl))}`);
|
|
524
|
-
}
|
|
525
|
-
if (resultLines.length > maxLines) {
|
|
526
|
-
lines.push(`${INDENT} ${theme.fg("dim", `... ${resultLines.length - maxLines} more`)}`);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
#formatToolArgs(toolName: string, args: Record<string, unknown>): string {
|
|
531
|
-
switch (toolName) {
|
|
532
|
-
case "read":
|
|
533
|
-
case "write":
|
|
534
|
-
case "edit":
|
|
535
|
-
return args.path ? `path: ${args.path}` : "";
|
|
536
|
-
case "search":
|
|
537
|
-
return [
|
|
538
|
-
args.pattern ? `pattern: ${args.pattern}` : "",
|
|
539
|
-
Array.isArray(args.paths) ? `paths: ${args.paths.join(", ")}` : "",
|
|
540
|
-
]
|
|
541
|
-
.filter(Boolean)
|
|
542
|
-
.join(", ");
|
|
543
|
-
case "find":
|
|
544
|
-
return Array.isArray(args.paths) ? `paths: ${args.paths.join(", ")}` : "";
|
|
545
|
-
case "bash": {
|
|
546
|
-
const cmd = args.command;
|
|
547
|
-
return typeof cmd === "string" ? replaceTabs(cmd) : "";
|
|
548
|
-
}
|
|
549
|
-
case "lsp":
|
|
550
|
-
return [args.action, args.file, args.symbol].filter(Boolean).join(" ");
|
|
551
|
-
case "ast_grep":
|
|
552
|
-
case "ast_edit":
|
|
553
|
-
return args.path ? `path: ${args.path}` : "";
|
|
554
|
-
case "task": {
|
|
555
|
-
const tasks = args.tasks;
|
|
556
|
-
return Array.isArray(tasks) ? `${tasks.length} task(s)` : "";
|
|
557
|
-
}
|
|
558
|
-
default: {
|
|
559
|
-
const parts: string[] = [];
|
|
560
|
-
let total = 0;
|
|
561
|
-
for (const [key, value] of Object.entries(args)) {
|
|
562
|
-
if (key.startsWith("_")) continue;
|
|
563
|
-
const v = typeof value === "string" ? value : JSON.stringify(value);
|
|
564
|
-
const entry = `${key}: ${replaceTabs(v ?? "")}`;
|
|
565
|
-
if (total + entry.length > MAX_TOOL_ARGS_CHARS) break;
|
|
566
|
-
parts.push(entry);
|
|
567
|
-
total += entry.length;
|
|
568
|
-
}
|
|
569
|
-
return parts.join(", ");
|
|
570
|
-
}
|
|
571
|
-
}
|
|
122
|
+
if (progress.cost > 0) stats.push(`$${progress.cost.toFixed(2)}`);
|
|
123
|
+
return stats.length ? [theme.fg("dim", stats.join(theme.sep.dot))] : [];
|
|
572
124
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const fromByte = this.#transcriptCache?.bytesRead ?? 0;
|
|
580
|
-
const result = readFileIncremental(sessionFile, fromByte);
|
|
581
|
-
if (!result) {
|
|
582
|
-
logger.debug("Session observer: failed to read session file", { path: sessionFile });
|
|
583
|
-
return this.#transcriptCache?.entries ?? null;
|
|
584
|
-
}
|
|
585
|
-
|
|
125
|
+
#load(filePath: string): SessionMessageEntry[] {
|
|
126
|
+
if (this.#cache?.path !== filePath) this.#cache = undefined;
|
|
127
|
+
const fromByte = this.#cache?.bytesRead ?? 0;
|
|
128
|
+
const result = readFileIncremental(filePath, fromByte);
|
|
129
|
+
if (!result) return this.#cache?.entries ?? [];
|
|
586
130
|
if (result.newSize < fromByte) {
|
|
587
|
-
this.#
|
|
588
|
-
return this.#
|
|
131
|
+
this.#cache = undefined;
|
|
132
|
+
return this.#load(filePath);
|
|
589
133
|
}
|
|
590
|
-
|
|
591
|
-
if (
|
|
592
|
-
this.#transcriptCache = { path: sessionFile, bytesRead: 0, entries: [] };
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (result.text.length > 0) {
|
|
134
|
+
if (!this.#cache) this.#cache = { path: filePath, bytesRead: 0, entries: [] };
|
|
135
|
+
if (result.text) {
|
|
596
136
|
const lastNewline = result.text.lastIndexOf("\n");
|
|
597
137
|
if (lastNewline >= 0) {
|
|
598
|
-
const
|
|
599
|
-
const
|
|
600
|
-
for (const entry of newEntries) {
|
|
138
|
+
const complete = result.text.slice(0, lastNewline + 1);
|
|
139
|
+
for (const entry of parseSessionEntries(complete)) {
|
|
601
140
|
if (entry.type === "message") {
|
|
602
|
-
this.#
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if (!this.#transcriptCache.model && msg.role === "assistant") {
|
|
606
|
-
this.#transcriptCache.model = msg.model;
|
|
607
|
-
}
|
|
608
|
-
} else if (entry.type === "model_change") {
|
|
609
|
-
this.#transcriptCache.model = entry.model;
|
|
610
|
-
}
|
|
141
|
+
this.#cache.entries.push(entry);
|
|
142
|
+
if (!this.#cache.model && entry.message.role === "assistant") this.#cache.model = entry.message.model;
|
|
143
|
+
} else if (entry.type === "model_change") this.#cache.model = entry.model;
|
|
611
144
|
}
|
|
612
|
-
this.#
|
|
145
|
+
this.#cache.bytesRead = fromByte + Buffer.byteLength(complete, "utf-8");
|
|
613
146
|
}
|
|
614
147
|
}
|
|
615
|
-
return this.#
|
|
148
|
+
return this.#cache.entries;
|
|
616
149
|
}
|
|
150
|
+
}
|
|
617
151
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
this.#scrollOffset + PAGE_SIZE,
|
|
680
|
-
Math.max(0, this.#renderedLines.length - this.#viewportHeight),
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
} else {
|
|
684
|
-
this.#scrollOffset = Math.min(
|
|
685
|
-
this.#scrollOffset + PAGE_SIZE,
|
|
686
|
-
Math.max(0, this.#renderedLines.length - this.#viewportHeight),
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
this.#rebuildAndScroll();
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// Page Up
|
|
694
|
-
if (matchesKey(keyData, "pageUp")) {
|
|
695
|
-
if (entryCount > 0) {
|
|
696
|
-
const prevIndex = this.#selectedEntryIndex;
|
|
697
|
-
this.#selectedEntryIndex = Math.max(this.#selectedEntryIndex - 5, 0);
|
|
698
|
-
// If selection didn't move (top of list or single oversized entry), fall back to line scroll
|
|
699
|
-
if (this.#selectedEntryIndex === prevIndex) {
|
|
700
|
-
this.#scrollOffset = Math.max(this.#scrollOffset - PAGE_SIZE, 0);
|
|
701
|
-
}
|
|
702
|
-
} else {
|
|
703
|
-
this.#scrollOffset = Math.max(this.#scrollOffset - PAGE_SIZE, 0);
|
|
704
|
-
}
|
|
705
|
-
this.#rebuildAndScroll();
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Enter — toggle expand/collapse, or dive into nested session
|
|
710
|
-
if (matchesKey(keyData, "enter") || keyData === "\r" || keyData === "\n") {
|
|
711
|
-
if (entryCount > 0 && this.#selectedEntryIndex < entryCount) {
|
|
712
|
-
// Toggle expand/collapse
|
|
713
|
-
if (this.#expandedEntries.has(this.#selectedEntryIndex)) {
|
|
714
|
-
this.#expandedEntries.delete(this.#selectedEntryIndex);
|
|
715
|
-
} else {
|
|
716
|
-
this.#expandedEntries.add(this.#selectedEntryIndex);
|
|
152
|
+
function entriesFromMessages(entries: readonly SessionMessageEntry[]): TranscriptViewerEntry[] {
|
|
153
|
+
const results = new Map<string, ToolResultMessage>();
|
|
154
|
+
for (const entry of entries)
|
|
155
|
+
if (entry.message.role === "toolResult") results.set(entry.message.toolCallId, entry.message);
|
|
156
|
+
const output: TranscriptViewerEntry[] = [];
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
const message = entry.message;
|
|
159
|
+
if (message.role === "assistant") {
|
|
160
|
+
if (message.errorMessage && !isSilentAbort(message.errorMessage))
|
|
161
|
+
output.push({
|
|
162
|
+
id: `${entry.id}:error`,
|
|
163
|
+
kind: "text",
|
|
164
|
+
label: "✗ Error:",
|
|
165
|
+
payload: { text: message.errorMessage, metadata: {}, source: message },
|
|
166
|
+
foldable: true,
|
|
167
|
+
});
|
|
168
|
+
message.content.forEach((content, contentIndex) => {
|
|
169
|
+
if (content.type === "thinking" && content.thinking.trim())
|
|
170
|
+
output.push({
|
|
171
|
+
id: `${entry.id}:thinking:${contentIndex}`,
|
|
172
|
+
kind: "thinking",
|
|
173
|
+
label: "Thinking",
|
|
174
|
+
payload: { text: content.thinking, metadata: {}, source: content },
|
|
175
|
+
foldable: true,
|
|
176
|
+
getDisplayText: expanded => truncateThinking(content.thinking, expanded),
|
|
177
|
+
});
|
|
178
|
+
if (content.type === "text" && content.text.trim())
|
|
179
|
+
output.push({
|
|
180
|
+
id: `${entry.id}:text:${contentIndex}`,
|
|
181
|
+
kind: "text",
|
|
182
|
+
label: "Response",
|
|
183
|
+
payload: { text: content.text, metadata: {}, source: content },
|
|
184
|
+
foldable: true,
|
|
185
|
+
});
|
|
186
|
+
if (content.type === "toolCall") {
|
|
187
|
+
const result = results.get(content.id);
|
|
188
|
+
const resultText =
|
|
189
|
+
result?.content
|
|
190
|
+
.filter(part => part.type === "text")
|
|
191
|
+
.map(part => part.text)
|
|
192
|
+
.join("\n")
|
|
193
|
+
.trim() ?? "";
|
|
194
|
+
const summary = formatToolArgs(content.name, content.arguments);
|
|
195
|
+
const callText = [summary, content.intent].filter(Boolean).join("\n");
|
|
196
|
+
const resultDisplay = result?.isError ? `✗ ${resultText || "Error"}` : resultText || "✓ done";
|
|
197
|
+
output.push({
|
|
198
|
+
id: `tool:${content.id}`,
|
|
199
|
+
kind: "tool",
|
|
200
|
+
label: content.name,
|
|
201
|
+
payload: {
|
|
202
|
+
text: [callText, resultDisplay].filter(Boolean).join("\n"),
|
|
203
|
+
metadata: {
|
|
204
|
+
name: content.name,
|
|
205
|
+
arguments: content.arguments,
|
|
206
|
+
intent: content.intent,
|
|
207
|
+
isError: result?.isError ?? false,
|
|
208
|
+
},
|
|
209
|
+
source: { call: content, result },
|
|
210
|
+
},
|
|
211
|
+
foldable: true,
|
|
212
|
+
});
|
|
717
213
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// ] / → / Tab — next sub-agent session
|
|
740
|
-
if (keyData === "]" || matchesKey(keyData, "tab") || matchesKey(keyData, "right")) {
|
|
741
|
-
this.#cycleSession(1);
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// [ / ← / Shift+Tab — previous sub-agent session
|
|
746
|
-
if (keyData === "[" || matchesKey(keyData, "shift+tab") || matchesKey(keyData, "left")) {
|
|
747
|
-
this.#cycleSession(-1);
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/** Get the ordered list of sub-agent session IDs (excludes main) */
|
|
753
|
-
#getSubagentSessionIds(): string[] {
|
|
754
|
-
return this.#registry
|
|
755
|
-
.getSessions()
|
|
756
|
-
.filter(s => s.kind === "subagent")
|
|
757
|
-
.map(s => s.id);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/** Cycle to next (+1) or previous (-1) sub-agent session */
|
|
761
|
-
#cycleSession(direction: 1 | -1): void {
|
|
762
|
-
const ids = this.#getSubagentSessionIds();
|
|
763
|
-
if (ids.length <= 1) return;
|
|
764
|
-
const currentIdx = ids.indexOf(this.#selectedSessionId ?? "");
|
|
765
|
-
if (currentIdx < 0) return;
|
|
766
|
-
const nextIdx = (currentIdx + direction + ids.length) % ids.length;
|
|
767
|
-
this.#selectedSessionId = ids[nextIdx];
|
|
768
|
-
this.#transcriptCache = undefined;
|
|
769
|
-
this.#scrollOffset = 0;
|
|
770
|
-
this.#selectedEntryIndex = 0;
|
|
771
|
-
this.#expandedEntries.clear();
|
|
772
|
-
this.#wasAtBottom = true;
|
|
773
|
-
this.#rebuildViewerContent();
|
|
774
|
-
// Auto-scroll to bottom: select last entry
|
|
775
|
-
if (this.#viewerEntries.length > 0) {
|
|
776
|
-
this.#selectedEntryIndex = this.#viewerEntries.length - 1;
|
|
777
|
-
this.#wasAtBottom = true;
|
|
778
|
-
this.#rebuildViewerContent();
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (message.role === "user" || message.role === "developer") {
|
|
217
|
+
const text =
|
|
218
|
+
typeof message.content === "string"
|
|
219
|
+
? message.content
|
|
220
|
+
: message.content
|
|
221
|
+
.filter(part => part.type === "text")
|
|
222
|
+
.map(part => part.text)
|
|
223
|
+
.join("\n");
|
|
224
|
+
if (text.trim())
|
|
225
|
+
output.push({
|
|
226
|
+
id: entry.id,
|
|
227
|
+
kind: "user",
|
|
228
|
+
label: message.role === "developer" ? "System" : "User",
|
|
229
|
+
payload: { text, metadata: {}, source: message },
|
|
230
|
+
foldable: true,
|
|
231
|
+
});
|
|
779
232
|
}
|
|
780
233
|
}
|
|
234
|
+
return output;
|
|
235
|
+
}
|
|
781
236
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (entry.lineCount >= this.#viewportHeight) {
|
|
799
|
-
// Entry taller than viewport: only snap when it's completely out of view.
|
|
800
|
-
// If the viewport overlaps the entry at all, the user may be paging within it.
|
|
801
|
-
if (this.#scrollOffset + this.#viewportHeight <= entryTop) {
|
|
802
|
-
// Viewport is entirely above the entry — snap to entry top
|
|
803
|
-
this.#scrollOffset = Math.max(0, entryTop - 1);
|
|
804
|
-
} else if (this.#scrollOffset >= entryBottom) {
|
|
805
|
-
// Viewport is entirely below the entry — snap to show entry bottom
|
|
806
|
-
this.#scrollOffset = Math.max(0, entryBottom - this.#viewportHeight);
|
|
807
|
-
}
|
|
808
|
-
// Otherwise: viewport overlaps the entry — don't override manual scroll
|
|
809
|
-
} else {
|
|
810
|
-
// Entry fits in viewport: ensure it's fully visible
|
|
811
|
-
if (entryTop < this.#scrollOffset) {
|
|
812
|
-
this.#scrollOffset = Math.max(0, entryTop - 1);
|
|
813
|
-
}
|
|
814
|
-
if (entryBottom > this.#scrollOffset + this.#viewportHeight) {
|
|
815
|
-
this.#scrollOffset = Math.max(0, entryBottom - this.#viewportHeight + 1);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
237
|
+
function formatToolArgs(name: string, args: Record<string, unknown>): string {
|
|
238
|
+
if (name === "read" || name === "write" || name === "edit") return args.path ? `path: ${args.path}` : "";
|
|
239
|
+
if (name === "bash") return typeof args.command === "string" ? args.command.replaceAll("\t", " ") : "";
|
|
240
|
+
if (name === "search")
|
|
241
|
+
return [
|
|
242
|
+
args.pattern ? `pattern: ${args.pattern}` : "",
|
|
243
|
+
Array.isArray(args.paths) ? `paths: ${args.paths.join(", ")}` : "",
|
|
244
|
+
]
|
|
245
|
+
.filter(Boolean)
|
|
246
|
+
.join(", ");
|
|
247
|
+
return Object.entries(args)
|
|
248
|
+
.filter(([key]) => !key.startsWith("_"))
|
|
249
|
+
.map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`)
|
|
250
|
+
.join(", ")
|
|
251
|
+
.slice(0, MAX_TOOL_ARGS_CHARS);
|
|
819
252
|
}
|
|
820
253
|
|
|
821
|
-
|
|
822
|
-
|
|
254
|
+
function truncateThinking(text: string, expanded: boolean): string {
|
|
255
|
+
const limit = expanded ? 4_000 : 200;
|
|
256
|
+
return text.length > limit ? `${text.slice(0, limit)}...` : text;
|
|
257
|
+
}
|
|
823
258
|
|
|
824
259
|
function readFileIncremental(filePath: string, fromByte: number): { text: string; newSize: number } | null {
|
|
825
260
|
try {
|
|
826
261
|
const stat = fs.statSync(filePath);
|
|
827
262
|
if (stat.size <= fromByte) return { text: "", newSize: stat.size };
|
|
828
|
-
const
|
|
263
|
+
const buffer = Buffer.alloc(stat.size - fromByte);
|
|
829
264
|
const fd = fs.openSync(filePath, "r");
|
|
830
265
|
try {
|
|
831
|
-
fs.readSync(fd,
|
|
266
|
+
fs.readSync(fd, buffer, 0, buffer.length, fromByte);
|
|
832
267
|
} finally {
|
|
833
268
|
fs.closeSync(fd);
|
|
834
269
|
}
|
|
835
|
-
return { text:
|
|
270
|
+
return { text: buffer.toString("utf-8"), newSize: stat.size };
|
|
836
271
|
} catch {
|
|
837
272
|
return null;
|
|
838
273
|
}
|