hyacinth-ai 0.9.6
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 +21 -0
- package/README.md +205 -0
- package/commands.json +25 -0
- package/dist/agents/builtins.d.ts +8 -0
- package/dist/agents/builtins.js +50 -0
- package/dist/agents/config-loader.d.ts +15 -0
- package/dist/agents/config-loader.js +75 -0
- package/dist/agents/delegate-tool.d.ts +109 -0
- package/dist/agents/delegate-tool.js +407 -0
- package/dist/agents/index.d.ts +5 -0
- package/dist/agents/index.js +5 -0
- package/dist/agents/registry.d.ts +2 -0
- package/dist/agents/registry.js +2 -0
- package/dist/bypass/agents/companion/index.d.ts +57 -0
- package/dist/bypass/agents/companion/index.js +487 -0
- package/dist/bypass/agents/orchestrator/index.d.ts +17 -0
- package/dist/bypass/agents/orchestrator/index.js +414 -0
- package/dist/bypass/agents/orchestrator.d.ts +16 -0
- package/dist/bypass/agents/orchestrator.js +377 -0
- package/dist/bypass/agents/world-engine/agent.d.ts +41 -0
- package/dist/bypass/agents/world-engine/agent.js +385 -0
- package/dist/bypass/agents/world-engine/index.d.ts +52 -0
- package/dist/bypass/agents/world-engine/index.js +198 -0
- package/dist/bypass/agents/world-engine/sim.d.ts +14 -0
- package/dist/bypass/agents/world-engine/sim.js +120 -0
- package/dist/bypass/agents/world-engine/store.d.ts +110 -0
- package/dist/bypass/agents/world-engine/store.js +519 -0
- package/dist/bypass/agents/world-engine/ticker.d.ts +37 -0
- package/dist/bypass/agents/world-engine/ticker.js +214 -0
- package/dist/bypass/agents/world-engine/tools.d.ts +6 -0
- package/dist/bypass/agents/world-engine/tools.js +380 -0
- package/dist/bypass/agents/world-engine/types.d.ts +202 -0
- package/dist/bypass/agents/world-engine/types.js +56 -0
- package/dist/bypass/agents/world-engine.d.ts +20 -0
- package/dist/bypass/agents/world-engine.js +63 -0
- package/dist/bypass/base.d.ts +48 -0
- package/dist/bypass/base.js +126 -0
- package/dist/bypass/index.d.ts +5 -0
- package/dist/bypass/index.js +6 -0
- package/dist/bypass/manager.d.ts +41 -0
- package/dist/bypass/manager.js +179 -0
- package/dist/bypass/types.d.ts +63 -0
- package/dist/bypass/types.js +14 -0
- package/dist/channels/auto-detect.d.ts +25 -0
- package/dist/channels/auto-detect.js +77 -0
- package/dist/channels/builtin/http-webhook.d.ts +25 -0
- package/dist/channels/builtin/http-webhook.js +287 -0
- package/dist/channels/builtin/tui-channel.d.ts +33 -0
- package/dist/channels/builtin/tui-channel.js +61 -0
- package/dist/channels/builtin/tui-ws-session.d.ts +16 -0
- package/dist/channels/builtin/tui-ws-session.js +90 -0
- package/dist/channels/builtin/webui-output-handler.d.ts +30 -0
- package/dist/channels/builtin/webui-output-handler.js +93 -0
- package/dist/channels/index.d.ts +5 -0
- package/dist/channels/index.js +3 -0
- package/dist/channels/interface.d.ts +148 -0
- package/dist/channels/interface.js +15 -0
- package/dist/channels/manager.d.ts +43 -0
- package/dist/channels/manager.js +156 -0
- package/dist/channels/message-queue.d.ts +36 -0
- package/dist/channels/message-queue.js +91 -0
- package/dist/channels/plugins/clawbot/clawbot-auth.d.ts +50 -0
- package/dist/channels/plugins/clawbot/clawbot-auth.js +235 -0
- package/dist/channels/plugins/clawbot/clawbot-channel.d.ts +76 -0
- package/dist/channels/plugins/clawbot/clawbot-channel.js +566 -0
- package/dist/channels/plugins/clawbot/clawbot-client.d.ts +180 -0
- package/dist/channels/plugins/clawbot/clawbot-client.js +312 -0
- package/dist/channels/plugins/clawbot/clawbot-config.d.ts +30 -0
- package/dist/channels/plugins/clawbot/clawbot-config.js +28 -0
- package/dist/channels/plugins/clawbot/clawbot-message-queue.d.ts +27 -0
- package/dist/channels/plugins/clawbot/clawbot-message-queue.js +114 -0
- package/dist/channels/plugins/clawbot/clawbot-session.d.ts +10 -0
- package/dist/channels/plugins/clawbot/clawbot-session.js +26 -0
- package/dist/channels/plugins/clawbot/index.d.ts +17 -0
- package/dist/channels/plugins/clawbot/index.js +58 -0
- package/dist/channels/plugins/feishu/feishu-channel.d.ts +57 -0
- package/dist/channels/plugins/feishu/feishu-channel.js +602 -0
- package/dist/channels/plugins/feishu/feishu-client.d.ts +23 -0
- package/dist/channels/plugins/feishu/feishu-client.js +86 -0
- package/dist/channels/plugins/feishu/feishu-config.d.ts +53 -0
- package/dist/channels/plugins/feishu/feishu-config.js +35 -0
- package/dist/channels/plugins/feishu/feishu-event.d.ts +102 -0
- package/dist/channels/plugins/feishu/feishu-event.js +257 -0
- package/dist/channels/plugins/feishu/feishu-log-suppressor.d.ts +7 -0
- package/dist/channels/plugins/feishu/feishu-log-suppressor.js +34 -0
- package/dist/channels/plugins/feishu/feishu-message-queue.d.ts +27 -0
- package/dist/channels/plugins/feishu/feishu-message-queue.js +112 -0
- package/dist/channels/plugins/feishu/feishu-send.d.ts +44 -0
- package/dist/channels/plugins/feishu/feishu-send.js +232 -0
- package/dist/channels/plugins/feishu/feishu-session.d.ts +44 -0
- package/dist/channels/plugins/feishu/feishu-session.js +84 -0
- package/dist/channels/plugins/feishu/feishu-streaming.d.ts +49 -0
- package/dist/channels/plugins/feishu/feishu-streaming.js +185 -0
- package/dist/channels/plugins/feishu/feishu-transport.d.ts +29 -0
- package/dist/channels/plugins/feishu/feishu-transport.js +99 -0
- package/dist/channels/plugins/feishu/index.d.ts +22 -0
- package/dist/channels/plugins/feishu/index.js +68 -0
- package/dist/cli/doctor.d.ts +20 -0
- package/dist/cli/doctor.js +317 -0
- package/dist/context/cache-strategy.d.ts +76 -0
- package/dist/context/cache-strategy.js +123 -0
- package/dist/context/composer.d.ts +77 -0
- package/dist/context/composer.js +318 -0
- package/dist/context/compressor.d.ts +218 -0
- package/dist/context/compressor.js +909 -0
- package/dist/context/index.d.ts +14 -0
- package/dist/context/index.js +8 -0
- package/dist/context/interface.d.ts +33 -0
- package/dist/context/interface.js +2 -0
- package/dist/context/manifest-defaults.d.ts +51 -0
- package/dist/context/manifest-defaults.js +133 -0
- package/dist/context/manifest-loader.d.ts +16 -0
- package/dist/context/manifest-loader.js +103 -0
- package/dist/context/manifest-types.d.ts +26 -0
- package/dist/context/manifest-types.js +2 -0
- package/dist/context/precision/analyzer.d.ts +31 -0
- package/dist/context/precision/analyzer.js +131 -0
- package/dist/context/precision/companion.d.ts +27 -0
- package/dist/context/precision/companion.js +144 -0
- package/dist/context/precision/default.d.ts +14 -0
- package/dist/context/precision/default.js +17 -0
- package/dist/context/precision/index.d.ts +9 -0
- package/dist/context/precision/index.js +7 -0
- package/dist/context/precision/keyword-pool.d.ts +27 -0
- package/dist/context/precision/keyword-pool.js +66 -0
- package/dist/context/precision/strategy.d.ts +18 -0
- package/dist/context/precision/strategy.js +51 -0
- package/dist/context/precision/summary-store.d.ts +18 -0
- package/dist/context/precision/summary-store.js +67 -0
- package/dist/context/precision/types.d.ts +23 -0
- package/dist/context/precision/types.js +2 -0
- package/dist/context/profiles.d.ts +45 -0
- package/dist/context/profiles.js +92 -0
- package/dist/context/prompt-builder.d.ts +76 -0
- package/dist/context/prompt-builder.js +133 -0
- package/dist/context/retriever.d.ts +44 -0
- package/dist/context/retriever.js +203 -0
- package/dist/context/router.d.ts +122 -0
- package/dist/context/router.js +291 -0
- package/dist/context/section-resolver.d.ts +38 -0
- package/dist/context/section-resolver.js +398 -0
- package/dist/context/tokenizer.d.ts +23 -0
- package/dist/context/tokenizer.js +78 -0
- package/dist/dependency/analyzer.d.ts +33 -0
- package/dist/dependency/analyzer.js +242 -0
- package/dist/dependency/data-flow-tracker.d.ts +12 -0
- package/dist/dependency/data-flow-tracker.js +97 -0
- package/dist/dependency/function-parser.d.ts +34 -0
- package/dist/dependency/function-parser.js +295 -0
- package/dist/dependency/index.d.ts +14 -0
- package/dist/dependency/index.js +20 -0
- package/dist/dependency/parser.d.ts +10 -0
- package/dist/dependency/parser.js +93 -0
- package/dist/dependency/types.d.ts +57 -0
- package/dist/dependency/types.js +2 -0
- package/dist/env/env-collector.d.ts +45 -0
- package/dist/env/env-collector.js +221 -0
- package/dist/env/index.d.ts +3 -0
- package/dist/env/index.js +2 -0
- package/dist/event-store.d.ts +27 -0
- package/dist/event-store.js +71 -0
- package/dist/evolution/git-manager.d.ts +46 -0
- package/dist/evolution/git-manager.js +276 -0
- package/dist/evolution/index.d.ts +3 -0
- package/dist/evolution/index.js +2 -0
- package/dist/flow/bootstrap-flow.d.ts +19 -0
- package/dist/flow/bootstrap-flow.js +72 -0
- package/dist/flow/flow-registry.d.ts +12 -0
- package/dist/flow/flow-registry.js +74 -0
- package/dist/flow/flow-tools.d.ts +8 -0
- package/dist/flow/flow-tools.js +106 -0
- package/dist/flow/index.d.ts +6 -0
- package/dist/flow/index.js +8 -0
- package/dist/flow/todo-flow.d.ts +25 -0
- package/dist/flow/todo-flow.js +107 -0
- package/dist/flow/types.d.ts +78 -0
- package/dist/flow/types.js +12 -0
- package/dist/flows/bootstrap.d.ts +18 -0
- package/dist/flows/bootstrap.js +119 -0
- package/dist/flows/index.d.ts +6 -0
- package/dist/flows/index.js +11 -0
- package/dist/flows/todo.d.ts +26 -0
- package/dist/flows/todo.js +174 -0
- package/dist/flows/tools.d.ts +8 -0
- package/dist/flows/tools.js +119 -0
- package/dist/flows/types.d.ts +27 -0
- package/dist/flows/types.js +10 -0
- package/dist/gateway/cli.d.ts +5 -0
- package/dist/gateway/cli.js +1027 -0
- package/dist/gateway/factory.d.ts +80 -0
- package/dist/gateway/factory.js +832 -0
- package/dist/gateway/guardian.d.ts +8 -0
- package/dist/gateway/guardian.js +30 -0
- package/dist/gateway/index.d.ts +3 -0
- package/dist/gateway/index.js +3 -0
- package/dist/gateway/server.d.ts +17 -0
- package/dist/gateway/server.js +96 -0
- package/dist/gateway/tui.d.ts +16 -0
- package/dist/gateway/tui.js +3273 -0
- package/dist/hot-reload/agent-watcher.d.ts +24 -0
- package/dist/hot-reload/agent-watcher.js +175 -0
- package/dist/hot-reload/bundle-watcher.d.ts +12 -0
- package/dist/hot-reload/bundle-watcher.js +35 -0
- package/dist/hot-reload/channel-watcher.d.ts +19 -0
- package/dist/hot-reload/channel-watcher.js +54 -0
- package/dist/hot-reload/command-watcher.d.ts +14 -0
- package/dist/hot-reload/command-watcher.js +54 -0
- package/dist/hot-reload/config-watcher.d.ts +20 -0
- package/dist/hot-reload/config-watcher.js +58 -0
- package/dist/hot-reload/index.d.ts +12 -0
- package/dist/hot-reload/index.js +12 -0
- package/dist/hot-reload/manager.d.ts +58 -0
- package/dist/hot-reload/manager.js +243 -0
- package/dist/hot-reload/manifest-watcher.d.ts +9 -0
- package/dist/hot-reload/manifest-watcher.js +42 -0
- package/dist/hot-reload/mcp-watcher.d.ts +18 -0
- package/dist/hot-reload/mcp-watcher.js +64 -0
- package/dist/hot-reload/model-catalog-watcher.d.ts +18 -0
- package/dist/hot-reload/model-catalog-watcher.js +62 -0
- package/dist/hot-reload/plugin-watcher.d.ts +20 -0
- package/dist/hot-reload/plugin-watcher.js +56 -0
- package/dist/hot-reload/prompt-watcher.d.ts +6 -0
- package/dist/hot-reload/prompt-watcher.js +38 -0
- package/dist/hot-reload/provider-watcher.d.ts +16 -0
- package/dist/hot-reload/provider-watcher.js +45 -0
- package/dist/hot-reload/skill-watcher.d.ts +19 -0
- package/dist/hot-reload/skill-watcher.js +82 -0
- package/dist/hot-reload/tool-watcher.d.ts +16 -0
- package/dist/hot-reload/tool-watcher.js +168 -0
- package/dist/hot-reload/workflow-watcher.d.ts +19 -0
- package/dist/hot-reload/workflow-watcher.js +84 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/knowledge/fts5-retriever.d.ts +30 -0
- package/dist/knowledge/fts5-retriever.js +223 -0
- package/dist/knowledge/index.d.ts +11 -0
- package/dist/knowledge/index.js +14 -0
- package/dist/knowledge/indexer.d.ts +41 -0
- package/dist/knowledge/indexer.js +186 -0
- package/dist/knowledge/query-preprocessor.d.ts +13 -0
- package/dist/knowledge/query-preprocessor.js +63 -0
- package/dist/knowledge/retriever.d.ts +45 -0
- package/dist/knowledge/retriever.js +9 -0
- package/dist/knowledge/store.d.ts +27 -0
- package/dist/knowledge/store.js +61 -0
- package/dist/knowledge/structured-store.d.ts +78 -0
- package/dist/knowledge/structured-store.js +280 -0
- package/dist/knowledge/structured-tools.d.ts +12 -0
- package/dist/knowledge/structured-tools.js +151 -0
- package/dist/knowledge/tools.d.ts +20 -0
- package/dist/knowledge/tools.js +238 -0
- package/dist/knowledge/watcher.d.ts +28 -0
- package/dist/knowledge/watcher.js +113 -0
- package/dist/lifecycle/index.d.ts +12 -0
- package/dist/lifecycle/index.js +14 -0
- package/dist/lifecycle/interface.d.ts +94 -0
- package/dist/lifecycle/interface.js +2 -0
- package/dist/lifecycle/local-model.d.ts +62 -0
- package/dist/lifecycle/local-model.js +331 -0
- package/dist/lifecycle/manager.d.ts +70 -0
- package/dist/lifecycle/manager.js +374 -0
- package/dist/lifecycle/supervisor.d.ts +89 -0
- package/dist/lifecycle/supervisor.js +277 -0
- package/dist/lifecycle-integration-test.d.ts +2 -0
- package/dist/lifecycle-integration-test.js +72 -0
- package/dist/local-model/download-manager.d.ts +59 -0
- package/dist/local-model/download-manager.js +517 -0
- package/dist/local-model/index.d.ts +109 -0
- package/dist/local-model/index.js +233 -0
- package/dist/local-model/llamacpp-provider.d.ts +95 -0
- package/dist/local-model/llamacpp-provider.js +379 -0
- package/dist/local-model/model-bridge.d.ts +48 -0
- package/dist/local-model/model-bridge.js +256 -0
- package/dist/local-model/model-registry.d.ts +47 -0
- package/dist/local-model/model-registry.js +188 -0
- package/dist/local-model/types.d.ts +49 -0
- package/dist/local-model/types.js +2 -0
- package/dist/logging/index.d.ts +3 -0
- package/dist/logging/index.js +3 -0
- package/dist/logging/logger.d.ts +56 -0
- package/dist/logging/logger.js +94 -0
- package/dist/machine/flows/bootstrap.d.ts +18 -0
- package/dist/machine/flows/bootstrap.js +119 -0
- package/dist/machine/flows/index.d.ts +6 -0
- package/dist/machine/flows/index.js +11 -0
- package/dist/machine/flows/spec.d.ts +17 -0
- package/dist/machine/flows/spec.js +176 -0
- package/dist/machine/flows/todo.d.ts +26 -0
- package/dist/machine/flows/todo.js +175 -0
- package/dist/machine/flows/tools.d.ts +8 -0
- package/dist/machine/flows/tools.js +119 -0
- package/dist/machine/flows/types.d.ts +33 -0
- package/dist/machine/flows/types.js +10 -0
- package/dist/machine/index.d.ts +8 -0
- package/dist/machine/index.js +9 -0
- package/dist/machine/registry.d.ts +34 -0
- package/dist/machine/registry.js +98 -0
- package/dist/machine/runner.d.ts +41 -0
- package/dist/machine/runner.js +177 -0
- package/dist/machine/types.d.ts +121 -0
- package/dist/machine/types.js +9 -0
- package/dist/mcp/bridge.d.ts +33 -0
- package/dist/mcp/bridge.js +99 -0
- package/dist/mcp/client.d.ts +37 -0
- package/dist/mcp/client.js +161 -0
- package/dist/mcp/config.d.ts +15 -0
- package/dist/mcp/config.js +85 -0
- package/dist/mcp/index.d.ts +7 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/install-manager.d.ts +31 -0
- package/dist/mcp/install-manager.js +158 -0
- package/dist/mcp/lifecycle.d.ts +41 -0
- package/dist/mcp/lifecycle.js +175 -0
- package/dist/mcp/process-transport.d.ts +21 -0
- package/dist/mcp/process-transport.js +59 -0
- package/dist/mcp/side-effect.d.ts +5 -0
- package/dist/mcp/side-effect.js +21 -0
- package/dist/mcp/system.d.ts +95 -0
- package/dist/mcp/system.js +279 -0
- package/dist/memory/companion-session.d.ts +22 -0
- package/dist/memory/companion-session.js +77 -0
- package/dist/memory/compression-boundary.d.ts +20 -0
- package/dist/memory/compression-boundary.js +40 -0
- package/dist/memory/conversation.d.ts +34 -0
- package/dist/memory/conversation.js +107 -0
- package/dist/memory/events.d.ts +42 -0
- package/dist/memory/events.js +56 -0
- package/dist/memory/index.d.ts +7 -0
- package/dist/memory/index.js +6 -0
- package/dist/memory/memory-store.d.ts +11 -0
- package/dist/memory/memory-store.js +51 -0
- package/dist/memory/session-allowlist.d.ts +11 -0
- package/dist/memory/session-allowlist.js +50 -0
- package/dist/memory/session.d.ts +48 -0
- package/dist/memory/session.js +275 -0
- package/dist/memory/stats.d.ts +23 -0
- package/dist/memory/stats.js +59 -0
- package/dist/memory/summary.d.ts +7 -0
- package/dist/memory/summary.js +23 -0
- package/dist/multimodal/index.d.ts +66 -0
- package/dist/multimodal/index.js +210 -0
- package/dist/orchestrator/index.d.ts +5 -0
- package/dist/orchestrator/index.js +4 -0
- package/dist/orchestrator/loop.d.ts +331 -0
- package/dist/orchestrator/loop.js +2443 -0
- package/dist/orchestrator/plan-store.d.ts +81 -0
- package/dist/orchestrator/plan-store.js +167 -0
- package/dist/orchestrator/planner.d.ts +20 -0
- package/dist/orchestrator/planner.js +34 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +2 -0
- package/dist/parser/router.d.ts +30 -0
- package/dist/parser/router.js +61 -0
- package/dist/plugins/api.d.ts +39 -0
- package/dist/plugins/api.js +85 -0
- package/dist/plugins/define.d.ts +27 -0
- package/dist/plugins/define.js +32 -0
- package/dist/plugins/index.d.ts +9 -0
- package/dist/plugins/index.js +5 -0
- package/dist/plugins/loader.d.ts +39 -0
- package/dist/plugins/loader.js +128 -0
- package/dist/plugins/manager.d.ts +61 -0
- package/dist/plugins/manager.js +257 -0
- package/dist/plugins/types.d.ts +82 -0
- package/dist/plugins/types.js +2 -0
- package/dist/prompts/agents/code-reviewer.md +16 -0
- package/dist/prompts/agents/security-auditor.md +17 -0
- package/dist/prompts/agents/test-writer.md +15 -0
- package/dist/prompts/attention/attention.md +12 -0
- package/dist/prompts/environment/environment.md +18 -0
- package/dist/prompts/environment/feishu.md +107 -0
- package/dist/prompts/flows/bootstrap-confirm.md +14 -0
- package/dist/prompts/flows/bootstrap-identity.md +11 -0
- package/dist/prompts/flows/bootstrap-soul.md +19 -0
- package/dist/prompts/flows/bootstrap-user.md +11 -0
- package/dist/prompts/flows/spec-checklist.md +27 -0
- package/dist/prompts/flows/spec-define.md +64 -0
- package/dist/prompts/flows/spec-executing-checklist.md +15 -0
- package/dist/prompts/flows/spec-executing-tasks.md +15 -0
- package/dist/prompts/flows/spec-spec.md +51 -0
- package/dist/prompts/flows/spec-tasks.md +32 -0
- package/dist/prompts/flows/todo-execution.md +7 -0
- package/dist/prompts/flows/todo-planning.md +18 -0
- package/dist/prompts/framework/architecture.md +131 -0
- package/dist/prompts/identity/identity.md +2 -0
- package/dist/prompts/loader.d.ts +24 -0
- package/dist/prompts/loader.js +102 -0
- package/dist/prompts/loader.ts +119 -0
- package/dist/prompts/orchestration/summary.md +14 -0
- package/dist/prompts/persona/BOOTSTRAP.md +5 -0
- package/dist/prompts/persona/IDENTITY.md +16 -0
- package/dist/prompts/persona/PartnerMemory.md +5 -0
- package/dist/prompts/persona/PartnerSoul.md +7 -0
- package/dist/prompts/persona/SOUL.md +30 -0
- package/dist/prompts/persona/USER.md +16 -0
- package/dist/prompts/precise/analyze.md +23 -0
- package/dist/prompts/precise/persona.md +18 -0
- package/dist/prompts/safety/coding-standards.md +5 -0
- package/dist/prompts/safety/safety.md +4 -0
- package/dist/prompts/skills/code-review.md +4 -0
- package/dist/prompts/skills/debug.md +7 -0
- package/dist/prompts/skills/framework-reference.md +131 -0
- package/dist/prompts/skills/refactor.md +6 -0
- package/dist/prompts/summary.md +22 -0
- package/dist/prompts/tools/framework-capabilities.md +241 -0
- package/dist/prompts/tools/tool-rules.md +15 -0
- package/dist/provider/anthropic.d.ts +62 -0
- package/dist/provider/anthropic.js +361 -0
- package/dist/provider/catalog.d.ts +58 -0
- package/dist/provider/catalog.js +97 -0
- package/dist/provider/compatible.d.ts +89 -0
- package/dist/provider/compatible.js +328 -0
- package/dist/provider/config.d.ts +23 -0
- package/dist/provider/config.js +158 -0
- package/dist/provider/deepseek.d.ts +25 -0
- package/dist/provider/deepseek.js +37 -0
- package/dist/provider/fallback.d.ts +51 -0
- package/dist/provider/fallback.js +113 -0
- package/dist/provider/gemini.d.ts +27 -0
- package/dist/provider/gemini.js +170 -0
- package/dist/provider/index.d.ts +36 -0
- package/dist/provider/index.js +42 -0
- package/dist/provider/interface.d.ts +62 -0
- package/dist/provider/interface.js +2 -0
- package/dist/provider/local-config.d.ts +37 -0
- package/dist/provider/local-config.js +146 -0
- package/dist/provider/local.d.ts +45 -0
- package/dist/provider/local.js +248 -0
- package/dist/provider/manager.d.ts +67 -0
- package/dist/provider/manager.js +355 -0
- package/dist/provider/mimo.d.ts +19 -0
- package/dist/provider/mimo.js +30 -0
- package/dist/provider/minimax.d.ts +22 -0
- package/dist/provider/minimax.js +33 -0
- package/dist/provider/model-catalog-loader.d.ts +42 -0
- package/dist/provider/model-catalog-loader.js +89 -0
- package/dist/provider/model-channel-registry.d.ts +130 -0
- package/dist/provider/model-channel-registry.js +488 -0
- package/dist/provider/model-router.d.ts +60 -0
- package/dist/provider/model-router.js +87 -0
- package/dist/provider/openai.d.ts +54 -0
- package/dist/provider/openai.js +274 -0
- package/dist/provider/qwen.d.ts +19 -0
- package/dist/provider/qwen.js +30 -0
- package/dist/provider/resilient.d.ts +46 -0
- package/dist/provider/resilient.js +181 -0
- package/dist/provider/retry.d.ts +16 -0
- package/dist/provider/retry.js +78 -0
- package/dist/provider/router.d.ts +68 -0
- package/dist/provider/router.js +145 -0
- package/dist/provider/tool-args-recovery.d.ts +36 -0
- package/dist/provider/tool-args-recovery.js +91 -0
- package/dist/provider/user-id.d.ts +13 -0
- package/dist/provider/user-id.js +53 -0
- package/dist/provider/zhipu.d.ts +19 -0
- package/dist/provider/zhipu.js +32 -0
- package/dist/registry/agent.registry.d.ts +57 -0
- package/dist/registry/agent.registry.js +224 -0
- package/dist/registry/base.d.ts +54 -0
- package/dist/registry/base.js +99 -0
- package/dist/registry/channel.registry.d.ts +53 -0
- package/dist/registry/channel.registry.js +87 -0
- package/dist/registry/index.d.ts +9 -0
- package/dist/registry/index.js +10 -0
- package/dist/registry/mcp.registry.d.ts +2 -0
- package/dist/registry/mcp.registry.js +2 -0
- package/dist/registry/plugin.registry.d.ts +33 -0
- package/dist/registry/plugin.registry.js +66 -0
- package/dist/registry/provider.registry.d.ts +51 -0
- package/dist/registry/provider.registry.js +104 -0
- package/dist/registry/skill.registry.d.ts +33 -0
- package/dist/registry/skill.registry.js +95 -0
- package/dist/registry/tool.registry.d.ts +68 -0
- package/dist/registry/tool.registry.js +185 -0
- package/dist/repair/loop-guard.d.ts +98 -0
- package/dist/repair/loop-guard.js +217 -0
- package/dist/repair/scavenge.d.ts +19 -0
- package/dist/repair/scavenge.js +81 -0
- package/dist/repair/storm.d.ts +7 -0
- package/dist/repair/storm.js +7 -0
- package/dist/rollback/index.d.ts +5 -0
- package/dist/rollback/index.js +4 -0
- package/dist/rollback/rollback-tool.d.ts +13 -0
- package/dist/rollback/rollback-tool.js +167 -0
- package/dist/rollback/turn-recorder.d.ts +37 -0
- package/dist/rollback/turn-recorder.js +183 -0
- package/dist/rollback/turn-store.d.ts +29 -0
- package/dist/rollback/turn-store.js +114 -0
- package/dist/rollback/types.d.ts +51 -0
- package/dist/rollback/types.js +9 -0
- package/dist/runtime/config-center.d.ts +92 -0
- package/dist/runtime/config-center.js +467 -0
- package/dist/runtime/config-schema.d.ts +201 -0
- package/dist/runtime/config-schema.js +2 -0
- package/dist/runtime/defaults.d.ts +3 -0
- package/dist/runtime/defaults.js +146 -0
- package/dist/schedule/cron.d.ts +24 -0
- package/dist/schedule/cron.js +145 -0
- package/dist/schedule/index.d.ts +6 -0
- package/dist/schedule/index.js +4 -0
- package/dist/schedule/persistence.d.ts +33 -0
- package/dist/schedule/persistence.js +106 -0
- package/dist/schedule/scheduler.d.ts +85 -0
- package/dist/schedule/scheduler.js +630 -0
- package/dist/schedule/types.d.ts +185 -0
- package/dist/schedule/types.js +2 -0
- package/dist/setup/config.d.ts +189 -0
- package/dist/setup/config.js +253 -0
- package/dist/setup/index.d.ts +3 -0
- package/dist/setup/index.js +3 -0
- package/dist/setup/model-defaults.d.ts +13 -0
- package/dist/setup/model-defaults.js +87 -0
- package/dist/setup/persona-bootstrap.d.ts +42 -0
- package/dist/setup/persona-bootstrap.js +379 -0
- package/dist/setup/wizard.d.ts +32 -0
- package/dist/setup/wizard.js +384 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.js +4 -0
- package/dist/skills/loader.d.ts +12 -0
- package/dist/skills/loader.js +83 -0
- package/dist/skills/registry.d.ts +2 -0
- package/dist/skills/registry.js +2 -0
- package/dist/skills/skill-tool.d.ts +20 -0
- package/dist/skills/skill-tool.js +49 -0
- package/dist/tools/archive.d.ts +17 -0
- package/dist/tools/archive.js +131 -0
- package/dist/tools/ask-user.d.ts +9 -0
- package/dist/tools/ask-user.js +62 -0
- package/dist/tools/background-registry.d.ts +45 -0
- package/dist/tools/background-registry.js +164 -0
- package/dist/tools/bash.d.ts +56 -0
- package/dist/tools/bash.js +391 -0
- package/dist/tools/bundle-registry.d.ts +42 -0
- package/dist/tools/bundle-registry.js +239 -0
- package/dist/tools/bundle-tools.d.ts +11 -0
- package/dist/tools/bundle-tools.js +168 -0
- package/dist/tools/channel-info.d.ts +5 -0
- package/dist/tools/channel-info.js +38 -0
- package/dist/tools/compression.d.ts +7 -0
- package/dist/tools/compression.js +21 -0
- package/dist/tools/config.d.ts +23 -0
- package/dist/tools/config.js +255 -0
- package/dist/tools/db-query.d.ts +12 -0
- package/dist/tools/db-query.js +107 -0
- package/dist/tools/diagnostics.d.ts +30 -0
- package/dist/tools/diagnostics.js +111 -0
- package/dist/tools/diff-channel.d.ts +9 -0
- package/dist/tools/diff-channel.js +17 -0
- package/dist/tools/diff-files.d.ts +17 -0
- package/dist/tools/diff-files.js +88 -0
- package/dist/tools/disk-usage.d.ts +34 -0
- package/dist/tools/disk-usage.js +258 -0
- package/dist/tools/edit.d.ts +30 -0
- package/dist/tools/edit.js +235 -0
- package/dist/tools/executor.d.ts +32 -0
- package/dist/tools/executor.js +66 -0
- package/dist/tools/file-tracker.d.ts +20 -0
- package/dist/tools/file-tracker.js +44 -0
- package/dist/tools/filtered-registry.d.ts +29 -0
- package/dist/tools/filtered-registry.js +50 -0
- package/dist/tools/flow.d.ts +10 -0
- package/dist/tools/flow.js +180 -0
- package/dist/tools/git-tool.d.ts +24 -0
- package/dist/tools/git-tool.js +113 -0
- package/dist/tools/glob.d.ts +29 -0
- package/dist/tools/glob.js +152 -0
- package/dist/tools/grep.d.ts +14 -0
- package/dist/tools/grep.js +325 -0
- package/dist/tools/http-request.d.ts +14 -0
- package/dist/tools/http-request.js +126 -0
- package/dist/tools/index.d.ts +43 -0
- package/dist/tools/index.js +76 -0
- package/dist/tools/injection-filter.d.ts +6 -0
- package/dist/tools/injection-filter.js +71 -0
- package/dist/tools/insert.d.ts +13 -0
- package/dist/tools/insert.js +61 -0
- package/dist/tools/interface.d.ts +30 -0
- package/dist/tools/interface.js +2 -0
- package/dist/tools/json-edit.d.ts +15 -0
- package/dist/tools/json-edit.js +122 -0
- package/dist/tools/multi-edit.d.ts +9 -0
- package/dist/tools/multi-edit.js +146 -0
- package/dist/tools/path-sandbox.d.ts +14 -0
- package/dist/tools/path-sandbox.js +60 -0
- package/dist/tools/process-tools.d.ts +15 -0
- package/dist/tools/process-tools.js +95 -0
- package/dist/tools/python-bridge/bridge.d.ts +24 -0
- package/dist/tools/python-bridge/bridge.js +39 -0
- package/dist/tools/python-bridge/executor.d.ts +6 -0
- package/dist/tools/python-bridge/executor.js +69 -0
- package/dist/tools/python-bridge/index.d.ts +5 -0
- package/dist/tools/python-bridge/index.js +4 -0
- package/dist/tools/python-bridge/parser.d.ts +21 -0
- package/dist/tools/python-bridge/parser.js +258 -0
- package/dist/tools/read.d.ts +16 -0
- package/dist/tools/read.js +276 -0
- package/dist/tools/registry.d.ts +2 -0
- package/dist/tools/registry.js +2 -0
- package/dist/tools/restart.d.ts +10 -0
- package/dist/tools/restart.js +46 -0
- package/dist/tools/result-buffer.d.ts +66 -0
- package/dist/tools/result-buffer.js +146 -0
- package/dist/tools/runtime-control.d.ts +163 -0
- package/dist/tools/runtime-control.js +1780 -0
- package/dist/tools/symbol-references.d.ts +5 -0
- package/dist/tools/symbol-references.js +331 -0
- package/dist/tools/system-info.d.ts +3 -0
- package/dist/tools/system-info.js +55 -0
- package/dist/tools/write.d.ts +18 -0
- package/dist/tools/write.js +118 -0
- package/dist/tools/xref/index.d.ts +25 -0
- package/dist/tools/xref/index.js +25 -0
- package/dist/tools/xref/manager.d.ts +85 -0
- package/dist/tools/xref/manager.js +948 -0
- package/dist/tools/xref/parser.d.ts +34 -0
- package/dist/tools/xref/parser.js +64 -0
- package/dist/tools/xref/regex-parser.d.ts +26 -0
- package/dist/tools/xref/regex-parser.js +446 -0
- package/dist/tools/xref/schema.d.ts +104 -0
- package/dist/tools/xref/schema.js +68 -0
- package/dist/tools/xref/ts-parser.d.ts +20 -0
- package/dist/tools/xref/ts-parser.js +403 -0
- package/dist/tools/xref/xref-build.d.ts +20 -0
- package/dist/tools/xref/xref-build.js +128 -0
- package/dist/tools/xref/xref-graph.d.ts +24 -0
- package/dist/tools/xref/xref-graph.js +88 -0
- package/dist/tools/xref/xref-query.d.ts +30 -0
- package/dist/tools/xref/xref-query.js +123 -0
- package/dist/types.d.ts +217 -0
- package/dist/types.js +2 -0
- package/dist/ui/assistant-message.d.ts +7 -0
- package/dist/ui/assistant-message.js +18 -0
- package/dist/ui/chat-log.d.ts +52 -0
- package/dist/ui/chat-log.js +246 -0
- package/dist/ui/command-registry.d.ts +73 -0
- package/dist/ui/command-registry.js +838 -0
- package/dist/ui/diff-component.d.ts +14 -0
- package/dist/ui/diff-component.js +42 -0
- package/dist/ui/hyperlink-markdown.d.ts +10 -0
- package/dist/ui/hyperlink-markdown.js +21 -0
- package/dist/ui/markdown-message.d.ts +10 -0
- package/dist/ui/markdown-message.js +16 -0
- package/dist/ui/osc8-hyperlinks.d.ts +4 -0
- package/dist/ui/osc8-hyperlinks.js +142 -0
- package/dist/ui/pi-tui-editor.d.ts +35 -0
- package/dist/ui/pi-tui-editor.js +79 -0
- package/dist/ui/slash-commands.d.ts +16 -0
- package/dist/ui/slash-commands.js +29 -0
- package/dist/ui/slash-panel.d.ts +54 -0
- package/dist/ui/slash-panel.js +199 -0
- package/dist/ui/theme.d.ts +120 -0
- package/dist/ui/theme.js +154 -0
- package/dist/ui/tool-display.d.ts +13 -0
- package/dist/ui/tool-display.js +85 -0
- package/dist/ui/tool-execution.d.ts +22 -0
- package/dist/ui/tool-execution.js +87 -0
- package/dist/ui/tui-formatters.d.ts +2 -0
- package/dist/ui/tui-formatters.js +145 -0
- package/dist/ui/user-message.d.ts +5 -0
- package/dist/ui/user-message.js +11 -0
- package/dist/update/check.d.ts +8 -0
- package/dist/update/check.js +28 -0
- package/dist/update/config.d.ts +6 -0
- package/dist/update/config.js +18 -0
- package/dist/update/download.d.ts +3 -0
- package/dist/update/download.js +27 -0
- package/dist/update/index.d.ts +6 -0
- package/dist/update/index.js +5 -0
- package/dist/update/install.d.ts +3 -0
- package/dist/update/install.js +33 -0
- package/dist/update/types.d.ts +16 -0
- package/dist/update/types.js +2 -0
- package/dist/utils/diff.d.ts +9 -0
- package/dist/utils/diff.js +45 -0
- package/dist/utils/misc.d.ts +12 -0
- package/dist/utils/misc.js +34 -0
- package/dist/world-engine/agent.d.ts +41 -0
- package/dist/world-engine/agent.js +385 -0
- package/dist/world-engine/index.d.ts +45 -0
- package/dist/world-engine/index.js +160 -0
- package/dist/world-engine/sim.d.ts +14 -0
- package/dist/world-engine/sim.js +120 -0
- package/dist/world-engine/store.d.ts +110 -0
- package/dist/world-engine/store.js +519 -0
- package/dist/world-engine/ticker.d.ts +37 -0
- package/dist/world-engine/ticker.js +214 -0
- package/dist/world-engine/tools.d.ts +6 -0
- package/dist/world-engine/tools.js +380 -0
- package/dist/world-engine/types.d.ts +202 -0
- package/dist/world-engine/types.js +56 -0
- package/docs/api.md +180 -0
- package/docs/architecture.md +206 -0
- package/docs/diagnostics/2026-06-08-config-prompt-guidance.md +67 -0
- package/docs/superpowers/plans/2026-06-08-global-bootstrap-mode.md +89 -0
- package/docs/superpowers/specs/2026-06-08-global-bootstrap-mode-design.md +174 -0
- package/docs/user-guide.md +226 -0
- package/package.json +63 -0
- package/plugins/example-greeter/index.js +55 -0
- package/plugins/example-greeter/plugin.json +8 -0
- package/pnpm-workspace.yaml +5 -0
- package/provider_arch.md +179 -0
- package/start-webui.sh +27 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/webui-channel-design.md +789 -0
|
@@ -0,0 +1,3273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Gateway — pi-tui component-based terminal UI for the Agent framework.
|
|
3
|
+
*
|
|
4
|
+
* Powered by @earendil-works/pi-tui.
|
|
5
|
+
*
|
|
6
|
+
* Layout (top to bottom):
|
|
7
|
+
* headerContainer — Agent · model · Turns · Mode · Provider
|
|
8
|
+
* contextBarText — Context ████░░░░ 45%
|
|
9
|
+
* chatLog — message history (grows to fill)
|
|
10
|
+
* footerText — Ctrl+C exit | Ctrl+L clear | ...
|
|
11
|
+
* editor — user input area
|
|
12
|
+
*/
|
|
13
|
+
import { execSync } from 'node:child_process';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import http from 'node:http';
|
|
18
|
+
import { CombinedAutocompleteProvider, Container, Key, Loader, matchesKey, ProcessTerminal, Text, TUI, } from '@earendil-works/pi-tui';
|
|
19
|
+
import { StatsManager } from '../memory/stats.js';
|
|
20
|
+
import { ConfigManager } from '../setup/config.js';
|
|
21
|
+
import { getModelContextWindow } from '../setup/model-defaults.js';
|
|
22
|
+
import { RuntimeConfigCenter } from '../runtime/config-center.js';
|
|
23
|
+
import { ProviderManager } from '../provider/manager.js';
|
|
24
|
+
import { LifecycleSupervisor } from '../lifecycle/index.js';
|
|
25
|
+
import { createAgent } from './factory.js';
|
|
26
|
+
import { getBootstrapStatus, DEFAULT_PERSONA_DIR } from '../setup/persona-bootstrap.js';
|
|
27
|
+
import { createLogger } from '../logging/logger.js';
|
|
28
|
+
import { TuiChannel } from '../channels/builtin/tui-channel.js';
|
|
29
|
+
import { ChannelManager } from '../channels/manager.js';
|
|
30
|
+
import { MessageQueue, QueueMessageMode } from '../channels/index.js';
|
|
31
|
+
import { registerConfigChannels, getChannelPlugins } from '../channels/auto-detect.js';
|
|
32
|
+
import { filterCommands, getCommandsByCategory, getCategoryLabel, } from '../ui/slash-commands.js';
|
|
33
|
+
import { SlashSubPanel } from '../ui/slash-panel.js';
|
|
34
|
+
import { CommandRegistry } from '../ui/command-registry.js';
|
|
35
|
+
import { ChatLog } from '../ui/chat-log.js';
|
|
36
|
+
import { CustomEditor } from '../ui/pi-tui-editor.js';
|
|
37
|
+
import { theme, editorTheme } from '../ui/theme.js';
|
|
38
|
+
import { readRecentEvents } from '../event-store.js';
|
|
39
|
+
import { LocalModelModule } from '../local-model/index.js';
|
|
40
|
+
import { setAskUserHandler } from '../tools/ask-user.js';
|
|
41
|
+
const logger = createLogger('tui');
|
|
42
|
+
// ─── Constants ────────────────────────────────────────────────────────────
|
|
43
|
+
const BOX_H = '\u2500'; // ─
|
|
44
|
+
// ─── Status Bar Formatters ────────────────────────────────────────────────
|
|
45
|
+
function visualWidth(s) {
|
|
46
|
+
let w = 0;
|
|
47
|
+
for (const ch of s) {
|
|
48
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
49
|
+
// CJK, fullwidth forms, emoji: count as 2
|
|
50
|
+
w += (cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
|
51
|
+
(cp >= 0x2e80 && cp <= 0xa4cf) || // CJK Radicals, Kangxi, Ideographs
|
|
52
|
+
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul Syllables
|
|
53
|
+
(cp >= 0xf900 && cp <= 0xfaff) || // CJK Compatibility Ideographs
|
|
54
|
+
(cp >= 0xfe10 && cp <= 0xfe19) || // Vertical forms
|
|
55
|
+
(cp >= 0xfe30 && cp <= 0xfe6f) || // CJK Compatibility Forms
|
|
56
|
+
(cp >= 0xff00 && cp <= 0xff60) || // Fullwidth Forms
|
|
57
|
+
(cp >= 0xffe0 && cp <= 0xffe6) || // Fullwidth Signs
|
|
58
|
+
(cp >= 0x1f300 && cp <= 0x1f9ff) || // Emoji & Symbols
|
|
59
|
+
(cp >= 0x20000 && cp <= 0x2fa1f) // CJK Extension
|
|
60
|
+
? 2 : 1;
|
|
61
|
+
}
|
|
62
|
+
return w;
|
|
63
|
+
}
|
|
64
|
+
function truncateByVisualWidth(s, maxVisualWidth) {
|
|
65
|
+
if (visualWidth(s) <= maxVisualWidth)
|
|
66
|
+
return s;
|
|
67
|
+
let w = 0;
|
|
68
|
+
let result = '';
|
|
69
|
+
for (const ch of s) {
|
|
70
|
+
const cw = visualWidth(ch);
|
|
71
|
+
if (w + cw + 3 > maxVisualWidth) {
|
|
72
|
+
result += '...';
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
w += cw;
|
|
76
|
+
result += ch;
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
function formatStatusBar(info, modelName, providerInfo, modeLabel) {
|
|
81
|
+
const planInfo = info.planStepsTotal !== undefined
|
|
82
|
+
? `Plan: ${info.planStepsDone ?? 0}/${info.planStepsTotal}`
|
|
83
|
+
: '';
|
|
84
|
+
const modelDisplay = truncateByVisualWidth(modelName, 28);
|
|
85
|
+
const providerPart = providerInfo
|
|
86
|
+
? theme.dim(' | ') +
|
|
87
|
+
'Provider: ' +
|
|
88
|
+
(providerInfo.isLocal ? theme.success : theme.accent)(providerInfo.providerLabel)
|
|
89
|
+
: '';
|
|
90
|
+
const parts = [
|
|
91
|
+
theme.fg(' Hyacinth'),
|
|
92
|
+
theme.dim(' \u00b7 '),
|
|
93
|
+
theme.accent(modelDisplay),
|
|
94
|
+
theme.dim(' | '),
|
|
95
|
+
`Turns: ${theme.fg(String(info.turnCount))}/${theme.fg(String(info.maxTurns))}`,
|
|
96
|
+
providerPart,
|
|
97
|
+
];
|
|
98
|
+
if (planInfo) {
|
|
99
|
+
parts.push(theme.dim(' | '), planInfo);
|
|
100
|
+
}
|
|
101
|
+
if (modeLabel) {
|
|
102
|
+
parts.push(theme.dim(' | '), theme.accent(modeLabel));
|
|
103
|
+
}
|
|
104
|
+
return parts.join('');
|
|
105
|
+
}
|
|
106
|
+
function formatContextBar(tokensUsed, maxTokens, width = 40) {
|
|
107
|
+
const ratio = Math.min(tokensUsed / maxTokens, 1);
|
|
108
|
+
const filledW = Math.floor(ratio * width);
|
|
109
|
+
const emptyW = width - filledW;
|
|
110
|
+
const bar = '\u2588'.repeat(filledW) + '\u2591'.repeat(emptyW);
|
|
111
|
+
let colorFn;
|
|
112
|
+
if (ratio < 0.5)
|
|
113
|
+
colorFn = theme.success;
|
|
114
|
+
else if (ratio < 0.8)
|
|
115
|
+
colorFn = theme.warning;
|
|
116
|
+
else
|
|
117
|
+
colorFn = theme.error;
|
|
118
|
+
const usedK = (tokensUsed / 1000).toFixed(0) + 'K';
|
|
119
|
+
const maxK = (maxTokens / 1000).toFixed(0) + 'K';
|
|
120
|
+
const pct = (ratio * 100).toFixed(0) + '%';
|
|
121
|
+
return theme.dim('Context: ') + colorFn(bar) + ' ' + theme.fg(pct) + theme.dim(` (${usedK} / ${maxK} tokens)`);
|
|
122
|
+
}
|
|
123
|
+
// ─── Event Replay ─────────────────────────────────────────────────────────
|
|
124
|
+
/**
|
|
125
|
+
* Replay recent events from the previous session into the chat log.
|
|
126
|
+
* Shows the last conversation on TUI re-entry.
|
|
127
|
+
*/
|
|
128
|
+
function replayEvents(chatLog, sessionDir) {
|
|
129
|
+
readRecentEvents(sessionDir, 50).then(events => {
|
|
130
|
+
if (events.length === 0)
|
|
131
|
+
return;
|
|
132
|
+
chatLog.addSystem('── Previous Session ──');
|
|
133
|
+
let pendingText = '';
|
|
134
|
+
let pendingThinking = '';
|
|
135
|
+
let userMsgSeen = false;
|
|
136
|
+
for (const event of events) {
|
|
137
|
+
switch (event.type) {
|
|
138
|
+
case 'user_input':
|
|
139
|
+
// Flush any pending thinking + text before showing user message
|
|
140
|
+
if (pendingThinking) {
|
|
141
|
+
pendingThinking = '';
|
|
142
|
+
}
|
|
143
|
+
if (pendingText) {
|
|
144
|
+
chatLog.addSystem(pendingText);
|
|
145
|
+
pendingText = '';
|
|
146
|
+
}
|
|
147
|
+
chatLog.addUser(event.content || '');
|
|
148
|
+
userMsgSeen = true;
|
|
149
|
+
break;
|
|
150
|
+
case 'text':
|
|
151
|
+
// Accumulate text chunks into one message
|
|
152
|
+
pendingText += event.content || '';
|
|
153
|
+
break;
|
|
154
|
+
case 'thinking':
|
|
155
|
+
pendingThinking += event.content || '';
|
|
156
|
+
break;
|
|
157
|
+
case 'tool_call':
|
|
158
|
+
// Flush pending thinking + text before tool call
|
|
159
|
+
if (pendingThinking) {
|
|
160
|
+
pendingThinking = '';
|
|
161
|
+
}
|
|
162
|
+
if (pendingText) {
|
|
163
|
+
chatLog.addSystem(pendingText);
|
|
164
|
+
pendingText = '';
|
|
165
|
+
}
|
|
166
|
+
chatLog.startTool(event.id || 'replay', event.name || 'unknown', JSON.stringify(event.input || {}).slice(0, 100));
|
|
167
|
+
break;
|
|
168
|
+
case 'tool_result':
|
|
169
|
+
chatLog.updateToolResult(event.tool_use_id || 'replay', (event.content || '').slice(0, 500), { isError: event.name === 'error' });
|
|
170
|
+
break;
|
|
171
|
+
case 'error':
|
|
172
|
+
// 跳过回放中的原始错误日志(避免旧 session 的 API 错误 JSON 污染 UI)
|
|
173
|
+
break;
|
|
174
|
+
case 'stop':
|
|
175
|
+
// Flush remaining thinking + text
|
|
176
|
+
if (pendingThinking) {
|
|
177
|
+
pendingThinking = '';
|
|
178
|
+
}
|
|
179
|
+
if (pendingText) {
|
|
180
|
+
chatLog.addSystem(pendingText);
|
|
181
|
+
pendingText = '';
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
default:
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Flush any remaining thinking + text
|
|
189
|
+
if (pendingThinking) {
|
|
190
|
+
pendingThinking = '';
|
|
191
|
+
}
|
|
192
|
+
if (pendingText) {
|
|
193
|
+
chatLog.addSystem(pendingText);
|
|
194
|
+
}
|
|
195
|
+
}).catch(() => {
|
|
196
|
+
// Silently ignore replay errors — don't block TUI startup
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// ─── Main TUI ─────────────────────────────────────────────────────────────
|
|
200
|
+
export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxContext, maxMessages = 10000, skipSetup, personaDir = DEFAULT_PERSONA_DIR, bootstrapStatus, localModelProvider, continuationMessage) {
|
|
201
|
+
// ── Force UTF-8 console encoding on Windows ──
|
|
202
|
+
if (process.platform === 'win32') {
|
|
203
|
+
try {
|
|
204
|
+
execSync('chcp 65001 > nul', { stdio: 'ignore' });
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// non-fatal: terminal may still work with limited character support
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// ── Redirect stderr to file ──
|
|
211
|
+
const logDir = path.join(os.homedir(), '.agent');
|
|
212
|
+
await fs.promises.mkdir(logDir, { recursive: true });
|
|
213
|
+
const logStream = fs.createWriteStream(path.join(logDir, 'tui.log'), { flags: 'a' });
|
|
214
|
+
const origStderrWrite = process.stderr.write.bind(process.stderr);
|
|
215
|
+
process.stderr.write = (chunk, ...args) => {
|
|
216
|
+
logStream.write(typeof chunk === 'string' ? chunk : String(chunk));
|
|
217
|
+
const cb = args[args.length - 1];
|
|
218
|
+
if (typeof cb === 'function')
|
|
219
|
+
cb();
|
|
220
|
+
return true;
|
|
221
|
+
};
|
|
222
|
+
// ── Provider ──
|
|
223
|
+
const configManager = new ConfigManager(process.cwd());
|
|
224
|
+
await configManager.loadEnvKeys();
|
|
225
|
+
let activeProvider = provider;
|
|
226
|
+
if (!provider.getModel() || provider.getModel() === '') {
|
|
227
|
+
try {
|
|
228
|
+
const manager = await ProviderManager.createFromConfigFile(undefined, process.cwd());
|
|
229
|
+
activeProvider = manager.getProvider();
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// fallback
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
let providerTypeStart = activeProvider.getProviderType();
|
|
236
|
+
let modelName = activeProvider.getModel();
|
|
237
|
+
// ── Last known TurnInfo (updated each turn, used by /provider handler) ──
|
|
238
|
+
let lastTurnCount = 0;
|
|
239
|
+
let lastTokensUsed = 0;
|
|
240
|
+
// ── Lifecycle ──
|
|
241
|
+
const supervisor = new LifecycleSupervisor();
|
|
242
|
+
supervisor.installSignalHandlers();
|
|
243
|
+
// ── Local Model ──
|
|
244
|
+
const localModel = LocalModelModule.getInstance();
|
|
245
|
+
localModel.initialize(process.cwd());
|
|
246
|
+
let slashSubPanelActive = false; // 二级菜单激活时跳过主监听器的上下键处理
|
|
247
|
+
// ── TUI — pi-tui setup ──
|
|
248
|
+
const tui = new TUI(new ProcessTerminal());
|
|
249
|
+
const root = new Container();
|
|
250
|
+
// Header area: contains Text only (no Loader)
|
|
251
|
+
const headerContainer = new Container();
|
|
252
|
+
let headerText = new Text('', 0, 0);
|
|
253
|
+
headerContainer.addChild(headerText);
|
|
254
|
+
const contextBarText = new Text('', 0, 0);
|
|
255
|
+
const chatLog = new ChatLog();
|
|
256
|
+
const footerText = new Text('', 0, 0);
|
|
257
|
+
const editor = new CustomEditor(tui, editorTheme);
|
|
258
|
+
// Permission selection bar — shown between chat and editor during tool permission prompts
|
|
259
|
+
const permissionBar = new Text('', 0, 0);
|
|
260
|
+
const permissionQueue = [];
|
|
261
|
+
let permissionSelection = 0; // 0=Yes, 1=AOR, 2=Always, 3=No
|
|
262
|
+
let askUserState = null;
|
|
263
|
+
const askUserBar = new Text('', 0, 0);
|
|
264
|
+
const askUserContent = new Text('', 0, 0);
|
|
265
|
+
root.addChild(headerContainer);
|
|
266
|
+
root.addChild(chatLog);
|
|
267
|
+
root.addChild(contextBarText);
|
|
268
|
+
// Thinking indicator — below chatLog, above editor (prevents content jitter)
|
|
269
|
+
const thinkingBar = new Container();
|
|
270
|
+
let thinkingLoader = null;
|
|
271
|
+
root.addChild(thinkingBar);
|
|
272
|
+
root.addChild(footerText);
|
|
273
|
+
root.addChild(permissionBar);
|
|
274
|
+
root.addChild(askUserContent);
|
|
275
|
+
root.addChild(askUserBar);
|
|
276
|
+
root.addChild(editor);
|
|
277
|
+
tui.addChild(root);
|
|
278
|
+
tui.setFocus(editor);
|
|
279
|
+
// ── Header helpers — header always shows text (never Loader) ──
|
|
280
|
+
function updateHeaderText(text) {
|
|
281
|
+
headerText.setText(text);
|
|
282
|
+
}
|
|
283
|
+
// ── Thinking indicator helpers — Loader between chatLog and editor ──
|
|
284
|
+
function showThinkingIndicator(message) {
|
|
285
|
+
if (thinkingLoader) {
|
|
286
|
+
thinkingLoader.setMessage(message);
|
|
287
|
+
return thinkingLoader;
|
|
288
|
+
}
|
|
289
|
+
const loader = new Loader(tui, (spinner) => theme.accent(spinner), (text) => theme.accent(text), message);
|
|
290
|
+
thinkingBar.addChild(loader);
|
|
291
|
+
thinkingLoader = loader;
|
|
292
|
+
return loader;
|
|
293
|
+
}
|
|
294
|
+
function hideThinkingIndicator() {
|
|
295
|
+
if (thinkingLoader) {
|
|
296
|
+
thinkingLoader.stop();
|
|
297
|
+
thinkingBar.removeChild(thinkingLoader);
|
|
298
|
+
thinkingLoader = null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// ── State for refreshStatus ──
|
|
302
|
+
let prevCompressCount = 0;
|
|
303
|
+
let compactionMessage = null;
|
|
304
|
+
let compressionLoader = null;
|
|
305
|
+
let compressionTimeout = null;
|
|
306
|
+
let prevProviderLabel = null;
|
|
307
|
+
let prevProviderIsLocal = false;
|
|
308
|
+
let fallbackMessage = null;
|
|
309
|
+
let isThinking = false;
|
|
310
|
+
function refreshStatus(info) {
|
|
311
|
+
// ── Track last known values for /provider handler ──
|
|
312
|
+
lastTurnCount = info.turnCount;
|
|
313
|
+
lastTokensUsed = info.tokensUsed;
|
|
314
|
+
// ── Read live config (overrides startup defaults) ──
|
|
315
|
+
const liveCfg = RuntimeConfigCenter.getInstance();
|
|
316
|
+
const liveMaxContext = liveCfg.get('session.maxContext');
|
|
317
|
+
const liveMaxTurns = liveCfg.get('session.maxTurns') || maxTurns;
|
|
318
|
+
const activeInfo = {
|
|
319
|
+
...info,
|
|
320
|
+
maxTurns: liveMaxTurns,
|
|
321
|
+
};
|
|
322
|
+
// Compaction detection
|
|
323
|
+
if (info.compressCount > prevCompressCount) {
|
|
324
|
+
prevCompressCount = info.compressCount;
|
|
325
|
+
compactionMessage = '\u27f3 Compacting...';
|
|
326
|
+
setTimeout(() => {
|
|
327
|
+
compactionMessage = '\u2713 Compacted';
|
|
328
|
+
tui.requestRender();
|
|
329
|
+
setTimeout(() => {
|
|
330
|
+
compactionMessage = null;
|
|
331
|
+
tui.requestRender();
|
|
332
|
+
}, 2000);
|
|
333
|
+
}, 1000);
|
|
334
|
+
}
|
|
335
|
+
// Provider fallback detection \u2014 event-driven from onFallback callback
|
|
336
|
+
const providerInfo = loop.getProviderRoutingInfo();
|
|
337
|
+
if (providerInfo) {
|
|
338
|
+
if (prevProviderLabel !== null && providerInfo.providerLabel !== prevProviderLabel) {
|
|
339
|
+
// Provider changed via fallback \u2014 one-time notification already sent by onFallback
|
|
340
|
+
fallbackMessage = `\u26a0 Fallback: ${providerInfo.providerLabel}`;
|
|
341
|
+
setTimeout(() => { fallbackMessage = null; tui.requestRender(); }, 5000);
|
|
342
|
+
}
|
|
343
|
+
prevProviderLabel = providerInfo.providerLabel;
|
|
344
|
+
prevProviderIsLocal = providerInfo.isLocal;
|
|
345
|
+
}
|
|
346
|
+
const ap = loop.getActiveProvider();
|
|
347
|
+
const currentModel = ap.getModel();
|
|
348
|
+
const currentProviderType = ap.getProviderType();
|
|
349
|
+
const currentProviderInfo = providerInfo ?? { providerLabel: currentProviderType, isLocal: false, mode: 'auto' };
|
|
350
|
+
// Build mode label for header display
|
|
351
|
+
let modeHeaderLabel = null;
|
|
352
|
+
let statusContent = formatStatusBar(activeInfo, currentModel, currentProviderInfo, modeHeaderLabel);
|
|
353
|
+
// Compaction message
|
|
354
|
+
if (compactionMessage) {
|
|
355
|
+
statusContent += ' ' + theme.warning(compactionMessage);
|
|
356
|
+
}
|
|
357
|
+
// Fallback message
|
|
358
|
+
if (fallbackMessage) {
|
|
359
|
+
statusContent += ' ' + theme.warning(fallbackMessage);
|
|
360
|
+
}
|
|
361
|
+
// Update header (always Text)
|
|
362
|
+
if (isThinking && thinkingLoader) {
|
|
363
|
+
thinkingLoader.setMessage(statusContent);
|
|
364
|
+
}
|
|
365
|
+
else if (!isThinking) {
|
|
366
|
+
updateHeaderText(statusContent);
|
|
367
|
+
}
|
|
368
|
+
let ctxBar = formatContextBar(info.tokensUsed, liveMaxContext);
|
|
369
|
+
// 优先使用最新轮次的缓存命中率,否则从累计值计算
|
|
370
|
+
const hitRate = info.cacheHitRate != null
|
|
371
|
+
? info.cacheHitRate.toFixed(1)
|
|
372
|
+
: (info.cacheHitTokens != null && info.cacheMissTokens != null)
|
|
373
|
+
? (() => { const t = info.cacheHitTokens + info.cacheMissTokens; return t > 0 ? (info.cacheHitTokens / t * 100).toFixed(1) : null; })()
|
|
374
|
+
: null;
|
|
375
|
+
if (hitRate != null) {
|
|
376
|
+
ctxBar += theme.dim(` | Cache: ${hitRate}%`);
|
|
377
|
+
if (info.cacheHistory && info.cacheHistory.length > 1) {
|
|
378
|
+
ctxBar += theme.dim(` (${info.cacheHistory.length}t)`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Background process count
|
|
382
|
+
if (backgroundRegistry) {
|
|
383
|
+
const bgProcs = backgroundRegistry.list();
|
|
384
|
+
if (bgProcs.length > 0) {
|
|
385
|
+
const running = bgProcs.filter((p) => p.status === 'running').length;
|
|
386
|
+
ctxBar += theme.dim(' | ') + theme.accent(`⚙ ${running} bg`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
contextBarText.setText(ctxBar);
|
|
390
|
+
tui.requestRender();
|
|
391
|
+
updateTokenEstimate(); // 同步刷新 footer(工作流状态等)
|
|
392
|
+
}
|
|
393
|
+
// ── Output handler ──
|
|
394
|
+
let currentTextLine = '';
|
|
395
|
+
let pendingThinking = '';
|
|
396
|
+
let toolCounterFallback = 0;
|
|
397
|
+
let showThinking = false;
|
|
398
|
+
function showPermissionDialog(toolName, inputStr) {
|
|
399
|
+
permissionSelection = 0;
|
|
400
|
+
chatLog.addSystem(theme.warning(`\u250c Permission Required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`));
|
|
401
|
+
chatLog.addSystem(theme.warning('\u2502 ') + theme.fg(`${toolName}(${inputStr})`) + theme.warning(' \u2502'));
|
|
402
|
+
chatLog.addSystem(theme.warning(`\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`));
|
|
403
|
+
updatePermissionBar();
|
|
404
|
+
tui.requestRender();
|
|
405
|
+
}
|
|
406
|
+
function resolvePermission(result) {
|
|
407
|
+
const req = permissionQueue.shift();
|
|
408
|
+
if (!req)
|
|
409
|
+
return;
|
|
410
|
+
permissionBar.setText('');
|
|
411
|
+
chatLog.addSystem(result === 'no' ? theme.error(' \u25c6 Denied')
|
|
412
|
+
: result === 'aor' ? theme.warning(' \u25c6 AOR \u2014 all restrictions lifted')
|
|
413
|
+
: result === 'always' ? theme.success(' \u25c6 Always allowed')
|
|
414
|
+
: theme.success(' \u25c6 Approved'));
|
|
415
|
+
req.resolve(result);
|
|
416
|
+
if (permissionQueue.length > 0) {
|
|
417
|
+
const next = permissionQueue[0];
|
|
418
|
+
showPermissionDialog(next.toolName, next.inputStr);
|
|
419
|
+
}
|
|
420
|
+
tui.requestRender();
|
|
421
|
+
}
|
|
422
|
+
function updatePermissionBar() {
|
|
423
|
+
const labels = ['Yes', 'AOR', 'Always', 'No'];
|
|
424
|
+
const shortcuts = ['Y', 'O', 'A', 'N'];
|
|
425
|
+
const parts = labels.map((l, i) => {
|
|
426
|
+
const prefix = i === permissionSelection ? '\u25b6 ' : ' ';
|
|
427
|
+
if (i === permissionSelection) {
|
|
428
|
+
return theme.fg(`[ ${prefix}${l} (${shortcuts[i]}) ]`);
|
|
429
|
+
}
|
|
430
|
+
return theme.dim(` ${prefix}${l} (${shortcuts[i]}) `);
|
|
431
|
+
});
|
|
432
|
+
permissionBar.setText(theme.warning('\u250c Permission Required \u2500 ') +
|
|
433
|
+
parts.join(theme.dim(' \u2502 ')) +
|
|
434
|
+
theme.warning(' \u2500\u2500 Use \u2190\u2192 to select, Enter to confirm'));
|
|
435
|
+
}
|
|
436
|
+
// \u2500\u2500 Ask User \u6e32\u67d3 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
437
|
+
function renderAskUserForm() {
|
|
438
|
+
if (!askUserState) {
|
|
439
|
+
askUserContent.setText('');
|
|
440
|
+
askUserBar.setText('');
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const { questions, selectedOptions, customTexts, activeQuestion } = askUserState;
|
|
444
|
+
const supplementIdx = questions.length;
|
|
445
|
+
const totalTabs = supplementIdx + 1;
|
|
446
|
+
// \u6784\u5efa\u6807\u7b7e\u884c
|
|
447
|
+
let tabLine = '';
|
|
448
|
+
for (let i = 0; i < questions.length; i++) {
|
|
449
|
+
const isActive = i === activeQuestion;
|
|
450
|
+
const header = questions[i].header || `\u95ee\u9898${i + 1}`;
|
|
451
|
+
tabLine += isActive ? ` ${theme.fg(`[${header}]`)} ` : ` ${theme.dim(header)} `;
|
|
452
|
+
}
|
|
453
|
+
tabLine += activeQuestion === supplementIdx
|
|
454
|
+
? ` ${theme.fg('[\u8865\u5145]')} `
|
|
455
|
+
: ` ${theme.dim('\u8865\u5145')} `;
|
|
456
|
+
// \u6784\u5efa\u5185\u5bb9
|
|
457
|
+
let content = theme.warning('\u250c Ask User \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500') + '\n';
|
|
458
|
+
content += tabLine + '\n';
|
|
459
|
+
content += theme.warning('\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500') + '\n';
|
|
460
|
+
if (activeQuestion < supplementIdx) {
|
|
461
|
+
// \u666e\u901a\u95ee\u9898
|
|
462
|
+
const q = questions[activeQuestion];
|
|
463
|
+
content += `${q.question}\n\n`;
|
|
464
|
+
const opts = q.options ?? [];
|
|
465
|
+
const sel = selectedOptions.get(activeQuestion) ?? new Set();
|
|
466
|
+
const isMulti = q.multiSelect ?? false;
|
|
467
|
+
for (let i = 0; i < opts.length; i++) {
|
|
468
|
+
const selected = sel.has(i);
|
|
469
|
+
const bullet = isMulti
|
|
470
|
+
? (selected ? theme.fg('\u25c9') : '\u25cb')
|
|
471
|
+
: (selected ? theme.fg('\u25cf') : '\u25cb');
|
|
472
|
+
const highlight = i === askUserState.activeOption;
|
|
473
|
+
content += (highlight ? theme.fg(` ${bullet} ${opts[i]}`) : theme.dim(` ${bullet} ${opts[i]}`)) + '\n';
|
|
474
|
+
}
|
|
475
|
+
// \u81ea\u5b9a\u4e49\u8f93\u5165
|
|
476
|
+
if (q.customInput ?? false) {
|
|
477
|
+
const custom = customTexts.get(activeQuestion) ?? '';
|
|
478
|
+
content += `\n${theme.dim('\u81ea\u5b9a\u4e49:')} ${custom}${theme.dim('\u258c')}\n`;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
// "\u8865\u5145" tab
|
|
483
|
+
const custom = customTexts.get(supplementIdx) ?? '';
|
|
484
|
+
content += `${theme.fg('\u8865\u5145\u8bf4\u660e\uff08\u81ea\u7531\u8f93\u5165\uff0c\u6309 Enter \u63d0\u4ea4\u5168\u90e8\uff09')}\n\n`;
|
|
485
|
+
content += `${custom}${theme.dim('\u258c')}\n`;
|
|
486
|
+
}
|
|
487
|
+
content += theme.warning('\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500');
|
|
488
|
+
askUserContent.setText(content);
|
|
489
|
+
// \u5bfc\u822a\u680f
|
|
490
|
+
const currentLabel = activeQuestion < supplementIdx
|
|
491
|
+
? (questions[activeQuestion].header || `\u95ee\u9898${activeQuestion + 1}`)
|
|
492
|
+
: '\u8865\u5145';
|
|
493
|
+
const nav = activeQuestion < supplementIdx
|
|
494
|
+
? theme.warning(`\u2190\u2192 \u5207\u6362 \u2191\u2193 \u9009\u9879 \u7a7a\u683c \u9009\u4e2d \u21b5 ${currentLabel === '\u8865\u5145' ? '\u63d0\u4ea4' : '\u786e\u8ba4'}`)
|
|
495
|
+
: theme.warning(`\u2190\u2192 \u5207\u6362 \u21b5 \u63d0\u4ea4\u5168\u90e8\u7b54\u6848`);
|
|
496
|
+
askUserBar.setText(nav);
|
|
497
|
+
}
|
|
498
|
+
function resolveAskUser() {
|
|
499
|
+
if (!askUserState)
|
|
500
|
+
return;
|
|
501
|
+
const { questions, selectedOptions, customTexts, resolve } = askUserState;
|
|
502
|
+
const suppIdx = questions.length;
|
|
503
|
+
const result = {};
|
|
504
|
+
for (let i = 0; i < questions.length; i++) {
|
|
505
|
+
const q = questions[i];
|
|
506
|
+
const answers = [];
|
|
507
|
+
// \u9009\u4e2d\u7684\u9009\u9879
|
|
508
|
+
const sel = selectedOptions.get(i) ?? new Set();
|
|
509
|
+
for (const idx of sel) {
|
|
510
|
+
if (q.options && q.options[idx]) {
|
|
511
|
+
answers.push(q.options[idx]);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// \u81ea\u5b9a\u4e49\u8f93\u5165
|
|
515
|
+
const custom = customTexts.get(i) ?? '';
|
|
516
|
+
if (custom.trim()) {
|
|
517
|
+
answers.push(custom.trim());
|
|
518
|
+
}
|
|
519
|
+
// \u7528\u95ee\u9898\u539f\u6587\u4f5c\u4e3a key\uff08\u957f\u4e0a\u4e0b\u6587\u91cc\u6bd4 "0" "1" \u66f4\u6709\u8bed\u4e49\uff09
|
|
520
|
+
if (answers.length > 0) {
|
|
521
|
+
result[q.question] = answers;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// "\u8865\u5145" \u8f93\u5165
|
|
525
|
+
const suppText = customTexts.get(suppIdx) ?? '';
|
|
526
|
+
if (suppText.trim()) {
|
|
527
|
+
result['\u8865\u5145\u8bf4\u660e'] = [suppText.trim()];
|
|
528
|
+
}
|
|
529
|
+
askUserContent.setText('');
|
|
530
|
+
askUserBar.setText('');
|
|
531
|
+
const answeredCount = Object.keys(result).length;
|
|
532
|
+
chatLog.addSystem(theme.success(`\u25c6 Answered ${answeredCount} question(s)`));
|
|
533
|
+
askUserState = null;
|
|
534
|
+
resolve(JSON.stringify(result, null, 2));
|
|
535
|
+
tui.requestRender();
|
|
536
|
+
}
|
|
537
|
+
const tuiHandler = {
|
|
538
|
+
onText(content) {
|
|
539
|
+
currentTextLine += content;
|
|
540
|
+
},
|
|
541
|
+
onThinking(content) {
|
|
542
|
+
pendingThinking += content;
|
|
543
|
+
},
|
|
544
|
+
onTurnStart() {
|
|
545
|
+
isThinking = true;
|
|
546
|
+
pendingThinking = '';
|
|
547
|
+
const label = loop.pendingTaskName
|
|
548
|
+
? `⏰ ${loop.pendingTaskName}`
|
|
549
|
+
: 'Thinking...';
|
|
550
|
+
showThinkingIndicator(theme.accent(label));
|
|
551
|
+
},
|
|
552
|
+
onToolUse(name, inputSummary, toolId) {
|
|
553
|
+
if (pendingThinking.trim()) {
|
|
554
|
+
if (showThinking) {
|
|
555
|
+
chatLog.addSystem(theme.dim('\u{1F9E0} Thinking:\n') + theme.dim(pendingThinking.trim()));
|
|
556
|
+
}
|
|
557
|
+
pendingThinking = '';
|
|
558
|
+
}
|
|
559
|
+
if (currentTextLine.trim()) {
|
|
560
|
+
chatLog.finalizeAssistant(currentTextLine.trim());
|
|
561
|
+
currentTextLine = '';
|
|
562
|
+
}
|
|
563
|
+
const summary = inputSummary.length > 100 ? inputSummary.slice(0, 97) + '...' : inputSummary;
|
|
564
|
+
const id = toolId ?? `tool_${Date.now()}_${++toolCounterFallback}`;
|
|
565
|
+
chatLog.startTool(id, name, summary);
|
|
566
|
+
tui.requestRender();
|
|
567
|
+
},
|
|
568
|
+
onToolResult(content, isError, toolId) {
|
|
569
|
+
if (toolId) {
|
|
570
|
+
chatLog.updateToolResult(toolId, content, { isError });
|
|
571
|
+
}
|
|
572
|
+
tui.requestRender();
|
|
573
|
+
},
|
|
574
|
+
onDiff(toolId, filePath, diffLines) {
|
|
575
|
+
if (toolId) {
|
|
576
|
+
chatLog.showDiff(toolId, filePath, diffLines);
|
|
577
|
+
}
|
|
578
|
+
tui.requestRender();
|
|
579
|
+
},
|
|
580
|
+
onStatus(message, level) {
|
|
581
|
+
// 压缩指示器
|
|
582
|
+
if (message === 'compress-start') {
|
|
583
|
+
if (!compressionLoader) {
|
|
584
|
+
compressionLoader = new Loader(tui, (spinner) => theme.accent(spinner), (text) => theme.accent(text), 'Compressing...');
|
|
585
|
+
thinkingBar.addChild(compressionLoader);
|
|
586
|
+
}
|
|
587
|
+
// 超时保护:30 秒后自动清除 Loader
|
|
588
|
+
if (compressionTimeout)
|
|
589
|
+
clearTimeout(compressionTimeout);
|
|
590
|
+
compressionTimeout = setTimeout(() => {
|
|
591
|
+
if (compressionLoader) {
|
|
592
|
+
compressionLoader.stop();
|
|
593
|
+
thinkingBar.removeChild(compressionLoader);
|
|
594
|
+
compressionLoader = null;
|
|
595
|
+
}
|
|
596
|
+
compressionTimeout = null;
|
|
597
|
+
}, 30000);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (message === 'compress-end') {
|
|
601
|
+
if (compressionTimeout) {
|
|
602
|
+
clearTimeout(compressionTimeout);
|
|
603
|
+
compressionTimeout = null;
|
|
604
|
+
}
|
|
605
|
+
if (compressionLoader) {
|
|
606
|
+
compressionLoader.stop();
|
|
607
|
+
thinkingBar.removeChild(compressionLoader);
|
|
608
|
+
compressionLoader = null;
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (message.startsWith('compress-result:')) {
|
|
613
|
+
const parts = message.split(':');
|
|
614
|
+
const pre = parseInt(parts[1], 10);
|
|
615
|
+
const post = parseInt(parts[2], 10);
|
|
616
|
+
if (!isNaN(pre) && !isNaN(post)) {
|
|
617
|
+
const ratio = Math.round((1 - post / pre) * 100);
|
|
618
|
+
chatLog.addSystem(theme.success('Compressed: ') +
|
|
619
|
+
theme.dim(`${pre.toLocaleString()} → ${post.toLocaleString()} tokens`) +
|
|
620
|
+
theme.fg(` (${ratio}% reduced)`));
|
|
621
|
+
// 用压缩后的 token 数刷新 context bar
|
|
622
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, post));
|
|
623
|
+
}
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (level === 'error') {
|
|
627
|
+
// 原始 API 错误细节走日志,TUI 只显示简洁消息
|
|
628
|
+
if (message.length > 150 || message.includes('{') || message.includes('\n')) {
|
|
629
|
+
process.stderr.write(`[tui:error] ${message}\n`);
|
|
630
|
+
chatLog.addSystem(theme.errorBright('[Error] ') + theme.dim('Provider request failed, auto-switching...'));
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
chatLog.addSystem(theme.errorBright('[Error] ') + theme.error(message));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
else if (level === 'warn') {
|
|
637
|
+
chatLog.addSystem(theme.warning(message));
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
chatLog.addSystem(theme.fg(message));
|
|
641
|
+
// 如果消息是 provider 切换,同步刷新 UI
|
|
642
|
+
if (message.startsWith('Provider switched to ')) {
|
|
643
|
+
modelName = loop.getActiveProvider().getModel();
|
|
644
|
+
providerTypeStart = loop.getActiveProvider().getProviderType();
|
|
645
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
tui.requestRender();
|
|
649
|
+
},
|
|
650
|
+
onFlush() {
|
|
651
|
+
hideThinkingIndicator();
|
|
652
|
+
isThinking = false;
|
|
653
|
+
// 陪伴模式 session 切换 → 清空显示并重放新 session 历史,同步 sessionDir
|
|
654
|
+
if (loop._sessionSwitched) {
|
|
655
|
+
sessionDir = loop._sessionSwitched;
|
|
656
|
+
chatLog.clearAll();
|
|
657
|
+
replayEvents(chatLog, loop._sessionSwitched);
|
|
658
|
+
loop._sessionSwitched = undefined;
|
|
659
|
+
}
|
|
660
|
+
updateHeaderText(modelName);
|
|
661
|
+
const providerInfo = loop.getProviderRoutingInfo();
|
|
662
|
+
if (pendingThinking.trim()) {
|
|
663
|
+
if (showThinking) {
|
|
664
|
+
chatLog.addSystem(theme.dim('\u{1F9E0} Thinking:\n') + theme.dim(pendingThinking.trim()));
|
|
665
|
+
}
|
|
666
|
+
// thinking-only 模型兜底:thinking 有内容但 text 为空时,将 thinking 作为回复显示
|
|
667
|
+
if (!currentTextLine.trim()) {
|
|
668
|
+
currentTextLine = pendingThinking.trim();
|
|
669
|
+
}
|
|
670
|
+
pendingThinking = '';
|
|
671
|
+
}
|
|
672
|
+
if (currentTextLine.trim()) {
|
|
673
|
+
chatLog.finalizeAssistant(currentTextLine.trim());
|
|
674
|
+
currentTextLine = '';
|
|
675
|
+
}
|
|
676
|
+
tui.requestRender();
|
|
677
|
+
},
|
|
678
|
+
onPermissionRequest(toolName, input) {
|
|
679
|
+
const inputStr = Object.entries(input)
|
|
680
|
+
.map(([k, v]) => `${k}=${String(v).substring(0, 60)}`)
|
|
681
|
+
.join(', ');
|
|
682
|
+
return new Promise((resolve) => {
|
|
683
|
+
const isFirst = permissionQueue.length === 0;
|
|
684
|
+
permissionQueue.push({ resolve, toolName, inputStr });
|
|
685
|
+
if (isFirst) {
|
|
686
|
+
showPermissionDialog(toolName, inputStr);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
},
|
|
690
|
+
onInterrupt() {
|
|
691
|
+
hideThinkingIndicator();
|
|
692
|
+
isThinking = false;
|
|
693
|
+
pendingThinking = '';
|
|
694
|
+
currentTextLine = '';
|
|
695
|
+
},
|
|
696
|
+
onAskUser(questions) {
|
|
697
|
+
return new Promise((resolve) => {
|
|
698
|
+
const selectedOptions = new Map();
|
|
699
|
+
const customTexts = new Map();
|
|
700
|
+
for (let i = 0; i < questions.length; i++) {
|
|
701
|
+
selectedOptions.set(i, new Set());
|
|
702
|
+
customTexts.set(i, '');
|
|
703
|
+
}
|
|
704
|
+
// "补充" tab custom text
|
|
705
|
+
customTexts.set(questions.length, '');
|
|
706
|
+
askUserState = {
|
|
707
|
+
questions,
|
|
708
|
+
selectedOptions,
|
|
709
|
+
customTexts,
|
|
710
|
+
activeQuestion: 0,
|
|
711
|
+
activeOption: 0,
|
|
712
|
+
resolve,
|
|
713
|
+
};
|
|
714
|
+
renderAskUserForm();
|
|
715
|
+
tui.requestRender();
|
|
716
|
+
});
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
// ── Bootstrap detection ──
|
|
720
|
+
const resolvedBootstrapStatus = bootstrapStatus ?? (await getBootstrapStatus(personaDir));
|
|
721
|
+
// ── Local Model (llama.cpp) ──
|
|
722
|
+
try {
|
|
723
|
+
const loadedModels = await supervisor.loadAndStartModels(process.cwd());
|
|
724
|
+
if (loadedModels.length > 0) {
|
|
725
|
+
chatLog.addSystem(theme.success('\uD83D\uDDA5 Local Model Server'));
|
|
726
|
+
for (const m of loadedModels) {
|
|
727
|
+
chatLog.addSystem(theme.success(' \u2022 ') + theme.accent(m.name) + theme.dim(' \u2192 ') + theme.accent(m.baseUrl));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
733
|
+
chatLog.addSystem(theme.warning('\u26a0 Local model server not available: ' + msg));
|
|
734
|
+
}
|
|
735
|
+
tui.requestRender();
|
|
736
|
+
// ── 远程模式下跳过 createAgent,这些组件需手动初始化 ──
|
|
737
|
+
try {
|
|
738
|
+
CommandRegistry.getInstance(process.cwd());
|
|
739
|
+
}
|
|
740
|
+
catch { /* already initialized */ }
|
|
741
|
+
try {
|
|
742
|
+
const { RuntimeConfigCenter } = await import('../runtime/config-center.js');
|
|
743
|
+
const { getDefaultConfig } = await import('../runtime/defaults.js');
|
|
744
|
+
const configCenter = RuntimeConfigCenter.getInstance();
|
|
745
|
+
// 尝试读取配置——如果未初始化会抛异常
|
|
746
|
+
configCenter.get('session.maxTurns');
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
// 初始化 RuntimeConfigCenter
|
|
750
|
+
const { ConfigManager } = await import('../setup/config.js');
|
|
751
|
+
const { getDefaultConfig } = await import('../runtime/defaults.js');
|
|
752
|
+
const { RuntimeConfigCenter: RCC } = await import('../runtime/config-center.js');
|
|
753
|
+
const cm = new ConfigManager(process.cwd());
|
|
754
|
+
await cm.loadEnvKeys();
|
|
755
|
+
const cfg = await cm.load();
|
|
756
|
+
RCC.getInstance().initialize(getDefaultConfig(), cm);
|
|
757
|
+
RCC.getInstance().merge(cfg);
|
|
758
|
+
}
|
|
759
|
+
// ── 检测统一后端是否已运行 ──
|
|
760
|
+
let remoteWs = null;
|
|
761
|
+
try {
|
|
762
|
+
const healthy = await new Promise((resolve) => {
|
|
763
|
+
const req = http.get('http://127.0.0.1:3000/api/health', (res) => {
|
|
764
|
+
let d = '';
|
|
765
|
+
res.on('data', (c) => d += c);
|
|
766
|
+
res.on('end', () => resolve(d.includes('"ok"')));
|
|
767
|
+
});
|
|
768
|
+
req.on('error', () => resolve(false));
|
|
769
|
+
req.setTimeout(1500, () => { req.destroy(); resolve(false); });
|
|
770
|
+
});
|
|
771
|
+
if (healthy) {
|
|
772
|
+
const { default: WebSocket } = await import('ws');
|
|
773
|
+
remoteWs = await new Promise((resolve) => {
|
|
774
|
+
const ws = new WebSocket('ws://127.0.0.1:3000/tui');
|
|
775
|
+
ws.on('open', () => resolve(ws));
|
|
776
|
+
ws.on('error', () => resolve(null));
|
|
777
|
+
setTimeout(() => resolve(null), 3000);
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
catch { /* fall through to standalone */ }
|
|
782
|
+
// ── Agent + Session(远程模式下跳过,使用 WebSocket 连接) ──
|
|
783
|
+
let agent;
|
|
784
|
+
let sessionDir;
|
|
785
|
+
if (!remoteWs) {
|
|
786
|
+
agent = await createAgent({
|
|
787
|
+
cwd: process.cwd(),
|
|
788
|
+
provider: activeProvider,
|
|
789
|
+
maxTurns,
|
|
790
|
+
maxContext,
|
|
791
|
+
outputHandler: tuiHandler,
|
|
792
|
+
sessionId,
|
|
793
|
+
shouldContinue,
|
|
794
|
+
maxMessages,
|
|
795
|
+
personaDir,
|
|
796
|
+
bootstrapStatus: resolvedBootstrapStatus,
|
|
797
|
+
localModelProvider,
|
|
798
|
+
channel: 'tui',
|
|
799
|
+
});
|
|
800
|
+
sessionDir = agent.sessionDir;
|
|
801
|
+
// Wire ask_user tool to TUI handler
|
|
802
|
+
if (tuiHandler.onAskUser) {
|
|
803
|
+
setAskUserHandler((questions) => tuiHandler.onAskUser(questions));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
agent = undefined;
|
|
808
|
+
sessionDir = '';
|
|
809
|
+
}
|
|
810
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
811
|
+
const loop = agent?.loop ?? {
|
|
812
|
+
getActiveProvider: () => activeProvider,
|
|
813
|
+
getProviderRoutingInfo: () => null,
|
|
814
|
+
getTurnInfo: (_tc, _tu) => ({ turnCount: lastTurnCount, maxTurns, tokensUsed: lastTokensUsed, maxContextTokens: maxContext, sessionId: '', compressCount: 0 }),
|
|
815
|
+
setLifecycleSupervisor: () => { },
|
|
816
|
+
run: async () => { },
|
|
817
|
+
switchSession: async () => { },
|
|
818
|
+
switchProvider: async () => { },
|
|
819
|
+
getModelSources: () => null,
|
|
820
|
+
composeStrategy: null,
|
|
821
|
+
setScheduler: () => { },
|
|
822
|
+
notifyTaskFired: async () => { },
|
|
823
|
+
subscribeConfig: () => { },
|
|
824
|
+
isBootstrapPending: () => false,
|
|
825
|
+
shutdown: async () => { },
|
|
826
|
+
};
|
|
827
|
+
// 远程模式下 agent 为 undefined,这些变量仅用于本地 TUI 功能(斜杠命令等),用 any 避免 null 检查
|
|
828
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
829
|
+
const backgroundRegistry = agent?.backgroundRegistry;
|
|
830
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
831
|
+
const modelRouter = agent?.modelRouter;
|
|
832
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
833
|
+
const knowledgeBase = agent?.knowledgeBase;
|
|
834
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
835
|
+
const composeStrategy = agent?.composeStrategy;
|
|
836
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
837
|
+
const sessionManager = agent?.sessionManager;
|
|
838
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
839
|
+
const contextComposer = agent?.contextComposer;
|
|
840
|
+
let originalSessionDir = null; // /precise on 前保存的原始 session
|
|
841
|
+
let preciseModeActive = false;
|
|
842
|
+
// 检测启动时恢复的 session 类型(仅本地模式)
|
|
843
|
+
if (!remoteWs) {
|
|
844
|
+
const currentSessionId = path.basename(sessionDir);
|
|
845
|
+
const sessions = await sessionManager.list();
|
|
846
|
+
const resumedSession = sessions.find((s) => s.id === currentSessionId);
|
|
847
|
+
if (resumedSession?.type === 'precise') {
|
|
848
|
+
const { PreciseStrategy } = await import('../context/precision/index.js');
|
|
849
|
+
loop.composeStrategy = new PreciseStrategy(sessionDir);
|
|
850
|
+
preciseModeActive = true;
|
|
851
|
+
const normalSessions = sessions.filter((s) => (s.type ?? 'normal') === 'normal');
|
|
852
|
+
if (normalSessions.length > 0) {
|
|
853
|
+
originalSessionDir = sessionManager.getSessionDir(normalSessions[0].id);
|
|
854
|
+
}
|
|
855
|
+
chatLog.addSystem(theme.dim('精确模式 session 已恢复'));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// 注入 LifecycleSupervisor,实现运行时 provider 切换时自动管理本地模型进程
|
|
859
|
+
loop.setLifecycleSupervisor(supervisor);
|
|
860
|
+
const statsManager = new StatsManager();
|
|
861
|
+
// ── Channel bridge ──
|
|
862
|
+
const tuiChannel = new TuiChannel();
|
|
863
|
+
const channelManager = new ChannelManager();
|
|
864
|
+
channelManager.register(tuiChannel, { enabled: true });
|
|
865
|
+
// 自动检测配置驱动渠道(飞书等),有配置则自动注册
|
|
866
|
+
await registerConfigChannels(channelManager, process.cwd());
|
|
867
|
+
// 调用所有插件的 onGatewayInit 钩子(如飞书 SDK 日志拦截)
|
|
868
|
+
const cleanupFns = [];
|
|
869
|
+
for (const plugin of getChannelPlugins()) {
|
|
870
|
+
if (plugin.onGatewayInit) {
|
|
871
|
+
const cleanup = plugin.onGatewayInit();
|
|
872
|
+
if (cleanup)
|
|
873
|
+
cleanupFns.push(cleanup);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// 为所有渠道注入 TUI 同步回调(渠道自行决定是否启用 tuiSync)
|
|
877
|
+
for (const state of channelManager.getAll()) {
|
|
878
|
+
if (state.handler.id === 'tui')
|
|
879
|
+
continue;
|
|
880
|
+
state.config = {
|
|
881
|
+
...state.config,
|
|
882
|
+
onUserMessage: (label, content) => {
|
|
883
|
+
chatLog.addSystem(`💬 [${state.handler.name}:${label}] ${content}`);
|
|
884
|
+
tui.requestRender();
|
|
885
|
+
},
|
|
886
|
+
onAgentReply: (content) => {
|
|
887
|
+
chatLog.addSystem(`📤 [${state.handler.name}回复] ${content}`);
|
|
888
|
+
tui.requestRender();
|
|
889
|
+
},
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
tuiChannel.onReply = (content) => {
|
|
893
|
+
chatLog.addSystem(content);
|
|
894
|
+
tui.requestRender();
|
|
895
|
+
};
|
|
896
|
+
// 创建 AgentFactory(注入到渠道,渠道通过此接口创建 AgentLoop)
|
|
897
|
+
const agentFactory = {
|
|
898
|
+
createAgent: async (options) => {
|
|
899
|
+
return createAgent({
|
|
900
|
+
cwd: process.cwd(),
|
|
901
|
+
provider: activeProvider,
|
|
902
|
+
maxTurns: maxTurns ?? 20,
|
|
903
|
+
maxContext: maxContext ?? 40_000,
|
|
904
|
+
outputHandler: options.outputHandler,
|
|
905
|
+
sessionId: options.sessionId,
|
|
906
|
+
channel: options.channel ?? 'tui',
|
|
907
|
+
});
|
|
908
|
+
},
|
|
909
|
+
};
|
|
910
|
+
// 设置 TUI 渠道的消息处理回调
|
|
911
|
+
if (remoteWs) {
|
|
912
|
+
// ── 远程模式:通过 WebSocket 连接统一后端 ──
|
|
913
|
+
chatLog.addSystem(theme.success('🔗 Connected to running backend (port 3000)'));
|
|
914
|
+
remoteWs.on('message', (data) => {
|
|
915
|
+
try {
|
|
916
|
+
const msg = JSON.parse(data.toString());
|
|
917
|
+
switch (msg.type) {
|
|
918
|
+
case 'text':
|
|
919
|
+
tuiHandler.onText?.(msg.content);
|
|
920
|
+
break;
|
|
921
|
+
case 'thinking':
|
|
922
|
+
tuiHandler.onThinking?.(msg.content);
|
|
923
|
+
break;
|
|
924
|
+
case 'tool_use':
|
|
925
|
+
tuiHandler.onToolUse?.(msg.name, msg.inputSummary, msg.id);
|
|
926
|
+
break;
|
|
927
|
+
case 'tool_result':
|
|
928
|
+
tuiHandler.onToolResult?.(msg.content, msg.isError, msg.id);
|
|
929
|
+
break;
|
|
930
|
+
case 'diff':
|
|
931
|
+
tuiHandler.onDiff?.(msg.id, msg.filePath, msg.diffLines);
|
|
932
|
+
break;
|
|
933
|
+
case 'status':
|
|
934
|
+
tuiHandler.onStatus?.(msg.message, msg.level);
|
|
935
|
+
break;
|
|
936
|
+
case 'turn_start':
|
|
937
|
+
tuiHandler.onTurnStart?.();
|
|
938
|
+
break;
|
|
939
|
+
case 'flush':
|
|
940
|
+
tuiHandler.onFlush?.();
|
|
941
|
+
break;
|
|
942
|
+
case 'interrupt':
|
|
943
|
+
tuiHandler.onInterrupt?.();
|
|
944
|
+
break;
|
|
945
|
+
case 'turn_info': {
|
|
946
|
+
lastTurnCount = msg.turnCount ?? lastTurnCount;
|
|
947
|
+
lastTokensUsed = msg.tokensUsed ?? lastTokensUsed;
|
|
948
|
+
refreshStatus({ turnCount: lastTurnCount, maxTurns, tokensUsed: lastTokensUsed, maxContextTokens: maxContext, sessionId: '', compressCount: 0 });
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
case 'error':
|
|
952
|
+
chatLog.addSystem(theme.errorBright('[Error] ') + theme.error(msg.message));
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
catch { /* ignore */ }
|
|
957
|
+
});
|
|
958
|
+
remoteWs.on('close', () => {
|
|
959
|
+
chatLog.addSystem(theme.warning('⚠ Backend connection lost. Restart to reconnect.'));
|
|
960
|
+
});
|
|
961
|
+
tuiChannel.onHandleMessage = async (event) => {
|
|
962
|
+
if (remoteWs?.readyState === 1) {
|
|
963
|
+
remoteWs.send(JSON.stringify({ type: 'chat', content: event.content }));
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
tuiChannel.onHandleMessage = async (event) => {
|
|
969
|
+
await loop.run(event.content);
|
|
970
|
+
const stats = await statsManager.get(sessionDir);
|
|
971
|
+
const tc = stats.turn_count ?? 0;
|
|
972
|
+
refreshStatus(loop.getTurnInfo(tc, stats.current_context_tokens ?? 0));
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
// 启动所有渠道(每个渠道自行处理消息)
|
|
976
|
+
await channelManager.startAll(agentFactory);
|
|
977
|
+
// ── ASCII Art loader ──────────────────────────────────────────────
|
|
978
|
+
async function loadAsciiArt(maxWidth = 54) {
|
|
979
|
+
const asciiDir = path.join(os.homedir(), '.agent', 'ascii');
|
|
980
|
+
const lastFile = path.join(asciiDir, '_last.txt');
|
|
981
|
+
try {
|
|
982
|
+
if (!fs.existsSync(asciiDir)) {
|
|
983
|
+
fs.mkdirSync(asciiDir, { recursive: true });
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
const files = fs.readdirSync(asciiDir);
|
|
987
|
+
const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'];
|
|
988
|
+
const imageFiles = files.filter(f => imageExts.includes(path.extname(f).toLowerCase()));
|
|
989
|
+
if (imageFiles.length === 0)
|
|
990
|
+
return null;
|
|
991
|
+
let prevName = '';
|
|
992
|
+
try {
|
|
993
|
+
prevName = fs.readFileSync(lastFile, 'utf-8').trim();
|
|
994
|
+
}
|
|
995
|
+
catch { /* first */ }
|
|
996
|
+
const candidates = imageFiles.length > 1 ? imageFiles.filter(f => f !== prevName) : imageFiles;
|
|
997
|
+
const pool = candidates.length > 0 ? candidates : imageFiles;
|
|
998
|
+
const picked = pool[Math.floor(Math.random() * pool.length)];
|
|
999
|
+
try {
|
|
1000
|
+
fs.writeFileSync(lastFile, picked, 'utf-8');
|
|
1001
|
+
}
|
|
1002
|
+
catch { /* ignore */ }
|
|
1003
|
+
const imgPath = path.join(asciiDir, picked);
|
|
1004
|
+
const cachePath = path.join(asciiDir, picked + '.txt');
|
|
1005
|
+
try {
|
|
1006
|
+
const imgStat = fs.statSync(imgPath);
|
|
1007
|
+
const cacheStat = fs.statSync(cachePath);
|
|
1008
|
+
if (cacheStat.mtimeMs >= imgStat.mtimeMs) {
|
|
1009
|
+
return JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
catch { /* cache miss */ }
|
|
1013
|
+
const { imageToAscii } = await import('../orchestrator/loop.js');
|
|
1014
|
+
const result = await imageToAscii(imgPath, maxWidth);
|
|
1015
|
+
if (result) {
|
|
1016
|
+
try {
|
|
1017
|
+
fs.writeFileSync(cachePath, JSON.stringify(result), 'utf-8');
|
|
1018
|
+
}
|
|
1019
|
+
catch { /* ignore */ }
|
|
1020
|
+
}
|
|
1021
|
+
return result;
|
|
1022
|
+
}
|
|
1023
|
+
catch {
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
// ── Welcome ──
|
|
1028
|
+
const asciiArt = await loadAsciiArt(54);
|
|
1029
|
+
const cwd = process.cwd();
|
|
1030
|
+
const cwdDisplay = cwd.length > 50 ? '...' + cwd.slice(-47) : cwd;
|
|
1031
|
+
const boxLines = [];
|
|
1032
|
+
boxLines.push(theme.fg('\u256d' + BOX_H.repeat(58) + '\u256e'));
|
|
1033
|
+
if (asciiArt) {
|
|
1034
|
+
const artWidth = asciiArt.width;
|
|
1035
|
+
const pad = Math.max(0, 56 - artWidth);
|
|
1036
|
+
for (const line of asciiArt.text.split('\n')) {
|
|
1037
|
+
boxLines.push(theme.fg('\u2502 ') + line + ' '.repeat(pad) + theme.fg(' \u2502'));
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
// Hyacinth \u6807\u9898\u884c
|
|
1041
|
+
const titleLine = theme.fg(' Hyacinth');
|
|
1042
|
+
const titlePad = Math.max(0, 56 - 9); // ' Hyacinth' = 9 chars visible
|
|
1043
|
+
boxLines.push(theme.fg('\u2502 ') + titleLine + ' '.repeat(titlePad) + theme.fg(' \u2502'));
|
|
1044
|
+
// CWD \u884c
|
|
1045
|
+
const cwdPad = Math.max(0, 56 - [...cwdDisplay].length);
|
|
1046
|
+
boxLines.push(theme.fg('\u2502 ') + theme.dim(cwdDisplay) + ' '.repeat(cwdPad) + theme.fg(' \u2502'));
|
|
1047
|
+
boxLines.push(theme.fg('\u2570' + BOX_H.repeat(58) + '\u256f'));
|
|
1048
|
+
chatLog.addSystem(boxLines.join('\n'));
|
|
1049
|
+
chatLog.addSystem('');
|
|
1050
|
+
chatLog.addSystem(theme.dim('Persona: ') + theme.accent(personaDir));
|
|
1051
|
+
chatLog.addSystem('');
|
|
1052
|
+
// ── Bootstrap ──
|
|
1053
|
+
if (resolvedBootstrapStatus === 'pending') {
|
|
1054
|
+
chatLog.addSystem(theme.warning('\u256d' + BOX_H.repeat(56) + '\u256e'));
|
|
1055
|
+
chatLog.addSystem(theme.warning('\u2502 \uD83D\uDD27 Bootstrap Init'));
|
|
1056
|
+
chatLog.addSystem(theme.warning('\u2502 AI will ask you setup questions to configure the persona.'));
|
|
1057
|
+
chatLog.addSystem(theme.warning('\u2570' + BOX_H.repeat(56) + '\u256f'));
|
|
1058
|
+
chatLog.addSystem('');
|
|
1059
|
+
try {
|
|
1060
|
+
// Flow 已在 factory.ts 中自动激活,发送空消息让模型根据 flow_injection 引导对话
|
|
1061
|
+
await loop.run('');
|
|
1062
|
+
}
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1065
|
+
chatLog.addSystem(theme.errorBright('[Bootstrap Error] ') + theme.error(message));
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
chatLog.addSystem(theme.dim('Type ') +
|
|
1069
|
+
theme.success('exit') +
|
|
1070
|
+
theme.dim(' to quit. ') +
|
|
1071
|
+
theme.success('Ctrl+C') +
|
|
1072
|
+
theme.dim(' twice to force. ') +
|
|
1073
|
+
theme.success('Ctrl+P') +
|
|
1074
|
+
theme.dim(' to toggle provider.'));
|
|
1075
|
+
chatLog.addSystem('');
|
|
1076
|
+
tui.requestRender();
|
|
1077
|
+
// Replay previous session events
|
|
1078
|
+
if (sessionDir) {
|
|
1079
|
+
replayEvents(chatLog, sessionDir);
|
|
1080
|
+
}
|
|
1081
|
+
// ── Input state ──
|
|
1082
|
+
let isProcessing = false;
|
|
1083
|
+
const messageQueue = new MessageQueue();
|
|
1084
|
+
let inputHistory = [];
|
|
1085
|
+
let historyIndex = -1;
|
|
1086
|
+
// ── History persistence ──
|
|
1087
|
+
const historyFilePath = path.join(os.homedir(), '.agent', 'history.json');
|
|
1088
|
+
const HISTORY_MAX = 500;
|
|
1089
|
+
try {
|
|
1090
|
+
if (fs.existsSync(historyFilePath)) {
|
|
1091
|
+
const raw = fs.readFileSync(historyFilePath, 'utf-8');
|
|
1092
|
+
const parsed = JSON.parse(raw);
|
|
1093
|
+
if (Array.isArray(parsed)) {
|
|
1094
|
+
inputHistory = parsed.slice(-HISTORY_MAX);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
catch { /* ignore corrupt history */ }
|
|
1099
|
+
function saveHistory() {
|
|
1100
|
+
try {
|
|
1101
|
+
const dir = path.dirname(historyFilePath);
|
|
1102
|
+
if (!fs.existsSync(dir))
|
|
1103
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1104
|
+
fs.writeFileSync(historyFilePath, JSON.stringify(inputHistory.slice(-HISTORY_MAX), null, 2), 'utf-8');
|
|
1105
|
+
}
|
|
1106
|
+
catch { /* ignore write errors */ }
|
|
1107
|
+
}
|
|
1108
|
+
function updateTokenEstimate() {
|
|
1109
|
+
const text = editor.getText();
|
|
1110
|
+
const estimated = Math.ceil(text.length / 4);
|
|
1111
|
+
const ap = loop.getActiveProvider();
|
|
1112
|
+
const providerLabel = `${ap.getProviderType()} \u00b7 `;
|
|
1113
|
+
let footer = theme.dim(`Ctrl+C exit | Ctrl+L clear | Ctrl+P provider | Ctrl+F search | Ctrl+T tools`);
|
|
1114
|
+
// Context-aware hints
|
|
1115
|
+
if (isThinking) {
|
|
1116
|
+
footer += theme.accent(' | Esc to stop');
|
|
1117
|
+
}
|
|
1118
|
+
if (permissionQueue.length > 0) {
|
|
1119
|
+
footer += theme.warning(' | \u2190\u2192 select Enter confirm');
|
|
1120
|
+
}
|
|
1121
|
+
if (backgroundRegistry) {
|
|
1122
|
+
const bgCount = backgroundRegistry.list().filter((p) => p.status === 'running').length;
|
|
1123
|
+
if (bgCount > 0) {
|
|
1124
|
+
footer += theme.accent(` | \u2699 ${bgCount} bg process(es)`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
footer += theme.dim(`\n${providerLabel}${ap.getModel()} \u00b7 ~${estimated} tokens`);
|
|
1128
|
+
if (messageQueue.size > 0) {
|
|
1129
|
+
footer += theme.fg(` | Queue: ${messageQueue.size}`);
|
|
1130
|
+
}
|
|
1131
|
+
footerText.setText(footer);
|
|
1132
|
+
}
|
|
1133
|
+
function truncateMsg(text, max = 40) {
|
|
1134
|
+
return text.length > max ? text.slice(0, max) + '...' : text;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Process one message through the agent loop, then drain any queued messages.
|
|
1138
|
+
* Called by handleInput when the agent is idle.
|
|
1139
|
+
*/
|
|
1140
|
+
async function processBatch(initialText) {
|
|
1141
|
+
let text = initialText;
|
|
1142
|
+
while (text !== undefined) {
|
|
1143
|
+
isProcessing = true;
|
|
1144
|
+
editor.setText('');
|
|
1145
|
+
chatLog.addUser('❯ ' + text);
|
|
1146
|
+
chatLog.addSystem(theme.dim('─'.repeat(30)));
|
|
1147
|
+
try {
|
|
1148
|
+
const sid = sessionDir.split(/[\\/]/).pop() ?? 'tui-default';
|
|
1149
|
+
await tuiChannel.sendMessage(sid, text);
|
|
1150
|
+
}
|
|
1151
|
+
catch (error) {
|
|
1152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1153
|
+
chatLog.addSystem(theme.errorBright('[Error] ') + theme.error(message));
|
|
1154
|
+
}
|
|
1155
|
+
isProcessing = false;
|
|
1156
|
+
// Check queue for pending messages
|
|
1157
|
+
const next = messageQueue.dequeue();
|
|
1158
|
+
text = next?.text;
|
|
1159
|
+
}
|
|
1160
|
+
updateTokenEstimate();
|
|
1161
|
+
tui.requestRender();
|
|
1162
|
+
}
|
|
1163
|
+
async function handleInput(text) {
|
|
1164
|
+
let input = text.trim();
|
|
1165
|
+
if (!input) {
|
|
1166
|
+
// 定时任务完成后队列中有待处理消息 → 空回车触发队列消费
|
|
1167
|
+
if (!isProcessing && !loop.pendingTaskName && messageQueue.size > 0) {
|
|
1168
|
+
const next = messageQueue.dequeue();
|
|
1169
|
+
if (next)
|
|
1170
|
+
await processBatch(next.text);
|
|
1171
|
+
}
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
// Push to history
|
|
1175
|
+
if (inputHistory.length === 0 || inputHistory[inputHistory.length - 1] !== input) {
|
|
1176
|
+
inputHistory.push(input);
|
|
1177
|
+
if (inputHistory.length > HISTORY_MAX) {
|
|
1178
|
+
inputHistory.shift();
|
|
1179
|
+
}
|
|
1180
|
+
saveHistory();
|
|
1181
|
+
}
|
|
1182
|
+
historyIndex = -1;
|
|
1183
|
+
if (input === 'exit' || input === 'quit') {
|
|
1184
|
+
chatLog.addSystem(theme.dim('Goodbye.'));
|
|
1185
|
+
tui.requestRender();
|
|
1186
|
+
await channelManager.stopAll();
|
|
1187
|
+
await loop.shutdown();
|
|
1188
|
+
// 清理插件钩子
|
|
1189
|
+
for (const cleanup of cleanupFns)
|
|
1190
|
+
cleanup();
|
|
1191
|
+
await supervisor.shutdownAll();
|
|
1192
|
+
tui.stop();
|
|
1193
|
+
process.exit(0);
|
|
1194
|
+
}
|
|
1195
|
+
// 二级菜单:检查命令是否有 children
|
|
1196
|
+
if (input.startsWith('/')) {
|
|
1197
|
+
const spaceIdx = input.indexOf(' ');
|
|
1198
|
+
const cmdName = spaceIdx > 0 ? input.slice(1, spaceIdx) : input.slice(1);
|
|
1199
|
+
const cmdDef = CommandRegistry.getInstance().find(cmdName);
|
|
1200
|
+
if ((cmdDef?.children && cmdDef.children.length > 0) || !!cmdDef?.childrenProvider) {
|
|
1201
|
+
const args = spaceIdx > 0 ? input.slice(spaceIdx + 1).trim() : '';
|
|
1202
|
+
// Build dynamic entries for /model L1 panel
|
|
1203
|
+
let effectiveDef = cmdDef;
|
|
1204
|
+
if (cmdName === 'model') {
|
|
1205
|
+
const dynamicChildren = [];
|
|
1206
|
+
// a) Current online model
|
|
1207
|
+
const activeP = loop.getActiveProvider();
|
|
1208
|
+
dynamicChildren.push({
|
|
1209
|
+
name: 'current_online',
|
|
1210
|
+
description: `${activeP.getProviderType()} (online) - ${activeP.getModel()}`,
|
|
1211
|
+
icon: '\u2601',
|
|
1212
|
+
category: 'model',
|
|
1213
|
+
});
|
|
1214
|
+
// b) Local models
|
|
1215
|
+
for (const m of localModel.list()) {
|
|
1216
|
+
const isRunning = localModel.getBridge().isRunning(m.name);
|
|
1217
|
+
const status = isRunning ? '\u25C9 running' : '\u25CB stopped';
|
|
1218
|
+
dynamicChildren.push({
|
|
1219
|
+
name: 'local_' + m.name,
|
|
1220
|
+
description: `${m.name} (local) - ${status}`,
|
|
1221
|
+
icon: '\u{1F4BB}',
|
|
1222
|
+
category: 'model',
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
effectiveDef = {
|
|
1226
|
+
...cmdDef,
|
|
1227
|
+
children: [...dynamicChildren, ...(cmdDef.children ?? [])],
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
if (!args) {
|
|
1231
|
+
// 无参数 → 弹出二级面板
|
|
1232
|
+
slashSubPanelActive = true;
|
|
1233
|
+
const panel = new SlashSubPanel(effectiveDef);
|
|
1234
|
+
panel.show(tui, (result) => {
|
|
1235
|
+
slashSubPanelActive = false;
|
|
1236
|
+
if (result) {
|
|
1237
|
+
// 如果叶子命令有 args 占位符 → 自动填充到输入框,让用户继续输入
|
|
1238
|
+
const leafDef = CommandRegistry.getInstance().find(result.path);
|
|
1239
|
+
if (leafDef?.args && !leafDef.children && !leafDef.childrenProvider) {
|
|
1240
|
+
editor.setText(`/${result.path} `);
|
|
1241
|
+
updateTokenEstimate();
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
handleSlashSubCommand(result.path, '');
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
// 有参数 → 解析子命令路径并直接执行
|
|
1250
|
+
const subSpaceIdx = args.indexOf(' ');
|
|
1251
|
+
const subCmdName = subSpaceIdx > 0 ? args.slice(0, subSpaceIdx) : args;
|
|
1252
|
+
const subArgs = subSpaceIdx > 0 ? args.slice(subSpaceIdx + 1) : '';
|
|
1253
|
+
const subCmd = cmdDef?.children?.find((c) => c.name.toLowerCase() === subCmdName.toLowerCase());
|
|
1254
|
+
if (subCmd) {
|
|
1255
|
+
handleSlashSubCommand(`${cmdName}/${subCmd.name}`, subArgs);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
const childNames = (cmdDef?.children ?? []).map((c) => c.name).join(', ');
|
|
1259
|
+
chatLog.addSystem(theme.warning(`Unknown sub-command: /${cmdName} ${subCmdName}`) +
|
|
1260
|
+
theme.dim(`\nAvailable: /${cmdName} ${childNames}`));
|
|
1261
|
+
tui.requestRender();
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
// ===== Sub-Command Handler =====
|
|
1266
|
+
/** 持久化单个配置字段到 JSON 文件(不展开默认值) */
|
|
1267
|
+
async function persistConfigField(key, value) {
|
|
1268
|
+
const configPath = path.join(os.homedir(), '.agent', 'config.json');
|
|
1269
|
+
try {
|
|
1270
|
+
const raw = await fs.promises.readFile(configPath, 'utf-8');
|
|
1271
|
+
const obj = JSON.parse(raw);
|
|
1272
|
+
const parts = key.split('.');
|
|
1273
|
+
let cur = obj;
|
|
1274
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1275
|
+
if (!cur[parts[i]] || typeof cur[parts[i]] !== 'object') {
|
|
1276
|
+
cur[parts[i]] = {};
|
|
1277
|
+
}
|
|
1278
|
+
cur = cur[parts[i]];
|
|
1279
|
+
}
|
|
1280
|
+
cur[parts[parts.length - 1]] = value;
|
|
1281
|
+
await fs.promises.writeFile(configPath, JSON.stringify(obj, null, 2), 'utf-8');
|
|
1282
|
+
}
|
|
1283
|
+
catch { /* 文件不存在或解析失败,跳过持久化 */ }
|
|
1284
|
+
}
|
|
1285
|
+
/** /model thinking 公共逻辑 */
|
|
1286
|
+
function applyThinking(action) {
|
|
1287
|
+
const cfg = RuntimeConfigCenter.getInstance();
|
|
1288
|
+
const activeP = loop.getActiveProvider();
|
|
1289
|
+
const providerType = activeP.getProviderType();
|
|
1290
|
+
const effortOptions = {};
|
|
1291
|
+
if (providerType === 'deepseek') {
|
|
1292
|
+
effortOptions.high = { label: 'high', effort: 'high' };
|
|
1293
|
+
effortOptions.max = { label: 'max', effort: 'max' };
|
|
1294
|
+
}
|
|
1295
|
+
else if (providerType === 'anthropic') {
|
|
1296
|
+
effortOptions['4k'] = { label: '4K', effort: 4000 };
|
|
1297
|
+
effortOptions['8k'] = { label: '8K', effort: 8000 };
|
|
1298
|
+
effortOptions['16k'] = { label: '16K', effort: 16000 };
|
|
1299
|
+
effortOptions['32k'] = { label: '32K', effort: 32000 };
|
|
1300
|
+
}
|
|
1301
|
+
if (action === 'on') {
|
|
1302
|
+
cfg.set('provider.enableThinking', true);
|
|
1303
|
+
activeP.setThinking?.(true);
|
|
1304
|
+
persistConfigField('provider.enableThinking', true);
|
|
1305
|
+
chatLog.addSystem(theme.success('Thinking enabled'));
|
|
1306
|
+
}
|
|
1307
|
+
else if (action === 'off') {
|
|
1308
|
+
cfg.set('provider.enableThinking', false);
|
|
1309
|
+
activeP.setThinking?.(false);
|
|
1310
|
+
persistConfigField('provider.enableThinking', false);
|
|
1311
|
+
chatLog.addSystem(theme.success('Thinking disabled'));
|
|
1312
|
+
}
|
|
1313
|
+
else if (effortOptions[action]) {
|
|
1314
|
+
const opt = effortOptions[action];
|
|
1315
|
+
cfg.set('provider.enableThinking', true);
|
|
1316
|
+
activeP.setThinking?.(true, opt.effort);
|
|
1317
|
+
persistConfigField('provider.enableThinking', true);
|
|
1318
|
+
chatLog.addSystem(theme.success(`Thinking enabled (${opt.label})`));
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
const optsStr = Object.entries(effortOptions)
|
|
1322
|
+
.map(([k, v]) => ` ${k} → ${v.label}`)
|
|
1323
|
+
.join('\n');
|
|
1324
|
+
chatLog.addSystem(theme.warning(`Usage: /model thinking <on|off${optsStr ? '|' + Object.keys(effortOptions).join('|') : ''}>` + (optsStr ? '\n' + optsStr : '')));
|
|
1325
|
+
}
|
|
1326
|
+
tui.requestRender();
|
|
1327
|
+
updateTokenEstimate();
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* 通用 handler 路由:根据 "module.method" 格式的 handler 字符串,
|
|
1331
|
+
* 动态查找并调用对应模块的方法。
|
|
1332
|
+
*/
|
|
1333
|
+
function resolveHandler(handlerStr, args, localModel, chatLog, themeObj, tui) {
|
|
1334
|
+
const parts = handlerStr.split('.');
|
|
1335
|
+
if (parts.length < 2)
|
|
1336
|
+
return false;
|
|
1337
|
+
const moduleName = parts[0];
|
|
1338
|
+
const methodChain = parts.slice(1);
|
|
1339
|
+
let target;
|
|
1340
|
+
if (moduleName === 'localModel') {
|
|
1341
|
+
target = localModel;
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
1346
|
+
let fn = target;
|
|
1347
|
+
for (const prop of methodChain) {
|
|
1348
|
+
if (fn && typeof fn === 'object' && prop in fn) {
|
|
1349
|
+
fn = fn[prop];
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
chatLog.addSystem(themeObj.error(`Command handler not found: ${handlerStr}`));
|
|
1353
|
+
tui.requestRender();
|
|
1354
|
+
return true;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (typeof fn !== 'function') {
|
|
1358
|
+
chatLog.addSystem(themeObj.error(`Command handler not found: ${handlerStr}`));
|
|
1359
|
+
tui.requestRender();
|
|
1360
|
+
return true;
|
|
1361
|
+
}
|
|
1362
|
+
try {
|
|
1363
|
+
const result = args ? fn(args) : fn();
|
|
1364
|
+
if (result instanceof Promise) {
|
|
1365
|
+
result.then((val) => {
|
|
1366
|
+
if (val !== undefined && val !== null) {
|
|
1367
|
+
const display = typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
|
|
1368
|
+
chatLog.addSystem(themeObj.success(display));
|
|
1369
|
+
}
|
|
1370
|
+
tui.requestRender();
|
|
1371
|
+
}).catch((err) => {
|
|
1372
|
+
chatLog.addSystem(themeObj.error(`Handler error: ${err.message}`));
|
|
1373
|
+
tui.requestRender();
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
else if (result !== undefined && result !== null) {
|
|
1377
|
+
const display = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result);
|
|
1378
|
+
chatLog.addSystem(themeObj.success(display));
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
catch (err) {
|
|
1382
|
+
chatLog.addSystem(themeObj.error(`Handler error: ${err.message}`));
|
|
1383
|
+
tui.requestRender();
|
|
1384
|
+
return true;
|
|
1385
|
+
}
|
|
1386
|
+
return true;
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* 处理二级菜单子命令的执行。
|
|
1390
|
+
* path 格式: "model/switch" / "model/provider" 等
|
|
1391
|
+
*/
|
|
1392
|
+
async function handleSlashSubCommand(cmdPath, restArgs) {
|
|
1393
|
+
const cfg = RuntimeConfigCenter.getInstance();
|
|
1394
|
+
if (cmdPath.startsWith('session/')) {
|
|
1395
|
+
let sub = cmdPath.slice('session/'.length);
|
|
1396
|
+
// 支持嵌套路径: session/<id>/load → sub = "<id>/load"
|
|
1397
|
+
const subParts = sub.split('/');
|
|
1398
|
+
if (subParts.length === 2 && (subParts[1] === 'load' || subParts[1] === 'delete')) {
|
|
1399
|
+
// session/<id>/load 或 session/<id>/delete
|
|
1400
|
+
const sessionId = subParts[0];
|
|
1401
|
+
if (subParts[1] === 'load') {
|
|
1402
|
+
const SessionManager = (await import('../memory/session.js')).SessionManager;
|
|
1403
|
+
const sm = new SessionManager(process.cwd());
|
|
1404
|
+
const dir = sm.getSessionDir(sessionId);
|
|
1405
|
+
const fsPromises = await import('node:fs/promises');
|
|
1406
|
+
try {
|
|
1407
|
+
await fsPromises.access(dir);
|
|
1408
|
+
}
|
|
1409
|
+
catch {
|
|
1410
|
+
chatLog.addSystem(theme.warning(`Session "${sessionId}" not found. Use /session list to see available sessions.`));
|
|
1411
|
+
tui.requestRender();
|
|
1412
|
+
updateTokenEstimate();
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
// 就地切换,不重启
|
|
1416
|
+
await loop.switchSession(dir);
|
|
1417
|
+
sessionDir = dir;
|
|
1418
|
+
chatLog.clearAll();
|
|
1419
|
+
replayEvents(chatLog, dir);
|
|
1420
|
+
chatLog.addSystem(theme.success(`已切换到 session ${sessionId}`));
|
|
1421
|
+
updateTokenEstimate();
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
const SessionManager = (await import('../memory/session.js')).SessionManager;
|
|
1425
|
+
const sm = new SessionManager(process.cwd());
|
|
1426
|
+
const dir = sm.getSessionDir(sessionId);
|
|
1427
|
+
// 保护当前活跃 session
|
|
1428
|
+
if (dir === sessionDir) {
|
|
1429
|
+
chatLog.addSystem(theme.warning(`Cannot delete the currently active session "${sessionId}". Switch to another session first.`));
|
|
1430
|
+
tui.requestRender();
|
|
1431
|
+
updateTokenEstimate();
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
try {
|
|
1435
|
+
const fsPromises = await import('node:fs/promises');
|
|
1436
|
+
await fsPromises.access(dir);
|
|
1437
|
+
await fsPromises.rm(dir, { recursive: true, force: true });
|
|
1438
|
+
chatLog.addSystem(theme.success(`Session ${sessionId} deleted`));
|
|
1439
|
+
}
|
|
1440
|
+
catch {
|
|
1441
|
+
chatLog.addSystem(theme.warning(`Session ${sessionId} not found`));
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
tui.requestRender();
|
|
1445
|
+
updateTokenEstimate();
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (sub === 'list') {
|
|
1449
|
+
const SessionManager = (await import('../memory/session.js')).SessionManager;
|
|
1450
|
+
const sm = new SessionManager(process.cwd());
|
|
1451
|
+
const sessions = await sm.list();
|
|
1452
|
+
const modeType = preciseModeActive ? 'precise' : 'normal';
|
|
1453
|
+
const filtered = sessions.filter(s => (s.type ?? 'normal') === modeType);
|
|
1454
|
+
if (filtered.length === 0) {
|
|
1455
|
+
chatLog.addSystem(theme.dim(`No ${modeType} sessions found`));
|
|
1456
|
+
}
|
|
1457
|
+
else {
|
|
1458
|
+
for (const s of filtered) {
|
|
1459
|
+
const isCurrent = s.id === path.basename(sessionDir);
|
|
1460
|
+
const marker = isCurrent ? theme.success(' ← current') : '';
|
|
1461
|
+
const typeLabel = s.type ? theme.dim(` [${s.type}]`) : '';
|
|
1462
|
+
chatLog.addSystem(theme.accent(s.id) +
|
|
1463
|
+
theme.dim(` | created: ${s.createdAt}`) +
|
|
1464
|
+
theme.dim(` | updated: ${s.updatedAt}`) +
|
|
1465
|
+
typeLabel + marker);
|
|
1466
|
+
}
|
|
1467
|
+
chatLog.addSystem(theme.dim('─'.repeat(60)));
|
|
1468
|
+
chatLog.addSystem(theme.dim('加载会话: /session <完整ID>/load 例如: /session ' + (filtered[0]?.id ?? '') + '/load'));
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
else if (sub === 'new') {
|
|
1472
|
+
chatLog.addSystem(theme.warning('请使用 hyacinth start 启动新会话(当前会话需要退出)'));
|
|
1473
|
+
}
|
|
1474
|
+
else if (sub === 'load') {
|
|
1475
|
+
const id = restArgs?.trim();
|
|
1476
|
+
if (!id) {
|
|
1477
|
+
chatLog.addSystem(theme.warning('用法: /session load <sessionId>'));
|
|
1478
|
+
}
|
|
1479
|
+
else {
|
|
1480
|
+
chatLog.addSystem(theme.warning(`请使用 hyacinth start --session ${id} 加载会话`));
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
else if (sub === 'delete') {
|
|
1484
|
+
const id = restArgs?.trim();
|
|
1485
|
+
if (!id) {
|
|
1486
|
+
chatLog.addSystem(theme.warning('用法: /session delete <sessionId>'));
|
|
1487
|
+
}
|
|
1488
|
+
else {
|
|
1489
|
+
const SessionManager = (await import('../memory/session.js')).SessionManager;
|
|
1490
|
+
const sm = new SessionManager(process.cwd());
|
|
1491
|
+
// 检查 session 类型是否匹配当前模式
|
|
1492
|
+
const sessions = await sm.list();
|
|
1493
|
+
const target = sessions.find(s => s.id === id);
|
|
1494
|
+
if (!target) {
|
|
1495
|
+
chatLog.addSystem(theme.warning(`Session ${id} not found`));
|
|
1496
|
+
}
|
|
1497
|
+
else if ((target.type ?? 'normal') !== (preciseModeActive ? 'precise' : 'normal')) {
|
|
1498
|
+
chatLog.addSystem(theme.warning(`Session ${id} 是 [${target.type ?? 'normal'}] 类型,当前为 [${preciseModeActive ? 'precise' : 'normal'}] 模式,无法跨模式操作`));
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
const dir = sm.getSessionDir(id);
|
|
1502
|
+
try {
|
|
1503
|
+
const fsPromises = await import('node:fs/promises');
|
|
1504
|
+
await fsPromises.access(dir);
|
|
1505
|
+
await fsPromises.rm(dir, { recursive: true, force: true });
|
|
1506
|
+
chatLog.addSystem(theme.success(`Session ${id} deleted`));
|
|
1507
|
+
}
|
|
1508
|
+
catch {
|
|
1509
|
+
chatLog.addSystem(theme.warning(`Session ${id} not found`));
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
tui.requestRender();
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
switch (cmdPath) {
|
|
1518
|
+
case 'model/settings/switch':
|
|
1519
|
+
case 'model/switch': {
|
|
1520
|
+
if (!restArgs) {
|
|
1521
|
+
chatLog.addSystem(theme.warning('Usage: /model switch <model-name>'));
|
|
1522
|
+
tui.requestRender();
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
const activeP = loop.getActiveProvider();
|
|
1526
|
+
const providerType = activeP.getProviderType();
|
|
1527
|
+
cfg.set(`provider.${providerType}.model`, restArgs);
|
|
1528
|
+
cfg.save().catch(() => { });
|
|
1529
|
+
chatLog.addSystem(theme.success('Model name set to ') + theme.fg(String(restArgs)) + theme.dim(` (provider: ${providerType})`));
|
|
1530
|
+
tui.requestRender();
|
|
1531
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
case 'model/settings/provider':
|
|
1535
|
+
case 'model/provider': {
|
|
1536
|
+
if (!restArgs) {
|
|
1537
|
+
chatLog.addSystem(theme.warning('Usage: /model provider <anthropic|openai|deepseek|gemini|groq|xai|mistral|openrouter|moonshot|qwen|zhipu|minimax|mimo|local>'));
|
|
1538
|
+
tui.requestRender();
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
if (restArgs === 'local') {
|
|
1542
|
+
const lmList = localModel.list();
|
|
1543
|
+
// 无已注册模型 → 检查本地模型配置或直接切
|
|
1544
|
+
if (lmList.length === 0) {
|
|
1545
|
+
const { getLocalProviderConfigLoader } = await import('../provider/local-config.js');
|
|
1546
|
+
const localCfg = getLocalProviderConfigLoader();
|
|
1547
|
+
if (localCfg?.defaultModel) {
|
|
1548
|
+
// 本地模型已配置 → 直接切换
|
|
1549
|
+
try {
|
|
1550
|
+
await loop.switchProvider('local');
|
|
1551
|
+
// 显式持久化 provider 选择(switchProvider 不再负责持久化)
|
|
1552
|
+
cfg.set('provider.active', 'local');
|
|
1553
|
+
cfg.save().catch(() => { });
|
|
1554
|
+
modelName = loop.getActiveProvider().getModel();
|
|
1555
|
+
providerTypeStart = loop.getActiveProvider().getProviderType();
|
|
1556
|
+
chatLog.addSystem(theme.success(`Switched to local (${localCfg.baseUrl}, ${localCfg.defaultModel})`));
|
|
1557
|
+
chatLog.addSystem(theme.dim('Register models via /model local/register for process management.'));
|
|
1558
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
|
|
1559
|
+
}
|
|
1560
|
+
catch (err) {
|
|
1561
|
+
chatLog.addSystem(theme.error(`Switch failed: ${err.message}`));
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
else {
|
|
1565
|
+
chatLog.addSystem(theme.warning('No local models configured.'));
|
|
1566
|
+
chatLog.addSystem(theme.dim('Set a local model in config or register models via /model local/register.'));
|
|
1567
|
+
}
|
|
1568
|
+
tui.requestRender();
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
const targetName = localModel.getActive() ?? lmList[0].name;
|
|
1572
|
+
localModel.switch(targetName).then(async (info) => {
|
|
1573
|
+
if (info) {
|
|
1574
|
+
cfg.set('provider.local', {
|
|
1575
|
+
type: 'local',
|
|
1576
|
+
model: info.modelFile ?? targetName,
|
|
1577
|
+
baseUrl: info.baseUrl,
|
|
1578
|
+
});
|
|
1579
|
+
cfg.set('provider.local.modelKey', targetName);
|
|
1580
|
+
cfg.save().catch(() => { });
|
|
1581
|
+
try {
|
|
1582
|
+
await loop.switchProvider('local');
|
|
1583
|
+
// 显式持久化 provider 选择(switchProvider 不再负责持久化)
|
|
1584
|
+
cfg.set('provider.active', 'local');
|
|
1585
|
+
cfg.save().catch(() => { });
|
|
1586
|
+
modelName = loop.getActiveProvider().getModel();
|
|
1587
|
+
providerTypeStart = loop.getActiveProvider().getProviderType();
|
|
1588
|
+
chatLog.addSystem(theme.success(`Switched to local model: ${targetName} (port ${info.port})`));
|
|
1589
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
|
|
1590
|
+
}
|
|
1591
|
+
catch (swErr) {
|
|
1592
|
+
chatLog.addSystem(theme.error(`Failed: ${swErr.message}`));
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
else {
|
|
1596
|
+
chatLog.addSystem(theme.error(`Failed to start ${targetName}`));
|
|
1597
|
+
}
|
|
1598
|
+
tui.requestRender();
|
|
1599
|
+
}).catch((e) => {
|
|
1600
|
+
chatLog.addSystem(theme.error(`Failed: ${e.message}`));
|
|
1601
|
+
tui.requestRender();
|
|
1602
|
+
});
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
localModel.getBridge().stopAll().catch(() => { });
|
|
1606
|
+
try {
|
|
1607
|
+
await loop.switchProvider(restArgs);
|
|
1608
|
+
// 显式持久化 provider 选择(switchProvider 不再负责持久化)
|
|
1609
|
+
cfg.set('provider.active', restArgs);
|
|
1610
|
+
cfg.save().catch(() => { });
|
|
1611
|
+
}
|
|
1612
|
+
catch (e) {
|
|
1613
|
+
chatLog.addSystem(theme.error(`Failed: ${e.message}`));
|
|
1614
|
+
tui.requestRender();
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
modelName = loop.getActiveProvider().getModel();
|
|
1618
|
+
providerTypeStart = loop.getActiveProvider().getProviderType();
|
|
1619
|
+
chatLog.addSystem(theme.success('Provider switched to ') + theme.fg(String(restArgs)) + theme.dim(' (persisted)'));
|
|
1620
|
+
tui.requestRender();
|
|
1621
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
case 'model/settings/source':
|
|
1625
|
+
case 'model/source': {
|
|
1626
|
+
if (!restArgs) {
|
|
1627
|
+
chatLog.addSystem(theme.warning('Usage: /model source <role> <main|local|channel-name>'));
|
|
1628
|
+
tui.requestRender();
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const parts2 = restArgs.split(/\s+/).filter(Boolean);
|
|
1632
|
+
if (parts2.length < 2) {
|
|
1633
|
+
chatLog.addSystem(theme.warning('Usage: /model source <assessment|planning|compression|sub-agent|all> <main|local|channel-name>'));
|
|
1634
|
+
tui.requestRender();
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
const roleArg = parts2[0].toLowerCase();
|
|
1638
|
+
const sourceArg = parts2[1];
|
|
1639
|
+
const validRoles = ['assessment', 'planning', 'compression', 'sub-agent', 'all'];
|
|
1640
|
+
if (!validRoles.includes(roleArg)) {
|
|
1641
|
+
chatLog.addSystem(theme.warning('Role must be: assessment, planning, compression, sub-agent, or all'));
|
|
1642
|
+
tui.requestRender();
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
// 尝试用 ModelChannelRegistry 的 setRoleMapping(多通道模式)
|
|
1646
|
+
const modelRouter = loop.modelRouter;
|
|
1647
|
+
const registry = modelRouter?.getRegistry();
|
|
1648
|
+
const channelNames = registry ? registry.listChannelNames() : [];
|
|
1649
|
+
const isChannelName = channelNames.includes(sourceArg);
|
|
1650
|
+
try {
|
|
1651
|
+
if (isChannelName && registry) {
|
|
1652
|
+
// 映射到已注册的通道
|
|
1653
|
+
const roles = roleArg === 'all'
|
|
1654
|
+
? ['assessment', 'planning', 'compression', 'sub-agent']
|
|
1655
|
+
: [roleArg];
|
|
1656
|
+
for (const r of roles) {
|
|
1657
|
+
registry.setRoleMapping(r, sourceArg);
|
|
1658
|
+
}
|
|
1659
|
+
chatLog.addSystem(theme.success(`Mapped ${roles.join(', ')} → channel "${sourceArg}"`));
|
|
1660
|
+
}
|
|
1661
|
+
else {
|
|
1662
|
+
chatLog.addSystem(theme.warning(`Channel "${sourceArg}" not found. Available: ${channelNames.join(', ') || '(none)'}. Use /channel add ${sourceArg} <provider> [model] to create it.`));
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
catch (e) {
|
|
1666
|
+
chatLog.addSystem(theme.error(`Failed: ${e.message}`));
|
|
1667
|
+
}
|
|
1668
|
+
tui.requestRender();
|
|
1669
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
case 'model/settings/thinking':
|
|
1673
|
+
case 'model/thinking': {
|
|
1674
|
+
applyThinking(restArgs);
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
// 四级菜单:/model thinking on|off|high|max 通过子面板选择
|
|
1678
|
+
case 'model/settings/thinking/on':
|
|
1679
|
+
case 'model/thinking/on':
|
|
1680
|
+
applyThinking('on');
|
|
1681
|
+
return;
|
|
1682
|
+
case 'model/settings/thinking/off':
|
|
1683
|
+
case 'model/thinking/off':
|
|
1684
|
+
applyThinking('off');
|
|
1685
|
+
return;
|
|
1686
|
+
case 'model/settings/thinking/high':
|
|
1687
|
+
case 'model/thinking/high':
|
|
1688
|
+
applyThinking('high');
|
|
1689
|
+
return;
|
|
1690
|
+
case 'model/settings/thinking/max':
|
|
1691
|
+
case 'model/thinking/max':
|
|
1692
|
+
applyThinking('max');
|
|
1693
|
+
return;
|
|
1694
|
+
case 'model/settings/thinking/4k':
|
|
1695
|
+
case 'model/thinking/4k':
|
|
1696
|
+
applyThinking('4k');
|
|
1697
|
+
return;
|
|
1698
|
+
case 'model/settings/thinking/8k':
|
|
1699
|
+
case 'model/thinking/8k':
|
|
1700
|
+
applyThinking('8k');
|
|
1701
|
+
return;
|
|
1702
|
+
case 'model/settings/thinking/16k':
|
|
1703
|
+
case 'model/thinking/16k':
|
|
1704
|
+
applyThinking('16k');
|
|
1705
|
+
return;
|
|
1706
|
+
case 'model/settings/thinking/32k':
|
|
1707
|
+
case 'model/thinking/32k':
|
|
1708
|
+
applyThinking('32k');
|
|
1709
|
+
return;
|
|
1710
|
+
case 'model/settings/show-thinking':
|
|
1711
|
+
case 'model/show-thinking': {
|
|
1712
|
+
showThinking = !showThinking;
|
|
1713
|
+
chatLog.addSystem(theme.success(showThinking ? 'Thinking content will be shown' : 'Thinking content hidden'));
|
|
1714
|
+
tui.requestRender();
|
|
1715
|
+
updateTokenEstimate();
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
case 'model/settings/info':
|
|
1719
|
+
case 'model/info': {
|
|
1720
|
+
const providerName = String(cfg.get('provider.active') ?? 'unknown');
|
|
1721
|
+
const activeProvider = loop.getActiveProvider();
|
|
1722
|
+
const modelName = activeProvider ? activeProvider.getModel() : 'unknown';
|
|
1723
|
+
const lines = [];
|
|
1724
|
+
lines.push(theme.accent('=== Model Info ==='));
|
|
1725
|
+
lines.push(' Provider: ' + theme.fg(providerName));
|
|
1726
|
+
lines.push(' Model: ' + theme.fg(modelName));
|
|
1727
|
+
const routing = loop.getProviderRoutingInfo();
|
|
1728
|
+
if (routing) {
|
|
1729
|
+
lines.push(' Route: ' + theme.fg(routing.mode) + (routing.isLocal ? theme.success(' (local)') : theme.accent(' (online)')));
|
|
1730
|
+
}
|
|
1731
|
+
// 通道信息
|
|
1732
|
+
const modelRouter = loop.modelRouter;
|
|
1733
|
+
if (modelRouter) {
|
|
1734
|
+
const registry = modelRouter.getRegistry();
|
|
1735
|
+
const channels = registry.listChannels();
|
|
1736
|
+
const roles = registry.listRoles();
|
|
1737
|
+
if (channels.length > 1 || Object.keys(roles).some(r => roles[r] !== 'main')) {
|
|
1738
|
+
lines.push(theme.dim(' ── Channels ──'));
|
|
1739
|
+
for (const ch of channels) {
|
|
1740
|
+
const chRoles = Object.entries(roles)
|
|
1741
|
+
.filter(([, cn]) => cn === ch.name)
|
|
1742
|
+
.map(([r]) => r);
|
|
1743
|
+
const roleStr = chRoles.length > 0 ? ' ← ' + chRoles.join(', ') : '';
|
|
1744
|
+
lines.push(theme.dim(` ${ch.name}: ${ch.provider}${ch.model ? '/' + ch.model : ''}`) + theme.fg(roleStr));
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
// 旧 source 信息
|
|
1749
|
+
const sources = loop.getModelSources();
|
|
1750
|
+
if (sources) {
|
|
1751
|
+
const labels = { assessment: '评估', planning: '规划', compression: '压缩' };
|
|
1752
|
+
for (const [role, src] of Object.entries(sources)) {
|
|
1753
|
+
const label = labels[role] ?? role;
|
|
1754
|
+
const srcColor = src === 'local' ? theme.success(String(src)) : theme.accent(String(src));
|
|
1755
|
+
lines.push(' ' + theme.fg(label) + theme.dim(': ') + srcColor);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
chatLog.addSystem(lines.join('\n'));
|
|
1759
|
+
tui.requestRender();
|
|
1760
|
+
updateTokenEstimate();
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
case 'model/settings/context': {
|
|
1764
|
+
if (!restArgs) {
|
|
1765
|
+
const activeP = loop.getActiveProvider();
|
|
1766
|
+
const modelCtxWindow = getModelContextWindow(activeP.getProviderType(), activeP.getModel());
|
|
1767
|
+
chatLog.addSystem(theme.warning('Usage: /model settings context <tokens>') + theme.dim(` (1-${modelCtxWindow.toLocaleString()})`));
|
|
1768
|
+
tui.requestRender();
|
|
1769
|
+
updateTokenEstimate();
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
const tokens = parseInt(restArgs.trim(), 10);
|
|
1773
|
+
const activeP = loop.getActiveProvider();
|
|
1774
|
+
const modelCtxWindow = getModelContextWindow(activeP.getProviderType(), activeP.getModel());
|
|
1775
|
+
const upper = modelCtxWindow;
|
|
1776
|
+
if (isNaN(tokens) || tokens < 1 || tokens > upper) {
|
|
1777
|
+
chatLog.addSystem(theme.warning(`Usage: /model settings context <1-${upper.toLocaleString()}>`) + theme.dim(` (model: ${activeP.getModel()})`));
|
|
1778
|
+
tui.requestRender();
|
|
1779
|
+
updateTokenEstimate();
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
cfg.set('session.maxContext', tokens);
|
|
1783
|
+
cfg.save().catch(() => { });
|
|
1784
|
+
chatLog.addSystem(theme.success('Max context set to ') + theme.fg(tokens.toLocaleString() + ' tokens'));
|
|
1785
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
|
|
1786
|
+
updateTokenEstimate();
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
// ── 本地模型 L2: model/local/start ──
|
|
1790
|
+
case 'model/local/start': {
|
|
1791
|
+
const { detectLocalBackend } = await import('../provider/local-config.js');
|
|
1792
|
+
const detected = await detectLocalBackend();
|
|
1793
|
+
const ollamaBin = localModel.checkOllama();
|
|
1794
|
+
const llamacppBin = localModel.checkLlamacpp();
|
|
1795
|
+
const backend = restArgs?.toLowerCase();
|
|
1796
|
+
const validBackend = backend === 'ollama' || backend === 'llamacpp' || backend === 'llama.cpp';
|
|
1797
|
+
// 指定了 backend → 启动那个
|
|
1798
|
+
if (validBackend) {
|
|
1799
|
+
const target = (backend === 'llamacpp' || backend === 'llama.cpp') ? 'llamacpp' : 'ollama';
|
|
1800
|
+
if (target === 'ollama' && ollamaBin) {
|
|
1801
|
+
if (detected?.backend === 'ollama') {
|
|
1802
|
+
chatLog.addSystem(theme.success('Ollama 已在运行。使用 /model/local/switch 切换。'));
|
|
1803
|
+
}
|
|
1804
|
+
else {
|
|
1805
|
+
chatLog.addSystem(theme.accent('启动 Ollama...'));
|
|
1806
|
+
const info = await supervisor.startOllamaOnDemand(process.cwd());
|
|
1807
|
+
chatLog.addSystem(info ? theme.success('Ollama 已启动(框架管理进程)。') : theme.warning('启动失败,请手动运行 ollama serve。'));
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
else if (target === 'llamacpp' && llamacppBin) {
|
|
1811
|
+
const regModels = localModel.list();
|
|
1812
|
+
if (regModels.length === 0) {
|
|
1813
|
+
chatLog.addSystem(theme.warning('无注册的 llama.cpp 模型。请先用 /model/local/register 注册。'));
|
|
1814
|
+
}
|
|
1815
|
+
else {
|
|
1816
|
+
localModel.start(regModels[0].name).then(async (info) => {
|
|
1817
|
+
if (info) {
|
|
1818
|
+
cfg.set('provider.local', { type: 'local', model: info.modelFile ?? regModels[0].name, baseUrl: info.baseUrl });
|
|
1819
|
+
cfg.set('provider.local.modelKey', regModels[0].name);
|
|
1820
|
+
cfg.save().catch(() => { });
|
|
1821
|
+
try {
|
|
1822
|
+
await loop.switchProvider('local');
|
|
1823
|
+
cfg.set('provider.active', 'local');
|
|
1824
|
+
cfg.save().catch(() => { });
|
|
1825
|
+
chatLog.addSystem(theme.success(`llama.cpp ${regModels[0].name} started`));
|
|
1826
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
|
|
1827
|
+
}
|
|
1828
|
+
catch (swErr) {
|
|
1829
|
+
chatLog.addSystem(theme.error(`Switch failed: ${swErr.message}`));
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
tui.requestRender();
|
|
1833
|
+
}).catch((e) => { chatLog.addSystem(theme.error(`Failed: ${e.message}`)); tui.requestRender(); });
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
else {
|
|
1837
|
+
chatLog.addSystem(theme.warning(`${backend} 未安装。`));
|
|
1838
|
+
}
|
|
1839
|
+
tui.requestRender();
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
// 指定了已注册模型名 → 按名启动
|
|
1843
|
+
if (restArgs) {
|
|
1844
|
+
const lm = localModel.list().find((m) => m.name === restArgs);
|
|
1845
|
+
if (lm) {
|
|
1846
|
+
localModel.start(restArgs).then(async (info) => {
|
|
1847
|
+
if (info) {
|
|
1848
|
+
cfg.set('provider.local', { type: 'local', model: info.modelFile ?? restArgs, baseUrl: info.baseUrl });
|
|
1849
|
+
cfg.set('provider.local.modelKey', restArgs);
|
|
1850
|
+
cfg.save().catch(() => { });
|
|
1851
|
+
try {
|
|
1852
|
+
await loop.switchProvider('local');
|
|
1853
|
+
cfg.set('provider.active', 'local');
|
|
1854
|
+
cfg.save().catch(() => { });
|
|
1855
|
+
chatLog.addSystem(theme.success(`${restArgs} started on port ${info.port}`));
|
|
1856
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
|
|
1857
|
+
}
|
|
1858
|
+
catch (swErr) {
|
|
1859
|
+
chatLog.addSystem(theme.error(`Switch failed: ${swErr.message}`));
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
else {
|
|
1863
|
+
chatLog.addSystem(theme.error(`Failed to start ${restArgs}`));
|
|
1864
|
+
}
|
|
1865
|
+
tui.requestRender();
|
|
1866
|
+
}).catch((e) => { chatLog.addSystem(theme.error(`Failed: ${e.message}`)); tui.requestRender(); });
|
|
1867
|
+
}
|
|
1868
|
+
else {
|
|
1869
|
+
chatLog.addSystem(theme.warning(`Model "${restArgs}" not registered. Available: ${localModel.list().map((m) => m.name).join(', ') || 'none'}`));
|
|
1870
|
+
chatLog.addSystem(theme.dim('To start a backend: /model/local start ollama | /model/local start llamacpp'));
|
|
1871
|
+
tui.requestRender();
|
|
1872
|
+
}
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
// 无参 → 列出可用选项
|
|
1876
|
+
chatLog.addSystem(theme.fg('── 可用本地后端 ──'));
|
|
1877
|
+
if (detected)
|
|
1878
|
+
chatLog.addSystem(theme.success(`${detected.backend} 正在运行 — ${detected.baseUrl}`));
|
|
1879
|
+
if (ollamaBin)
|
|
1880
|
+
chatLog.addSystem(theme.dim(`Ollama ${detected?.backend === 'ollama' ? '(运行中)' : '— /model/local start ollama'}`));
|
|
1881
|
+
if (llamacppBin)
|
|
1882
|
+
chatLog.addSystem(theme.dim(`llama.cpp ${detected?.backend === 'llamacpp' ? '(运行中)' : '— /model/local start llamacpp'}`));
|
|
1883
|
+
if (!ollamaBin && !llamacppBin)
|
|
1884
|
+
chatLog.addSystem(theme.warning('本地模型服务未配置。请安装 Ollama 或 llama.cpp。'));
|
|
1885
|
+
const reg = localModel.list();
|
|
1886
|
+
if (reg.length > 0)
|
|
1887
|
+
chatLog.addSystem(theme.dim(`已注册模型: ${reg.map((m) => m.name).join(', ')}`));
|
|
1888
|
+
tui.requestRender();
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
// ── 本地模型 L2: model/local/stop ──
|
|
1892
|
+
case 'model/local/stop': {
|
|
1893
|
+
const { detectLocalBackend } = await import('../provider/local-config.js');
|
|
1894
|
+
const detected = await detectLocalBackend();
|
|
1895
|
+
const backend = restArgs?.toLowerCase();
|
|
1896
|
+
if (backend === 'ollama') {
|
|
1897
|
+
if (detected?.backend === 'ollama') {
|
|
1898
|
+
try {
|
|
1899
|
+
await supervisor.stopModel('ollama');
|
|
1900
|
+
chatLog.addSystem(theme.success('Ollama 已停止。'));
|
|
1901
|
+
}
|
|
1902
|
+
catch {
|
|
1903
|
+
chatLog.addSystem(theme.warning('无法停止 Ollama。请手动执行 ollama stop。'));
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
else {
|
|
1907
|
+
chatLog.addSystem(theme.dim('Ollama 未在运行。'));
|
|
1908
|
+
}
|
|
1909
|
+
tui.requestRender();
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
if (backend === 'llamacpp' || backend === 'llama.cpp') {
|
|
1913
|
+
const running = localModel.getBridge().getAllStatus().filter((s) => s.state === 'running');
|
|
1914
|
+
if (running.length > 0) {
|
|
1915
|
+
for (const m of running) {
|
|
1916
|
+
localModel.getBridge().stop(m.name).catch(() => { });
|
|
1917
|
+
chatLog.addSystem(theme.success(`Stopped: ${m.name}`));
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
else {
|
|
1921
|
+
chatLog.addSystem(theme.dim('llama.cpp 未在运行。'));
|
|
1922
|
+
}
|
|
1923
|
+
tui.requestRender();
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
// 无参 → 停止所有
|
|
1927
|
+
let stopped = 0;
|
|
1928
|
+
if (detected?.backend === 'ollama') {
|
|
1929
|
+
try {
|
|
1930
|
+
await supervisor.stopModel('ollama');
|
|
1931
|
+
stopped++;
|
|
1932
|
+
chatLog.addSystem(theme.success('Ollama 已停止。'));
|
|
1933
|
+
}
|
|
1934
|
+
catch {
|
|
1935
|
+
chatLog.addSystem(theme.warning('无法停止 Ollama。'));
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
const running = localModel.getBridge().getAllStatus().filter((s) => s.state === 'running');
|
|
1939
|
+
for (const m of running) {
|
|
1940
|
+
localModel.getBridge().stop(m.name).catch(() => { });
|
|
1941
|
+
stopped++;
|
|
1942
|
+
chatLog.addSystem(theme.success(`Stopped: ${m.name}`));
|
|
1943
|
+
}
|
|
1944
|
+
if (stopped === 0)
|
|
1945
|
+
chatLog.addSystem(theme.dim('没有运行中的本地服务。'));
|
|
1946
|
+
tui.requestRender();
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
// ── 本地模型 L2: model/local/status ──
|
|
1950
|
+
case 'model/local/status': {
|
|
1951
|
+
const { detectLocalBackend } = await import('../provider/local-config.js');
|
|
1952
|
+
const detected = await detectLocalBackend();
|
|
1953
|
+
const ollamaBin = localModel.checkOllama();
|
|
1954
|
+
const llamacppBin = localModel.checkLlamacpp();
|
|
1955
|
+
chatLog.addSystem(theme.fg('── 本地模型状态 ──'));
|
|
1956
|
+
chatLog.addSystem(detected ? theme.success(`运行中: ${detected.backend} — ${detected.baseUrl}`) : theme.dim('运行中: 无'));
|
|
1957
|
+
chatLog.addSystem(theme.dim(`Ollama: ${ollamaBin ? '已安装' : '未安装'} | llama.cpp: ${llamacppBin ? '已安装' : '未安装'}`));
|
|
1958
|
+
const reg = localModel.list();
|
|
1959
|
+
if (reg.length > 0)
|
|
1960
|
+
chatLog.addSystem(theme.dim(`已注册模型: ${reg.map((m) => m.name).join(', ')}`));
|
|
1961
|
+
tui.requestRender();
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
// ── 本地模型 L2: model/local/switch ──
|
|
1965
|
+
case 'model/local/switch': {
|
|
1966
|
+
try {
|
|
1967
|
+
await loop.switchProvider('local');
|
|
1968
|
+
// 显式持久化 provider 选择(switchProvider 不再负责持久化)
|
|
1969
|
+
cfg.set('provider.active', 'local');
|
|
1970
|
+
cfg.save().catch(() => { });
|
|
1971
|
+
modelName = loop.getActiveProvider().getModel();
|
|
1972
|
+
providerTypeStart = loop.getActiveProvider().getProviderType();
|
|
1973
|
+
chatLog.addSystem(theme.success(`Switched to local (${loop.getActiveProvider().getProviderType()})`));
|
|
1974
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
|
|
1975
|
+
}
|
|
1976
|
+
catch (err) {
|
|
1977
|
+
chatLog.addSystem(theme.error(`Switch failed: ${err.message}`));
|
|
1978
|
+
}
|
|
1979
|
+
tui.requestRender();
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
// ── 本地模型 L2: model/local/register ──
|
|
1983
|
+
case 'model/local/register': {
|
|
1984
|
+
const found = await localModel.scanUnregistered();
|
|
1985
|
+
if (found.length === 0) {
|
|
1986
|
+
chatLog.addSystem(theme.dim('无新模型。已检查 models/ 目录 (GGUF) 和 ollama list。'));
|
|
1987
|
+
}
|
|
1988
|
+
else {
|
|
1989
|
+
for (const f of found) {
|
|
1990
|
+
localModel.registerModel({ name: f.name, modelFile: f.modelFile, backend: f.backend });
|
|
1991
|
+
chatLog.addSystem(theme.success(`Registered: ${f.name} (${f.backend ?? 'llama.cpp'})`));
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
tui.requestRender();
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
// ── 本地模型 L2: model/local/unregister ──
|
|
1998
|
+
case 'model/local/unregister': {
|
|
1999
|
+
if (!restArgs) {
|
|
2000
|
+
chatLog.addSystem(theme.warning('Usage: /model/local unregister <name>'));
|
|
2001
|
+
tui.requestRender();
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
chatLog.addSystem(theme.success(localModel.unregister(restArgs)));
|
|
2005
|
+
tui.requestRender();
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
// ── 本地模型 L2: model/local/detect ──
|
|
2009
|
+
case 'model/local/detect': {
|
|
2010
|
+
const { detectLocalBackend } = await import('../provider/local-config.js');
|
|
2011
|
+
const detected = await detectLocalBackend();
|
|
2012
|
+
const ollamaBin = localModel.checkOllama();
|
|
2013
|
+
const llamacppBin = localModel.checkLlamacpp();
|
|
2014
|
+
chatLog.addSystem(theme.fg('── 本地模型检测 ──'));
|
|
2015
|
+
chatLog.addSystem(detected ? theme.success(`运行中: ${detected.backend} — ${detected.baseUrl}`) : theme.dim('运行中: 无'));
|
|
2016
|
+
chatLog.addSystem(theme.dim(`Ollama: ${ollamaBin ? '已安装 (' + ollamaBin + ')' : '未安装'}`));
|
|
2017
|
+
chatLog.addSystem(theme.dim(`llama.cpp: ${llamacppBin ? '已安装 (' + llamacppBin + ')' : '未安装'}`));
|
|
2018
|
+
const reg = localModel.list();
|
|
2019
|
+
if (reg.length > 0)
|
|
2020
|
+
chatLog.addSystem(theme.dim(`已注册模型: ${reg.map((m) => m.name).join(', ')}`));
|
|
2021
|
+
tui.requestRender();
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
default: {
|
|
2025
|
+
// 在线模型: model/online/<provider>/<modelName|config>
|
|
2026
|
+
if (cmdPath.startsWith('model/online/')) {
|
|
2027
|
+
const onlineParts = cmdPath.split('/');
|
|
2028
|
+
if (onlineParts.length >= 4) {
|
|
2029
|
+
const provider = onlineParts[2];
|
|
2030
|
+
const sub = onlineParts[3];
|
|
2031
|
+
if (sub === 'config') {
|
|
2032
|
+
chatLog.addSystem(theme.accent(`Configure ${provider}: Use /context <tokens> to adjust context window`));
|
|
2033
|
+
tui.requestRender();
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
localModel.getBridge().stopAll().catch(() => { });
|
|
2037
|
+
try {
|
|
2038
|
+
cfg.set(`provider.${provider}.model`, sub);
|
|
2039
|
+
cfg.save().catch(() => { });
|
|
2040
|
+
await loop.switchProvider(provider);
|
|
2041
|
+
// 显式持久化 provider 选择(switchProvider 不再负责持久化)
|
|
2042
|
+
cfg.set('provider.active', provider);
|
|
2043
|
+
cfg.save().catch(() => { });
|
|
2044
|
+
modelName = loop.getActiveProvider().getModel();
|
|
2045
|
+
providerTypeStart = loop.getActiveProvider().getProviderType();
|
|
2046
|
+
chatLog.addSystem(theme.success(`Switched to ${provider}/${sub}`));
|
|
2047
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
|
|
2048
|
+
}
|
|
2049
|
+
catch (err) {
|
|
2050
|
+
chatLog.addSystem(theme.error(`Switch failed: ${err.message}`));
|
|
2051
|
+
}
|
|
2052
|
+
tui.requestRender();
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
// 本地模型 L1 直接切换: model/local_<modelName>
|
|
2057
|
+
if (cmdPath.startsWith('model/local_')) {
|
|
2058
|
+
const lmName = cmdPath.slice('model/local_'.length);
|
|
2059
|
+
const lm = localModel.list().find((m) => m.name === lmName);
|
|
2060
|
+
if (lm) {
|
|
2061
|
+
localModel.switch(lmName).then(async (info) => {
|
|
2062
|
+
if (info) {
|
|
2063
|
+
chatLog.addSystem(theme.success(`Local model ${lmName} started on port ${info.port}`));
|
|
2064
|
+
cfg.set('provider.local', {
|
|
2065
|
+
type: 'local',
|
|
2066
|
+
model: info.modelFile ?? lmName,
|
|
2067
|
+
baseUrl: info.baseUrl,
|
|
2068
|
+
});
|
|
2069
|
+
cfg.set('provider.local.modelKey', lmName);
|
|
2070
|
+
cfg.save().catch(() => { });
|
|
2071
|
+
try {
|
|
2072
|
+
await loop.switchProvider('local');
|
|
2073
|
+
// 显式持久化 provider 选择(switchProvider 不再负责持久化)
|
|
2074
|
+
cfg.set('provider.active', 'local');
|
|
2075
|
+
cfg.save().catch(() => { });
|
|
2076
|
+
modelName = loop.getActiveProvider().getModel();
|
|
2077
|
+
providerTypeStart = loop.getActiveProvider().getProviderType();
|
|
2078
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
|
|
2079
|
+
}
|
|
2080
|
+
catch (swErr) {
|
|
2081
|
+
chatLog.addSystem(theme.warning(`Switch to local: ${swErr.message}`));
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
else {
|
|
2085
|
+
chatLog.addSystem(theme.error(`Failed to start ${lmName}`));
|
|
2086
|
+
}
|
|
2087
|
+
tui.requestRender();
|
|
2088
|
+
}).catch((e) => {
|
|
2089
|
+
chatLog.addSystem(theme.error(`Failed: ${e.message}`));
|
|
2090
|
+
tui.requestRender();
|
|
2091
|
+
});
|
|
2092
|
+
tui.requestRender();
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
// 在线模型 L1 直接切换: model/current_online
|
|
2097
|
+
if (cmdPath === 'model/current_online') {
|
|
2098
|
+
chatLog.addSystem(theme.dim('Already using current online model'));
|
|
2099
|
+
tui.requestRender();
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
// ── 压缩器控制: compress/* ──
|
|
2103
|
+
if (cmdPath === 'compress/threshold' || cmdPath.startsWith('compress/threshold ')) {
|
|
2104
|
+
const val = parseFloat((restArgs || '').trim());
|
|
2105
|
+
if (isNaN(val) || val < 0 || val > 1) {
|
|
2106
|
+
chatLog.addSystem(theme.warning('Usage: /compress threshold <0.0-1.0>'));
|
|
2107
|
+
tui.requestRender();
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
cfg.set('context.compressThreshold', val);
|
|
2111
|
+
cfg.save().catch(() => { });
|
|
2112
|
+
chatLog.addSystem(theme.success('Compress threshold: ') + theme.fg(String(val)));
|
|
2113
|
+
tui.requestRender();
|
|
2114
|
+
updateTokenEstimate();
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
if (cmdPath === 'compress/emergency' || cmdPath.startsWith('compress/emergency ')) {
|
|
2118
|
+
const val = parseFloat((restArgs || '').trim());
|
|
2119
|
+
if (isNaN(val) || val < 0 || val > 1) {
|
|
2120
|
+
chatLog.addSystem(theme.warning('Usage: /compress emergency <0.0-1.0>'));
|
|
2121
|
+
tui.requestRender();
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
cfg.set('context.emergencyThreshold', val);
|
|
2125
|
+
cfg.save().catch(() => { });
|
|
2126
|
+
chatLog.addSystem(theme.success('Emergency threshold: ') + theme.fg(String(val)));
|
|
2127
|
+
tui.requestRender();
|
|
2128
|
+
updateTokenEstimate();
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
if (cmdPath === 'compress/depth' || cmdPath.startsWith('compress/depth ')) {
|
|
2132
|
+
const val = parseFloat((restArgs || '').trim());
|
|
2133
|
+
if (isNaN(val) || val < 0 || val > 1) {
|
|
2134
|
+
chatLog.addSystem(theme.warning('Usage: /compress depth <0.0-1.0>'));
|
|
2135
|
+
tui.requestRender();
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
cfg.set('context.compressDepth', val);
|
|
2139
|
+
cfg.save().catch(() => { });
|
|
2140
|
+
chatLog.addSystem(theme.success('Compress depth: ') + theme.fg(String(val)));
|
|
2141
|
+
tui.requestRender();
|
|
2142
|
+
updateTokenEstimate();
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
// ── 通道管理: channel/* ──
|
|
2146
|
+
if (cmdPath.startsWith('channel/')) {
|
|
2147
|
+
const modelRouter = loop.modelRouter;
|
|
2148
|
+
if (!modelRouter) {
|
|
2149
|
+
chatLog.addSystem(theme.warning('ModelRouter not available'));
|
|
2150
|
+
tui.requestRender();
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
const registry = modelRouter.getRegistry();
|
|
2154
|
+
if (cmdPath === 'channel/list') {
|
|
2155
|
+
const channels = registry.listChannels();
|
|
2156
|
+
const roles = registry.listRoles();
|
|
2157
|
+
if (channels.length === 0) {
|
|
2158
|
+
chatLog.addSystem(theme.dim('No model channels configured. All roles use main provider.'));
|
|
2159
|
+
}
|
|
2160
|
+
else {
|
|
2161
|
+
const lines = [theme.accent('=== Model Channels ===')];
|
|
2162
|
+
for (const ch of channels) {
|
|
2163
|
+
const chRoles = Object.entries(roles)
|
|
2164
|
+
.filter(([, cn]) => cn === ch.name)
|
|
2165
|
+
.map(([r]) => r);
|
|
2166
|
+
const roleStr = chRoles.length > 0 ? theme.dim(' → ') + theme.fg(chRoles.join(', ')) : '';
|
|
2167
|
+
lines.push(theme.fg(` ${ch.name}`) + theme.dim(`: ${ch.provider}/${ch.model || 'default'}`) + roleStr);
|
|
2168
|
+
}
|
|
2169
|
+
lines.push('');
|
|
2170
|
+
lines.push(theme.accent('=== Role Mappings ==='));
|
|
2171
|
+
for (const [role, channel] of Object.entries(roles)) {
|
|
2172
|
+
lines.push(theme.dim(` ${role}`) + ' → ' + theme.fg(String(channel)));
|
|
2173
|
+
}
|
|
2174
|
+
chatLog.addSystem(lines.join('\n'));
|
|
2175
|
+
}
|
|
2176
|
+
tui.requestRender();
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
if (cmdPath === 'channel/add') {
|
|
2180
|
+
if (!restArgs) {
|
|
2181
|
+
chatLog.addSystem(theme.warning('Usage: /channel add <name> [provider] [model]'));
|
|
2182
|
+
tui.requestRender();
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const parts = restArgs.split(/\s+/).filter(Boolean);
|
|
2186
|
+
const [name, provider, model] = parts;
|
|
2187
|
+
try {
|
|
2188
|
+
registry.upsertChannel(name, { provider, model });
|
|
2189
|
+
const info = registry.getChannelInfo(name);
|
|
2190
|
+
chatLog.addSystem(theme.success(`Channel "${name}" added (${info?.provider}${model ? '/' + model : ''})`));
|
|
2191
|
+
}
|
|
2192
|
+
catch (e) {
|
|
2193
|
+
chatLog.addSystem(theme.error(`Failed: ${e.message}`));
|
|
2194
|
+
}
|
|
2195
|
+
tui.requestRender();
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
if (cmdPath === 'channel/remove') {
|
|
2199
|
+
if (!restArgs) {
|
|
2200
|
+
chatLog.addSystem(theme.warning('Usage: /channel remove <name>'));
|
|
2201
|
+
tui.requestRender();
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
const name = restArgs.trim();
|
|
2205
|
+
try {
|
|
2206
|
+
registry.removeChannel(name);
|
|
2207
|
+
chatLog.addSystem(theme.success(`Channel "${name}" removed`));
|
|
2208
|
+
}
|
|
2209
|
+
catch (e) {
|
|
2210
|
+
chatLog.addSystem(theme.error(`Failed: ${e.message}`));
|
|
2211
|
+
}
|
|
2212
|
+
tui.requestRender();
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
if (cmdPath === 'channel/role') {
|
|
2216
|
+
if (!restArgs) {
|
|
2217
|
+
chatLog.addSystem(theme.warning('Usage: /channel role <role> <channel>'));
|
|
2218
|
+
tui.requestRender();
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
const parts = restArgs.split(/\s+/).filter(Boolean);
|
|
2222
|
+
if (parts.length < 2) {
|
|
2223
|
+
chatLog.addSystem(theme.warning('Usage: /channel role <role> <channel>'));
|
|
2224
|
+
tui.requestRender();
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
const [role, channel] = parts;
|
|
2228
|
+
try {
|
|
2229
|
+
registry.setRoleMapping(role, channel);
|
|
2230
|
+
chatLog.addSystem(theme.success(`Role "${role}" → channel "${channel}"`));
|
|
2231
|
+
}
|
|
2232
|
+
catch (e) {
|
|
2233
|
+
chatLog.addSystem(theme.error(`Failed: ${e.message}`));
|
|
2234
|
+
}
|
|
2235
|
+
tui.requestRender();
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
// ── 通道子命令: channel/<name>/info | /model | /reset ──
|
|
2239
|
+
const chMatch = cmdPath.match(/^channel\/([^/]+)\/(info|model|reset)$/);
|
|
2240
|
+
if (chMatch) {
|
|
2241
|
+
const chName = chMatch[1];
|
|
2242
|
+
const action = chMatch[2];
|
|
2243
|
+
if (action === 'info') {
|
|
2244
|
+
const info = registry.getChannelInfo(chName);
|
|
2245
|
+
if (!info) {
|
|
2246
|
+
chatLog.addSystem(theme.warning(`Channel "${chName}" not found`));
|
|
2247
|
+
}
|
|
2248
|
+
else {
|
|
2249
|
+
const lines = [theme.accent(`=== Channel: ${info.name}${info.isMain ? ' (main)' : ''} ===`)];
|
|
2250
|
+
lines.push(theme.fg(' Provider: ') + info.provider);
|
|
2251
|
+
lines.push(theme.fg(' Model: ') + info.model);
|
|
2252
|
+
lines.push(theme.fg(' Type: ') + info.providerType);
|
|
2253
|
+
if (info.description)
|
|
2254
|
+
lines.push(theme.dim(' Desc: ') + info.description);
|
|
2255
|
+
if (info.roles.length > 0)
|
|
2256
|
+
lines.push(theme.fg(' Roles: ') + info.roles.join(', '));
|
|
2257
|
+
chatLog.addSystem(lines.join('\n'));
|
|
2258
|
+
}
|
|
2259
|
+
tui.requestRender();
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
if (action === 'model') {
|
|
2263
|
+
const parts = (restArgs || '').split(/\s+/).filter(Boolean);
|
|
2264
|
+
if (parts.length < 1) {
|
|
2265
|
+
chatLog.addSystem(theme.warning(`Usage: /channel/${chName}/model <provider> [model-name]`));
|
|
2266
|
+
tui.requestRender();
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
const provider = parts[0];
|
|
2270
|
+
const model = parts[1] || undefined;
|
|
2271
|
+
try {
|
|
2272
|
+
registry.setChannelModel(chName, provider, model);
|
|
2273
|
+
const updated = registry.getChannelInfo(chName);
|
|
2274
|
+
chatLog.addSystem(theme.success(`Channel "${chName}" model set → ${updated?.provider}/${updated?.model} (runtime only, not persisted)`));
|
|
2275
|
+
}
|
|
2276
|
+
catch (e) {
|
|
2277
|
+
chatLog.addSystem(theme.error(`Failed: ${e.message}`));
|
|
2278
|
+
}
|
|
2279
|
+
tui.requestRender();
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
if (action === 'reset') {
|
|
2283
|
+
try {
|
|
2284
|
+
registry.resetChannelModel(chName);
|
|
2285
|
+
const info = registry.getChannelInfo(chName);
|
|
2286
|
+
chatLog.addSystem(theme.success(`Channel "${chName}" reset → ${info?.provider}/${info?.model}`));
|
|
2287
|
+
}
|
|
2288
|
+
catch (e) {
|
|
2289
|
+
chatLog.addSystem(theme.error(`Failed: ${e.message}`));
|
|
2290
|
+
}
|
|
2291
|
+
tui.requestRender();
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
// 通用渠道 TUI 命令分发:cmdPath 如 "clawbot/login" → clawbot 渠道
|
|
2297
|
+
{
|
|
2298
|
+
const slashIdx = cmdPath.indexOf('/');
|
|
2299
|
+
if (slashIdx > 0) {
|
|
2300
|
+
const chId = cmdPath.slice(0, slashIdx);
|
|
2301
|
+
const chState = channelManager.get(chId);
|
|
2302
|
+
if (chState?.handler.handleTuiCommand) {
|
|
2303
|
+
const result = await chState.handler.handleTuiCommand(cmdPath, restArgs);
|
|
2304
|
+
if (result !== null) {
|
|
2305
|
+
chatLog.addSystem(result);
|
|
2306
|
+
tui.requestRender();
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
// 通用 handler 路由:查找命令定义的 handler 字段
|
|
2313
|
+
const cmdDef = CommandRegistry.getInstance().find(cmdPath);
|
|
2314
|
+
if (cmdDef?.handler) {
|
|
2315
|
+
resolveHandler(cmdDef.handler, restArgs, localModel, chatLog, theme, tui);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
chatLog.addSystem(theme.warning(`Unknown sub-command: /${cmdPath}`));
|
|
2319
|
+
tui.requestRender();
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
// Local slash commands
|
|
2324
|
+
if (input === '/help') {
|
|
2325
|
+
const grouped = getCommandsByCategory();
|
|
2326
|
+
const BOX_H_L = '\u2500';
|
|
2327
|
+
chatLog.addSystem(theme.fg('\u256d' + BOX_H_L.repeat(56) + '\u256e'));
|
|
2328
|
+
chatLog.addSystem(theme.fg('\u2502 Available Commands'));
|
|
2329
|
+
chatLog.addSystem(theme.fg('\u2570' + BOX_H_L.repeat(56) + '\u256f'));
|
|
2330
|
+
chatLog.addSystem('');
|
|
2331
|
+
for (const [category, cmds] of grouped) {
|
|
2332
|
+
const catLabel = getCategoryLabel(category);
|
|
2333
|
+
chatLog.addSystem(theme.accent('\u2500\u2500 ' + catLabel + ' \u2500\u2500'));
|
|
2334
|
+
for (const c of cmds) {
|
|
2335
|
+
if (c.deprecated) {
|
|
2336
|
+
chatLog.addSystem(' ' + theme.dim(c.icon) + ' ' + theme.dim('/' + c.name) +
|
|
2337
|
+
theme.dim(' ' + c.description) + theme.warning(' [deprecated]'));
|
|
2338
|
+
}
|
|
2339
|
+
else {
|
|
2340
|
+
const args = c.args ? theme.dim(' ' + c.args) : '';
|
|
2341
|
+
const hasChildren = c.children ? theme.accent(' \u25B8') : '';
|
|
2342
|
+
chatLog.addSystem(' ' + theme.accent(c.icon) + ' ' + theme.fg('/' + c.name) +
|
|
2343
|
+
hasChildren + args + theme.dim(' ' + c.description));
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
chatLog.addSystem('');
|
|
2347
|
+
}
|
|
2348
|
+
tui.requestRender();
|
|
2349
|
+
updateTokenEstimate();
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
if (input === '/clear') {
|
|
2353
|
+
chatLog.clearAll();
|
|
2354
|
+
tui.requestRender();
|
|
2355
|
+
updateTokenEstimate();
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
if (input === '/collapse') {
|
|
2359
|
+
const expanded = chatLog.toggleToolsExpanded();
|
|
2360
|
+
chatLog.addSystem(theme.dim('Tool outputs ') + theme.fg(expanded ? 'expanded' : 'collapsed'));
|
|
2361
|
+
tui.requestRender();
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
if (input === '/orchestrator on') {
|
|
2365
|
+
if (loop.bypassManager) {
|
|
2366
|
+
await loop.bypassManager.activateAgent('orchestrator');
|
|
2367
|
+
const { RuntimeConfigCenter } = await import('../runtime/config-center.js');
|
|
2368
|
+
RuntimeConfigCenter.getInstance().set('bypass.orchestratorEnabled', true);
|
|
2369
|
+
chatLog.addSystem(theme.success('上下文编排旁路Agent 已开启'));
|
|
2370
|
+
}
|
|
2371
|
+
else {
|
|
2372
|
+
chatLog.addSystem(theme.warning('旁路Agent 管理器未初始化'));
|
|
2373
|
+
}
|
|
2374
|
+
tui.requestRender();
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
if (input === '/orchestrator off') {
|
|
2378
|
+
if (loop.bypassManager) {
|
|
2379
|
+
await loop.bypassManager.deactivateAgent('orchestrator');
|
|
2380
|
+
const { RuntimeConfigCenter } = await import('../runtime/config-center.js');
|
|
2381
|
+
RuntimeConfigCenter.getInstance().set('bypass.orchestratorEnabled', false);
|
|
2382
|
+
chatLog.addSystem(theme.dim('上下文编排旁路Agent 已关闭'));
|
|
2383
|
+
}
|
|
2384
|
+
else {
|
|
2385
|
+
chatLog.addSystem(theme.warning('旁路Agent 管理器未初始化'));
|
|
2386
|
+
}
|
|
2387
|
+
tui.requestRender();
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
if (input === '/default-mode companion') {
|
|
2391
|
+
const { RuntimeConfigCenter } = await import('../runtime/config-center.js');
|
|
2392
|
+
const configCenter = RuntimeConfigCenter.getInstance();
|
|
2393
|
+
configCenter.set('startup.defaultMode', 'companion');
|
|
2394
|
+
await configCenter.save();
|
|
2395
|
+
chatLog.addSystem(theme.success('启动默认模式已设为 陪伴模式 💫(下次启动生效)'));
|
|
2396
|
+
tui.requestRender();
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
if (input === '/default-mode normal') {
|
|
2400
|
+
const { RuntimeConfigCenter } = await import('../runtime/config-center.js');
|
|
2401
|
+
const configCenter = RuntimeConfigCenter.getInstance();
|
|
2402
|
+
configCenter.set('startup.defaultMode', 'normal');
|
|
2403
|
+
await configCenter.save();
|
|
2404
|
+
chatLog.addSystem(theme.dim('启动默认模式已设为 普通模式 💻(下次启动生效)'));
|
|
2405
|
+
tui.requestRender();
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
if (input === '/zone4 on') {
|
|
2409
|
+
knowledgeBase.setZone4Enabled(true);
|
|
2410
|
+
contextComposer.activeConditions.add('zone4_enabled');
|
|
2411
|
+
const { RuntimeConfigCenter: RCC } = await import('../runtime/config-center.js');
|
|
2412
|
+
RCC.getInstance().set('kb.zone4', true);
|
|
2413
|
+
chatLog.addSystem(theme.success('Zone 4 已开启'));
|
|
2414
|
+
tui.requestRender();
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
if (input === '/zone4 off') {
|
|
2418
|
+
knowledgeBase.setZone4Enabled(false);
|
|
2419
|
+
contextComposer.activeConditions.delete('zone4_enabled');
|
|
2420
|
+
const { RuntimeConfigCenter: RCC1 } = await import('../runtime/config-center.js');
|
|
2421
|
+
RCC1.getInstance().set('kb.zone4', false);
|
|
2422
|
+
chatLog.addSystem(theme.dim('Zone 4 已关闭(知识库同步停用)'));
|
|
2423
|
+
tui.requestRender();
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
if (input === '/kb on') {
|
|
2427
|
+
if (!knowledgeBase.zone4Enabled) {
|
|
2428
|
+
knowledgeBase.setZone4Enabled(true);
|
|
2429
|
+
contextComposer.activeConditions.add('zone4_enabled');
|
|
2430
|
+
const { RuntimeConfigCenter: RCC2 } = await import('../runtime/config-center.js');
|
|
2431
|
+
RCC2.getInstance().set('kb.zone4', true);
|
|
2432
|
+
chatLog.addSystem(theme.dim('Zone 4 已同步开启'));
|
|
2433
|
+
}
|
|
2434
|
+
knowledgeBase.enable();
|
|
2435
|
+
const { RuntimeConfigCenter: RCC3 } = await import('../runtime/config-center.js');
|
|
2436
|
+
RCC3.getInstance().set('kb.enabled', true);
|
|
2437
|
+
chatLog.addSystem(theme.success('知识库已开启 — Zone 4 将注入检索结果'));
|
|
2438
|
+
tui.requestRender();
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
if (input === '/kb off') {
|
|
2442
|
+
knowledgeBase.disable();
|
|
2443
|
+
const { RuntimeConfigCenter: RCC4 } = await import('../runtime/config-center.js');
|
|
2444
|
+
RCC4.getInstance().set('kb.enabled', false);
|
|
2445
|
+
chatLog.addSystem(theme.dim('知识库已关闭'));
|
|
2446
|
+
tui.requestRender();
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
if (input === '/precise on') {
|
|
2450
|
+
const { PreciseStrategy } = await import('../context/precision/index.js');
|
|
2451
|
+
if (!originalSessionDir)
|
|
2452
|
+
originalSessionDir = sessionDir;
|
|
2453
|
+
// 复用已有 precise session(保留积累的关键词和摘要)
|
|
2454
|
+
const existingPrecise = (await sessionManager.list())
|
|
2455
|
+
.find((s) => s.type === 'precise');
|
|
2456
|
+
const newSession = existingPrecise ?? await sessionManager.create('precise');
|
|
2457
|
+
const newSessionDir = sessionManager.getSessionDir(newSession.id);
|
|
2458
|
+
const precise = new PreciseStrategy(newSessionDir);
|
|
2459
|
+
loop.composeStrategy = precise;
|
|
2460
|
+
await loop.switchSession(newSessionDir);
|
|
2461
|
+
sessionDir = newSessionDir;
|
|
2462
|
+
preciseModeActive = true;
|
|
2463
|
+
const label = existingPrecise ? '恢复已有' : '新建';
|
|
2464
|
+
chatLog.addSystem(theme.success(`精确模式已开启 — ${label} session: ${newSession.id}`));
|
|
2465
|
+
chatLog.clearAll();
|
|
2466
|
+
tui.requestRender();
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
if (input === '/precise off') {
|
|
2470
|
+
if (!preciseModeActive) {
|
|
2471
|
+
chatLog.addSystem(theme.dim('精确模式未开启'));
|
|
2472
|
+
tui.requestRender();
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
const { DefaultStrategy } = await import('../context/precision/index.js');
|
|
2476
|
+
loop.composeStrategy = new DefaultStrategy();
|
|
2477
|
+
if (originalSessionDir) {
|
|
2478
|
+
await loop.switchSession(originalSessionDir);
|
|
2479
|
+
sessionDir = originalSessionDir;
|
|
2480
|
+
originalSessionDir = null;
|
|
2481
|
+
chatLog.clearAll();
|
|
2482
|
+
replayEvents(chatLog, sessionDir);
|
|
2483
|
+
}
|
|
2484
|
+
preciseModeActive = false;
|
|
2485
|
+
chatLog.addSystem(theme.dim('精确模式已关闭,已恢复原始 session'));
|
|
2486
|
+
tui.requestRender();
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
if (input === '/exit') {
|
|
2490
|
+
chatLog.addSystem(theme.dim('Goodbye.'));
|
|
2491
|
+
tui.requestRender();
|
|
2492
|
+
await channelManager.stopAll();
|
|
2493
|
+
await loop.shutdown();
|
|
2494
|
+
await supervisor.shutdownAll();
|
|
2495
|
+
tui.stop();
|
|
2496
|
+
process.exit(0);
|
|
2497
|
+
}
|
|
2498
|
+
const cfg = RuntimeConfigCenter.getInstance();
|
|
2499
|
+
if (input === '/status') {
|
|
2500
|
+
const s = cfg.getAll();
|
|
2501
|
+
const items = [
|
|
2502
|
+
['Provider', s.provider.active],
|
|
2503
|
+
['Model', s.provider[s.provider.active]],
|
|
2504
|
+
['Active Workflow', 'none'],
|
|
2505
|
+
['Max Context', `${s.session.maxContext.toLocaleString()} tokens`],
|
|
2506
|
+
['Max Turns', s.session.maxTurns],
|
|
2507
|
+
['Compress Threshold', s.context.compressThreshold.toFixed(2)],
|
|
2508
|
+
['Confirmation', s.safety.requireConfirmation ? 'on' : 'off'],
|
|
2509
|
+
['Scavenge', s.repair.scavenge.enabled ? 'on' : 'off'],
|
|
2510
|
+
['Storm', s.repair.storm.enabled ? 'on' : 'off'],
|
|
2511
|
+
['Storm Window', s.repair.storm.windowSize],
|
|
2512
|
+
['Storm Threshold', s.repair.storm.threshold],
|
|
2513
|
+
['Log Level', s.logging.level],
|
|
2514
|
+
];
|
|
2515
|
+
chatLog.addSystem(theme.accent('\u2500\u2500 Status \u2500\u2500'));
|
|
2516
|
+
for (const [label, value] of items) {
|
|
2517
|
+
let display;
|
|
2518
|
+
if (typeof value === 'object' && value !== null && 'model' in value) {
|
|
2519
|
+
display = String(value.model);
|
|
2520
|
+
}
|
|
2521
|
+
else {
|
|
2522
|
+
display = String(value ?? 'N/A');
|
|
2523
|
+
}
|
|
2524
|
+
chatLog.addSystem(' ' + theme.fg(label) + theme.dim(': ') + theme.fg(display));
|
|
2525
|
+
}
|
|
2526
|
+
const sources = loop.getModelSources();
|
|
2527
|
+
if (sources) {
|
|
2528
|
+
chatLog.addSystem('');
|
|
2529
|
+
chatLog.addSystem(theme.accent('\u2500\u2500 Model Routing \u2500\u2500'));
|
|
2530
|
+
const labels = {
|
|
2531
|
+
assessment: '评估',
|
|
2532
|
+
planning: '规划',
|
|
2533
|
+
compression: '压缩',
|
|
2534
|
+
};
|
|
2535
|
+
const sourceColor = (src) => src === 'local' ? theme.success(src) : theme.accent(src);
|
|
2536
|
+
for (const [role, source] of Object.entries(sources)) {
|
|
2537
|
+
const label = labels[role] ?? role;
|
|
2538
|
+
chatLog.addSystem(' ' + theme.fg(label) + theme.dim(': ') + sourceColor(String(source)));
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
chatLog.addSystem('');
|
|
2542
|
+
tui.requestRender();
|
|
2543
|
+
updateTokenEstimate();
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
// ── /context <tokens> ──
|
|
2547
|
+
if (input.startsWith('/context ')) {
|
|
2548
|
+
const tokens = parseInt(input.slice(9).trim(), 10);
|
|
2549
|
+
const activeP = loop.getActiveProvider();
|
|
2550
|
+
const modelCtxWindow = getModelContextWindow(activeP.getProviderType(), activeP.getModel());
|
|
2551
|
+
const upper = modelCtxWindow; // 当前模型支持的最大上下文
|
|
2552
|
+
if (isNaN(tokens) || tokens < 1 || tokens > upper) {
|
|
2553
|
+
chatLog.addSystem(theme.warning(`Usage: /context <1-${upper.toLocaleString()}>`) + theme.dim(` (model: ${activeP.getModel()})`));
|
|
2554
|
+
tui.requestRender();
|
|
2555
|
+
updateTokenEstimate();
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
cfg.set('session.maxContext', tokens);
|
|
2559
|
+
cfg.save().catch(() => { });
|
|
2560
|
+
chatLog.addSystem(theme.success('Max context set to ') + theme.fg(tokens.toLocaleString() + ' tokens'));
|
|
2561
|
+
refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
|
|
2562
|
+
updateTokenEstimate();
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
// ── /turns <n> ──
|
|
2566
|
+
if (input.startsWith('/turns ')) {
|
|
2567
|
+
const n = parseInt(input.slice(7).trim(), 10);
|
|
2568
|
+
if (isNaN(n) || n < 1 || n > 100) {
|
|
2569
|
+
chatLog.addSystem(theme.warning('Usage: /turns <1-100>'));
|
|
2570
|
+
tui.requestRender();
|
|
2571
|
+
updateTokenEstimate();
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
cfg.set('session.maxTurns', n);
|
|
2575
|
+
cfg.save().catch(() => { });
|
|
2576
|
+
chatLog.addSystem(theme.success('Max turns set to ') + theme.fg(String(n)));
|
|
2577
|
+
tui.requestRender();
|
|
2578
|
+
updateTokenEstimate();
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
// ── /threshold <0.0-1.0> ──
|
|
2582
|
+
// ── /compress sub-commands (handled via handleSlashSubCommand) ──
|
|
2583
|
+
// ── /threshold <0.0-1.0> (deprecated: use /compress threshold) ──
|
|
2584
|
+
if (input.startsWith('/threshold ')) {
|
|
2585
|
+
const val = parseFloat(input.slice(11).trim());
|
|
2586
|
+
if (isNaN(val) || val < 0 || val > 1) {
|
|
2587
|
+
chatLog.addSystem(theme.warning('Usage: /threshold <0.0-1.0>'));
|
|
2588
|
+
tui.requestRender();
|
|
2589
|
+
updateTokenEstimate();
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
cfg.set('context.compressThreshold', val);
|
|
2593
|
+
cfg.save().catch(() => { });
|
|
2594
|
+
chatLog.addSystem(theme.success('Compression threshold set to ') + theme.fg(val.toFixed(2)));
|
|
2595
|
+
tui.requestRender();
|
|
2596
|
+
updateTokenEstimate();
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
// ── /confirm on|off ──
|
|
2600
|
+
if (input.startsWith('/confirm ')) {
|
|
2601
|
+
const arg = input.slice(9).trim();
|
|
2602
|
+
if (arg === 'on') {
|
|
2603
|
+
cfg.set('safety.requireConfirmation', true);
|
|
2604
|
+
cfg.save().catch(() => { });
|
|
2605
|
+
chatLog.addSystem(theme.success('Tool confirmation ') + theme.fg('enabled'));
|
|
2606
|
+
}
|
|
2607
|
+
else if (arg === 'off') {
|
|
2608
|
+
cfg.set('safety.requireConfirmation', false);
|
|
2609
|
+
cfg.save().catch(() => { });
|
|
2610
|
+
chatLog.addSystem(theme.warning('Tool confirmation ') + theme.fg('disabled'));
|
|
2611
|
+
}
|
|
2612
|
+
else {
|
|
2613
|
+
chatLog.addSystem(theme.warning('Usage: /confirm <on|off>'));
|
|
2614
|
+
}
|
|
2615
|
+
tui.requestRender();
|
|
2616
|
+
updateTokenEstimate();
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
// ── /log <level> ──
|
|
2620
|
+
if (input.startsWith('/log ')) {
|
|
2621
|
+
const arg = input.slice(5).trim();
|
|
2622
|
+
const validLevels = ['debug', 'info', 'warn', 'error', 'off'];
|
|
2623
|
+
if (validLevels.includes(arg)) {
|
|
2624
|
+
cfg.set('logging.level', arg);
|
|
2625
|
+
cfg.save().catch(() => { });
|
|
2626
|
+
chatLog.addSystem(theme.success('Log level set to ') + theme.fg(arg));
|
|
2627
|
+
}
|
|
2628
|
+
else {
|
|
2629
|
+
chatLog.addSystem(theme.warning('Usage: /log <debug|info|warn|error|off>'));
|
|
2630
|
+
}
|
|
2631
|
+
tui.requestRender();
|
|
2632
|
+
updateTokenEstimate();
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
// ── /scavenge on|off ──
|
|
2636
|
+
if (input.startsWith('/scavenge ')) {
|
|
2637
|
+
const arg = input.slice(10).trim();
|
|
2638
|
+
if (arg === 'on') {
|
|
2639
|
+
cfg.set('repair.scavenge.enabled', true);
|
|
2640
|
+
cfg.save().catch(() => { });
|
|
2641
|
+
chatLog.addSystem(theme.success('Scavenge repair ') + theme.fg('enabled'));
|
|
2642
|
+
}
|
|
2643
|
+
else if (arg === 'off') {
|
|
2644
|
+
cfg.set('repair.scavenge.enabled', false);
|
|
2645
|
+
cfg.save().catch(() => { });
|
|
2646
|
+
chatLog.addSystem(theme.warning('Scavenge repair ') + theme.fg('disabled'));
|
|
2647
|
+
}
|
|
2648
|
+
else {
|
|
2649
|
+
chatLog.addSystem(theme.warning('Usage: /scavenge <on|off>'));
|
|
2650
|
+
}
|
|
2651
|
+
tui.requestRender();
|
|
2652
|
+
updateTokenEstimate();
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
// ── /storm on|off ──
|
|
2656
|
+
if (input.startsWith('/storm ')) {
|
|
2657
|
+
const arg = input.slice(7).trim();
|
|
2658
|
+
if (arg === 'on') {
|
|
2659
|
+
cfg.set('repair.storm.enabled', true);
|
|
2660
|
+
cfg.save().catch(() => { });
|
|
2661
|
+
chatLog.addSystem(theme.success('Storm protection ') + theme.fg('enabled'));
|
|
2662
|
+
}
|
|
2663
|
+
else if (arg === 'off') {
|
|
2664
|
+
cfg.set('repair.storm.enabled', false);
|
|
2665
|
+
cfg.save().catch(() => { });
|
|
2666
|
+
chatLog.addSystem(theme.warning('Storm protection ') + theme.fg('disabled'));
|
|
2667
|
+
}
|
|
2668
|
+
else {
|
|
2669
|
+
chatLog.addSystem(theme.warning('Usage: /storm <on|off>'));
|
|
2670
|
+
}
|
|
2671
|
+
tui.requestRender();
|
|
2672
|
+
updateTokenEstimate();
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
// ── /storm-win <2-20> ──
|
|
2676
|
+
if (input.startsWith('/storm-win ')) {
|
|
2677
|
+
const n = parseInt(input.slice(11).trim(), 10);
|
|
2678
|
+
if (isNaN(n) || n < 2 || n > 20) {
|
|
2679
|
+
chatLog.addSystem(theme.warning('Usage: /storm-win <2-20>'));
|
|
2680
|
+
tui.requestRender();
|
|
2681
|
+
updateTokenEstimate();
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
cfg.set('repair.storm.windowSize', n);
|
|
2685
|
+
cfg.save().catch(() => { });
|
|
2686
|
+
chatLog.addSystem(theme.success('Storm window size set to ') + theme.fg(String(n)));
|
|
2687
|
+
tui.requestRender();
|
|
2688
|
+
updateTokenEstimate();
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
// ── /storm-th <1-10> ──
|
|
2692
|
+
if (input.startsWith('/storm-th ')) {
|
|
2693
|
+
const n = parseInt(input.slice(10).trim(), 10);
|
|
2694
|
+
if (isNaN(n) || n < 1 || n > 10) {
|
|
2695
|
+
chatLog.addSystem(theme.warning('Usage: /storm-th <1-10>'));
|
|
2696
|
+
tui.requestRender();
|
|
2697
|
+
updateTokenEstimate();
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2700
|
+
cfg.set('repair.storm.threshold', n);
|
|
2701
|
+
cfg.save().catch(() => { });
|
|
2702
|
+
chatLog.addSystem(theme.success('Storm threshold set to ') + theme.fg(String(n)));
|
|
2703
|
+
tui.requestRender();
|
|
2704
|
+
updateTokenEstimate();
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
// ── /schedule ──
|
|
2708
|
+
if (input === '/schedule') {
|
|
2709
|
+
const scheduler = loop.getScheduler();
|
|
2710
|
+
if (!scheduler) {
|
|
2711
|
+
chatLog.addSystem(theme.dim('No scheduler configured'));
|
|
2712
|
+
}
|
|
2713
|
+
else {
|
|
2714
|
+
const allTasks = scheduler.getTasks();
|
|
2715
|
+
const activeTasks = allTasks.filter((t) => t.enabled && t.nextRunAt !== null);
|
|
2716
|
+
const disabledCount = allTasks.length - activeTasks.length;
|
|
2717
|
+
if (activeTasks.length === 0) {
|
|
2718
|
+
chatLog.addSystem(theme.dim('No active scheduled tasks'));
|
|
2719
|
+
if (disabledCount > 0) {
|
|
2720
|
+
chatLog.addSystem(theme.dim(`(+${disabledCount} disabled/expired tasks)`));
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
else {
|
|
2724
|
+
chatLog.addSystem(theme.accent(`Scheduled Tasks (${activeTasks.length} active)`));
|
|
2725
|
+
for (const task of activeTasks) {
|
|
2726
|
+
const nextRun = task.nextRunAt
|
|
2727
|
+
? new Date(task.nextRunAt).toLocaleString()
|
|
2728
|
+
: 'N/A';
|
|
2729
|
+
const typeLabel = theme.dim(`[${task.scheduleType}]`);
|
|
2730
|
+
const runInfo = theme.dim(` | runs: ${task.runCount}`);
|
|
2731
|
+
chatLog.addSystem(' ' +
|
|
2732
|
+
theme.fg(task.name) +
|
|
2733
|
+
' ' +
|
|
2734
|
+
typeLabel +
|
|
2735
|
+
theme.dim(' | next: ') +
|
|
2736
|
+
theme.fg(nextRun) +
|
|
2737
|
+
runInfo);
|
|
2738
|
+
}
|
|
2739
|
+
if (disabledCount > 0) {
|
|
2740
|
+
chatLog.addSystem(theme.dim(`(+${disabledCount} disabled/expired tasks not shown)`));
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
tui.requestRender();
|
|
2745
|
+
updateTokenEstimate();
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
// ── /schedule-add <name> <time> ──
|
|
2749
|
+
if (input.startsWith('/schedule-add ')) {
|
|
2750
|
+
const args = input.slice(14).trim();
|
|
2751
|
+
const spaceIdx = args.lastIndexOf(' ');
|
|
2752
|
+
if (spaceIdx < 0) {
|
|
2753
|
+
chatLog.addSystem(theme.warning('Usage: /schedule-add <name> <HH:mm>'));
|
|
2754
|
+
tui.requestRender();
|
|
2755
|
+
updateTokenEstimate();
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
const name = args.slice(0, spaceIdx).trim();
|
|
2759
|
+
const time = args.slice(spaceIdx + 1).trim();
|
|
2760
|
+
if (!/^\d{2}:\d{2}$/.test(time)) {
|
|
2761
|
+
chatLog.addSystem(theme.warning('Time must be in HH:mm format (e.g., 02:00)'));
|
|
2762
|
+
tui.requestRender();
|
|
2763
|
+
updateTokenEstimate();
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
await loop.addScheduledTask(name, 'daily', time);
|
|
2767
|
+
chatLog.addSystem(theme.success('Task added: ') +
|
|
2768
|
+
theme.fg(name) +
|
|
2769
|
+
theme.dim(' at ') +
|
|
2770
|
+
theme.fg(time) +
|
|
2771
|
+
theme.dim(' daily'));
|
|
2772
|
+
tui.requestRender();
|
|
2773
|
+
updateTokenEstimate();
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2776
|
+
// ── /restart ──
|
|
2777
|
+
if (input === '/restart') {
|
|
2778
|
+
chatLog.addSystem(theme.accent('Restarting...'));
|
|
2779
|
+
tui.requestRender();
|
|
2780
|
+
const fsSync = (await import('node:fs')).default;
|
|
2781
|
+
const restartFile = path.join(os.homedir(), '.agent', '.restart-session');
|
|
2782
|
+
fsSync.mkdirSync(path.dirname(restartFile), { recursive: true });
|
|
2783
|
+
fsSync.writeFileSync(restartFile, path.basename(sessionDir), 'utf-8');
|
|
2784
|
+
setTimeout(() => process.exit(42), 200);
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
// ── /new ──
|
|
2788
|
+
if (input === '/new') {
|
|
2789
|
+
chatLog.addSystem(theme.accent('Starting new session...'));
|
|
2790
|
+
tui.requestRender();
|
|
2791
|
+
const fsSync = (await import('node:fs')).default;
|
|
2792
|
+
const newFlag = path.join(os.homedir(), '.agent', '.new-session');
|
|
2793
|
+
fsSync.mkdirSync(path.dirname(newFlag), { recursive: true });
|
|
2794
|
+
fsSync.writeFileSync(newFlag, 'true', 'utf-8');
|
|
2795
|
+
setTimeout(() => process.exit(42), 200);
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
// \u2500\u2500 Message Queue Integration \u2500\u2500
|
|
2799
|
+
const mode = MessageQueue.detectMode(input);
|
|
2800
|
+
const cleanText = MessageQueue.stripMarkers(input);
|
|
2801
|
+
if (isProcessing || loop.pendingTaskName) {
|
|
2802
|
+
messageQueue.enqueue(cleanText, mode);
|
|
2803
|
+
if (mode === QueueMessageMode.Insert) {
|
|
2804
|
+
chatLog.addSystem(theme.warning(`\u23e9 Inserting: ${truncateMsg(cleanText)}`));
|
|
2805
|
+
loop.interrupt();
|
|
2806
|
+
}
|
|
2807
|
+
else {
|
|
2808
|
+
chatLog.addSystem(theme.dim(`\u23f3 Queued (#${messageQueue.size}): ${truncateMsg(cleanText)}`));
|
|
2809
|
+
}
|
|
2810
|
+
updateTokenEstimate();
|
|
2811
|
+
tui.requestRender();
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
await processBatch(cleanText);
|
|
2815
|
+
}
|
|
2816
|
+
// ── Editor callbacks ──
|
|
2817
|
+
editor.onSubmit = (text) => {
|
|
2818
|
+
void handleInput(text);
|
|
2819
|
+
};
|
|
2820
|
+
editor.onCtrlC = () => {
|
|
2821
|
+
editor.setText('');
|
|
2822
|
+
if (isProcessing) {
|
|
2823
|
+
loop.interrupt();
|
|
2824
|
+
}
|
|
2825
|
+
else {
|
|
2826
|
+
chatLog.addSystem(theme.warning('Ctrl+C \u2014 press again to exit'));
|
|
2827
|
+
}
|
|
2828
|
+
tui.requestRender();
|
|
2829
|
+
};
|
|
2830
|
+
editor.onEscape = () => {
|
|
2831
|
+
if (searchMode) {
|
|
2832
|
+
closeSearch();
|
|
2833
|
+
}
|
|
2834
|
+
tui.requestRender();
|
|
2835
|
+
};
|
|
2836
|
+
editor.onCtrlL = () => {
|
|
2837
|
+
chatLog.clearAll();
|
|
2838
|
+
tui.requestRender();
|
|
2839
|
+
};
|
|
2840
|
+
editor.onCtrlP = () => {
|
|
2841
|
+
loop.toggleProvider();
|
|
2842
|
+
modelName = loop.getActiveProvider().getModel();
|
|
2843
|
+
providerTypeStart = loop.getActiveProvider().getProviderType();
|
|
2844
|
+
const providerInfo = loop.getProviderRoutingInfo();
|
|
2845
|
+
if (providerInfo) {
|
|
2846
|
+
chatLog.addSystem(theme.accent('Provider toggled to ') +
|
|
2847
|
+
theme.fg(providerInfo.providerLabel) +
|
|
2848
|
+
theme.dim(` (${providerInfo.mode})`));
|
|
2849
|
+
}
|
|
2850
|
+
else {
|
|
2851
|
+
chatLog.addSystem(theme.dim('No ProviderRouter configured.'));
|
|
2852
|
+
}
|
|
2853
|
+
const statsP = statsManager.get(sessionDir);
|
|
2854
|
+
statsP.then((st) => {
|
|
2855
|
+
const tc = st.turn_count ?? 0;
|
|
2856
|
+
refreshStatus(loop.getTurnInfo(tc, st.current_context_tokens ?? 0));
|
|
2857
|
+
}).catch(() => { });
|
|
2858
|
+
tui.requestRender();
|
|
2859
|
+
};
|
|
2860
|
+
// ── Queue cancellation: Backspace on empty input pops last queued message ──
|
|
2861
|
+
editor.onBackspaceOnEmpty = () => {
|
|
2862
|
+
if (messageQueue.isEmpty())
|
|
2863
|
+
return;
|
|
2864
|
+
const removed = messageQueue.pop();
|
|
2865
|
+
if (removed) {
|
|
2866
|
+
chatLog.addSystem(theme.dim(`[queue] removed: "${truncateMsg(removed.text)}" (${messageQueue.size} remaining)`));
|
|
2867
|
+
updateTokenEstimate();
|
|
2868
|
+
tui.requestRender();
|
|
2869
|
+
}
|
|
2870
|
+
};
|
|
2871
|
+
// ── Slash command autocomplete ──
|
|
2872
|
+
const slashCommands = filterCommands('').map((cmd) => ({
|
|
2873
|
+
name: cmd.name,
|
|
2874
|
+
description: cmd.description,
|
|
2875
|
+
argumentHint: cmd.args,
|
|
2876
|
+
}));
|
|
2877
|
+
const autocomplete = new CombinedAutocompleteProvider(slashCommands, process.cwd());
|
|
2878
|
+
editor.setAutocompleteProvider(autocomplete);
|
|
2879
|
+
// ── Global input listeners (for shortcuts not caught by the editor) ──
|
|
2880
|
+
let ctrlCCount = 0;
|
|
2881
|
+
let lastCtrlCTime = 0;
|
|
2882
|
+
let searchMode = false;
|
|
2883
|
+
let searchQuery = '';
|
|
2884
|
+
let searchMatches = [];
|
|
2885
|
+
let searchMatchIndex = 0;
|
|
2886
|
+
tui.addInputListener((data) => {
|
|
2887
|
+
// ── Slash autocomplete: Space → accept completion + insert space (never submit) ──
|
|
2888
|
+
// 只有回车键才发送消息,空格键仅用于接受补全并插入空格
|
|
2889
|
+
if (matchesKey(data, Key.space)) {
|
|
2890
|
+
const text = editor.getText().trimStart();
|
|
2891
|
+
if (text.startsWith('/') && !text.includes(' ') && editor.isShowingAutocomplete()) {
|
|
2892
|
+
// 接受补全并插入空格,让用户继续输入参数
|
|
2893
|
+
editor.handleInput('\t');
|
|
2894
|
+
editor.handleInput(' ');
|
|
2895
|
+
return { consume: true };
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
// ── Ask User form: full keyboard navigation ──
|
|
2899
|
+
if (askUserState) {
|
|
2900
|
+
const st = askUserState;
|
|
2901
|
+
const suppIdx = st.questions.length;
|
|
2902
|
+
const totalTabs = suppIdx + 1;
|
|
2903
|
+
const currentQ = st.activeQuestion < suppIdx ? st.questions[st.activeQuestion] : null;
|
|
2904
|
+
const opts = currentQ?.options ?? [];
|
|
2905
|
+
const isMulti = currentQ?.multiSelect ?? false;
|
|
2906
|
+
// Enter: submit (on "补充" tab or when focused on custom input)
|
|
2907
|
+
if (matchesKey(data, Key.enter)) {
|
|
2908
|
+
// 收集当前活动的自定义输入
|
|
2909
|
+
const editIdx = st.activeQuestion;
|
|
2910
|
+
const currentCustom = st.customTexts.get(editIdx) ?? '';
|
|
2911
|
+
if (currentCustom.trim()) {
|
|
2912
|
+
st.customTexts.set(editIdx, currentCustom.trim());
|
|
2913
|
+
}
|
|
2914
|
+
resolveAskUser();
|
|
2915
|
+
return { consume: true };
|
|
2916
|
+
}
|
|
2917
|
+
// Escape: cancel (discard form)
|
|
2918
|
+
if (matchesKey(data, Key.escape)) {
|
|
2919
|
+
askUserContent.setText('');
|
|
2920
|
+
askUserBar.setText('');
|
|
2921
|
+
askUserState = null;
|
|
2922
|
+
st.resolve('{}');
|
|
2923
|
+
tui.requestRender();
|
|
2924
|
+
return { consume: true };
|
|
2925
|
+
}
|
|
2926
|
+
// Left/Right: switch tabs
|
|
2927
|
+
if (matchesKey(data, Key.left)) {
|
|
2928
|
+
st.activeQuestion = (st.activeQuestion + totalTabs - 1) % totalTabs;
|
|
2929
|
+
st.activeOption = 0;
|
|
2930
|
+
renderAskUserForm();
|
|
2931
|
+
tui.requestRender();
|
|
2932
|
+
return { consume: true };
|
|
2933
|
+
}
|
|
2934
|
+
if (matchesKey(data, Key.right)) {
|
|
2935
|
+
st.activeQuestion = (st.activeQuestion + 1) % totalTabs;
|
|
2936
|
+
st.activeOption = 0;
|
|
2937
|
+
renderAskUserForm();
|
|
2938
|
+
tui.requestRender();
|
|
2939
|
+
return { consume: true };
|
|
2940
|
+
}
|
|
2941
|
+
// Up/Down: navigate options (only on question tabs)
|
|
2942
|
+
if (st.activeQuestion < suppIdx && opts.length > 0) {
|
|
2943
|
+
if (matchesKey(data, Key.up)) {
|
|
2944
|
+
st.activeOption = (st.activeOption + opts.length - 1) % opts.length;
|
|
2945
|
+
renderAskUserForm();
|
|
2946
|
+
tui.requestRender();
|
|
2947
|
+
return { consume: true };
|
|
2948
|
+
}
|
|
2949
|
+
if (matchesKey(data, Key.down)) {
|
|
2950
|
+
st.activeOption = (st.activeOption + 1) % opts.length;
|
|
2951
|
+
renderAskUserForm();
|
|
2952
|
+
tui.requestRender();
|
|
2953
|
+
return { consume: true };
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
// Space: toggle checkbox (works for both multiSelect and single-select)
|
|
2957
|
+
if (matchesKey(data, Key.space)) {
|
|
2958
|
+
if (st.activeQuestion < suppIdx && opts.length > 0) {
|
|
2959
|
+
const sel = st.selectedOptions.get(st.activeQuestion) ?? new Set();
|
|
2960
|
+
if (sel.has(st.activeOption)) {
|
|
2961
|
+
sel.delete(st.activeOption); // 取消选中
|
|
2962
|
+
}
|
|
2963
|
+
else {
|
|
2964
|
+
if (isMulti) {
|
|
2965
|
+
sel.add(st.activeOption);
|
|
2966
|
+
}
|
|
2967
|
+
else {
|
|
2968
|
+
// 单选:清除其它,只选中当前
|
|
2969
|
+
sel.clear();
|
|
2970
|
+
sel.add(st.activeOption);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
st.selectedOptions.set(st.activeQuestion, sel);
|
|
2974
|
+
}
|
|
2975
|
+
renderAskUserForm();
|
|
2976
|
+
tui.requestRender();
|
|
2977
|
+
return { consume: true };
|
|
2978
|
+
}
|
|
2979
|
+
// Backspace: delete last char in custom text
|
|
2980
|
+
if (matchesKey(data, Key.backspace)) {
|
|
2981
|
+
const editIdx = st.activeQuestion;
|
|
2982
|
+
const current = st.customTexts.get(editIdx) ?? '';
|
|
2983
|
+
st.customTexts.set(editIdx, current.slice(0, -1));
|
|
2984
|
+
renderAskUserForm();
|
|
2985
|
+
tui.requestRender();
|
|
2986
|
+
return { consume: true };
|
|
2987
|
+
}
|
|
2988
|
+
// Typing characters → add to current tab's custom text buffer
|
|
2989
|
+
// data is a string; printable chars have length 1 and are not control chars
|
|
2990
|
+
if (typeof data === 'string' && data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
2991
|
+
const editIdx = st.activeQuestion;
|
|
2992
|
+
const current = st.customTexts.get(editIdx) ?? '';
|
|
2993
|
+
st.customTexts.set(editIdx, current + data);
|
|
2994
|
+
renderAskUserForm();
|
|
2995
|
+
tui.requestRender();
|
|
2996
|
+
return { consume: true };
|
|
2997
|
+
}
|
|
2998
|
+
// Consume all other keys during ask_user
|
|
2999
|
+
return { consume: true };
|
|
3000
|
+
}
|
|
3001
|
+
// ── Permission bar: Left/Right arrows + Enter ──
|
|
3002
|
+
if (permissionQueue.length > 0) {
|
|
3003
|
+
if (matchesKey(data, Key.left)) {
|
|
3004
|
+
permissionSelection = (permissionSelection + 3) % 4;
|
|
3005
|
+
updatePermissionBar();
|
|
3006
|
+
tui.requestRender();
|
|
3007
|
+
return { consume: true };
|
|
3008
|
+
}
|
|
3009
|
+
if (matchesKey(data, Key.right)) {
|
|
3010
|
+
permissionSelection = (permissionSelection + 1) % 4;
|
|
3011
|
+
updatePermissionBar();
|
|
3012
|
+
tui.requestRender();
|
|
3013
|
+
return { consume: true };
|
|
3014
|
+
}
|
|
3015
|
+
if (matchesKey(data, Key.enter)) {
|
|
3016
|
+
const options = ['yes', 'aor', 'always', 'no'];
|
|
3017
|
+
resolvePermission(options[permissionSelection]);
|
|
3018
|
+
return { consume: true };
|
|
3019
|
+
}
|
|
3020
|
+
return { consume: true }; // consume all other keys during permission
|
|
3021
|
+
}
|
|
3022
|
+
// Ctrl+C double-tap exit (global, catches the case when editor is not the source)
|
|
3023
|
+
if (matchesKey(data, Key.ctrl('c'))) {
|
|
3024
|
+
const now = Date.now();
|
|
3025
|
+
if (now - lastCtrlCTime < 1000) {
|
|
3026
|
+
ctrlCCount++;
|
|
3027
|
+
}
|
|
3028
|
+
else {
|
|
3029
|
+
ctrlCCount = 1;
|
|
3030
|
+
}
|
|
3031
|
+
lastCtrlCTime = now;
|
|
3032
|
+
// Note: editor.onCtrlC handles the first press; this handles the second
|
|
3033
|
+
return undefined; // let editor handle Ctrl+C
|
|
3034
|
+
}
|
|
3035
|
+
// Ctrl+T: toggle tools expanded/collapsed
|
|
3036
|
+
if (matchesKey(data, Key.ctrl('t'))) {
|
|
3037
|
+
const expanded = chatLog.toggleToolsExpanded();
|
|
3038
|
+
chatLog.addSystem(theme.dim('Tools ') + theme.fg(expanded ? 'expanded' : 'collapsed'));
|
|
3039
|
+
tui.requestRender();
|
|
3040
|
+
return { consume: true };
|
|
3041
|
+
}
|
|
3042
|
+
// Ctrl+F: toggle search overlay
|
|
3043
|
+
if (matchesKey(data, Key.ctrl('f'))) {
|
|
3044
|
+
if (searchMode) {
|
|
3045
|
+
closeSearch();
|
|
3046
|
+
}
|
|
3047
|
+
else {
|
|
3048
|
+
openSearch();
|
|
3049
|
+
}
|
|
3050
|
+
return { consume: true };
|
|
3051
|
+
}
|
|
3052
|
+
// PgUp: scroll chat up
|
|
3053
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
3054
|
+
chatLog.scrollToLine(-10);
|
|
3055
|
+
tui.requestRender();
|
|
3056
|
+
return { consume: true };
|
|
3057
|
+
}
|
|
3058
|
+
// PgDn: scroll chat down
|
|
3059
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
3060
|
+
chatLog.scrollToLine(10);
|
|
3061
|
+
tui.requestRender();
|
|
3062
|
+
return { consume: true };
|
|
3063
|
+
}
|
|
3064
|
+
// Home: jump to top
|
|
3065
|
+
if (matchesKey(data, Key.home)) {
|
|
3066
|
+
chatLog.scrollToLine(0);
|
|
3067
|
+
tui.requestRender();
|
|
3068
|
+
return { consume: true };
|
|
3069
|
+
}
|
|
3070
|
+
// End: jump to bottom
|
|
3071
|
+
if (matchesKey(data, Key.end)) {
|
|
3072
|
+
chatLog.pinToBottom();
|
|
3073
|
+
tui.requestRender();
|
|
3074
|
+
return { consume: true };
|
|
3075
|
+
}
|
|
3076
|
+
// Arrow Up: history browse (when editor text is empty)
|
|
3077
|
+
if (matchesKey(data, Key.up)) {
|
|
3078
|
+
if (slashSubPanelActive)
|
|
3079
|
+
return undefined;
|
|
3080
|
+
const currentText = editor.getText();
|
|
3081
|
+
if (currentText === '' && inputHistory.length > 0) {
|
|
3082
|
+
if (historyIndex === -1) {
|
|
3083
|
+
historyIndex = inputHistory.length - 1;
|
|
3084
|
+
}
|
|
3085
|
+
else if (historyIndex > 0) {
|
|
3086
|
+
historyIndex--;
|
|
3087
|
+
}
|
|
3088
|
+
editor.setText(inputHistory[historyIndex]);
|
|
3089
|
+
tui.requestRender();
|
|
3090
|
+
return { consume: true };
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
// Arrow Down: history browse
|
|
3094
|
+
if (matchesKey(data, Key.down)) {
|
|
3095
|
+
if (slashSubPanelActive)
|
|
3096
|
+
return undefined;
|
|
3097
|
+
if (historyIndex >= 0) {
|
|
3098
|
+
if (historyIndex < inputHistory.length - 1) {
|
|
3099
|
+
historyIndex++;
|
|
3100
|
+
editor.setText(inputHistory[historyIndex]);
|
|
3101
|
+
}
|
|
3102
|
+
else {
|
|
3103
|
+
historyIndex = -1;
|
|
3104
|
+
editor.setText('');
|
|
3105
|
+
}
|
|
3106
|
+
tui.requestRender();
|
|
3107
|
+
return { consume: true };
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
return undefined;
|
|
3111
|
+
});
|
|
3112
|
+
// ── Search helpers ──
|
|
3113
|
+
let searchOverlayContainer = null;
|
|
3114
|
+
let searchOverlayText = null;
|
|
3115
|
+
/** Highlight occurrences of `query` in `text` (case-insensitive) using accent color */
|
|
3116
|
+
function highlightMatch(text, query, maxLen = 80) {
|
|
3117
|
+
if (!query)
|
|
3118
|
+
return theme.dim(text.slice(0, maxLen));
|
|
3119
|
+
const truncated = text.slice(0, maxLen);
|
|
3120
|
+
const lower = truncated.toLowerCase();
|
|
3121
|
+
const q = query.toLowerCase();
|
|
3122
|
+
let result = '';
|
|
3123
|
+
let idx = 0;
|
|
3124
|
+
while (idx < truncated.length) {
|
|
3125
|
+
const found = lower.indexOf(q, idx);
|
|
3126
|
+
if (found === -1) {
|
|
3127
|
+
result += theme.dim(truncated.slice(idx));
|
|
3128
|
+
break;
|
|
3129
|
+
}
|
|
3130
|
+
if (found > idx) {
|
|
3131
|
+
result += theme.dim(truncated.slice(idx, found));
|
|
3132
|
+
}
|
|
3133
|
+
result += theme.accent(truncated.slice(found, found + q.length));
|
|
3134
|
+
idx = found + q.length;
|
|
3135
|
+
}
|
|
3136
|
+
return result;
|
|
3137
|
+
}
|
|
3138
|
+
function performSearch(query) {
|
|
3139
|
+
searchQuery = query;
|
|
3140
|
+
const contentLines = chatLog.getContentLines();
|
|
3141
|
+
const cleanLines = contentLines.map((l) =>
|
|
3142
|
+
// eslint-disable-next-line no-control-regex
|
|
3143
|
+
l.replace(/\x1b\[[0-9;]*m/g, ''));
|
|
3144
|
+
searchMatches = [];
|
|
3145
|
+
const lowerQuery = query.toLowerCase();
|
|
3146
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
3147
|
+
if (cleanLines[i].toLowerCase().includes(lowerQuery)) {
|
|
3148
|
+
searchMatches.push(i);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
searchMatchIndex = 0;
|
|
3152
|
+
if (searchOverlayText) {
|
|
3153
|
+
if (searchMatches.length > 0) {
|
|
3154
|
+
const preview = highlightMatch(cleanLines[searchMatches[0]], query);
|
|
3155
|
+
searchOverlayText.setText(theme.accent('Search: ') +
|
|
3156
|
+
theme.fg(query) +
|
|
3157
|
+
theme.dim(` [${searchMatchIndex + 1}/${searchMatches.length}]`) +
|
|
3158
|
+
'\n' +
|
|
3159
|
+
preview);
|
|
3160
|
+
}
|
|
3161
|
+
else {
|
|
3162
|
+
searchOverlayText.setText(theme.accent('Search: ') + theme.fg(query) + theme.dim(' [0/0]'));
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
tui.requestRender();
|
|
3166
|
+
}
|
|
3167
|
+
function nextSearchMatch() {
|
|
3168
|
+
if (searchMatches.length === 0)
|
|
3169
|
+
return;
|
|
3170
|
+
searchMatchIndex = (searchMatchIndex + 1) % searchMatches.length;
|
|
3171
|
+
chatLog.scrollToLine(searchMatches[searchMatchIndex]);
|
|
3172
|
+
if (searchOverlayText) {
|
|
3173
|
+
const contentLines = chatLog.getContentLines();
|
|
3174
|
+
const cleanLines = contentLines.map((l) =>
|
|
3175
|
+
// eslint-disable-next-line no-control-regex
|
|
3176
|
+
l.replace(/\x1b\[[0-9;]*m/g, ''));
|
|
3177
|
+
const preview = highlightMatch(cleanLines[searchMatches[searchMatchIndex]] ?? '', searchQuery);
|
|
3178
|
+
searchOverlayText.setText(theme.accent('Search: ') +
|
|
3179
|
+
theme.fg(searchQuery) +
|
|
3180
|
+
theme.dim(` [${searchMatchIndex + 1}/${searchMatches.length}]`) +
|
|
3181
|
+
'\n' +
|
|
3182
|
+
preview);
|
|
3183
|
+
}
|
|
3184
|
+
tui.requestRender();
|
|
3185
|
+
}
|
|
3186
|
+
function openSearch() {
|
|
3187
|
+
searchMode = true;
|
|
3188
|
+
searchQuery = '';
|
|
3189
|
+
searchMatches = [];
|
|
3190
|
+
searchMatchIndex = 0;
|
|
3191
|
+
searchOverlayContainer = new Container();
|
|
3192
|
+
searchOverlayText = new Text(theme.accent('Search: ') + theme.dim('type to search, Enter for next, Esc to close'), 0, 0);
|
|
3193
|
+
searchOverlayContainer.addChild(searchOverlayText);
|
|
3194
|
+
tui.showOverlay(searchOverlayContainer);
|
|
3195
|
+
tui.requestRender();
|
|
3196
|
+
// Add a temporary input listener for search typing
|
|
3197
|
+
const searchListenerId = `search_${Date.now()}`;
|
|
3198
|
+
tui.addInputListener((data) => {
|
|
3199
|
+
if (!searchMode)
|
|
3200
|
+
return undefined;
|
|
3201
|
+
if (matchesKey(data, Key.escape)) {
|
|
3202
|
+
closeSearch();
|
|
3203
|
+
return { consume: true };
|
|
3204
|
+
}
|
|
3205
|
+
if (matchesKey(data, Key.enter)) {
|
|
3206
|
+
nextSearchMatch();
|
|
3207
|
+
return { consume: true };
|
|
3208
|
+
}
|
|
3209
|
+
if (matchesKey(data, Key.backspace)) {
|
|
3210
|
+
if (searchQuery.length > 0) {
|
|
3211
|
+
searchQuery = searchQuery.slice(0, -1);
|
|
3212
|
+
performSearch(searchQuery);
|
|
3213
|
+
}
|
|
3214
|
+
return { consume: true };
|
|
3215
|
+
}
|
|
3216
|
+
// Regular character input
|
|
3217
|
+
if (typeof data === 'string' && data.length === 1 && !data.startsWith('\x1b')) {
|
|
3218
|
+
searchQuery += data;
|
|
3219
|
+
performSearch(searchQuery);
|
|
3220
|
+
return { consume: true };
|
|
3221
|
+
}
|
|
3222
|
+
// Consume all other input while searching
|
|
3223
|
+
return { consume: true };
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
function closeSearch() {
|
|
3227
|
+
searchMode = false;
|
|
3228
|
+
searchQuery = '';
|
|
3229
|
+
searchMatches = [];
|
|
3230
|
+
searchMatchIndex = 0;
|
|
3231
|
+
searchOverlayText = null;
|
|
3232
|
+
if (searchOverlayContainer) {
|
|
3233
|
+
tui.hideOverlay();
|
|
3234
|
+
searchOverlayContainer = null;
|
|
3235
|
+
}
|
|
3236
|
+
tui.requestRender();
|
|
3237
|
+
}
|
|
3238
|
+
// ── Initial render ──
|
|
3239
|
+
const stats = await statsManager.get(sessionDir);
|
|
3240
|
+
const initialInfo = loop.getTurnInfo(0, stats.current_context_tokens ?? 0);
|
|
3241
|
+
refreshStatus(initialInfo);
|
|
3242
|
+
updateTokenEstimate();
|
|
3243
|
+
// ── Start TUI ──
|
|
3244
|
+
tui.start();
|
|
3245
|
+
// ── Auto-send continuation message after restart ──
|
|
3246
|
+
if (continuationMessage) {
|
|
3247
|
+
// Use setImmediate to let the TUI render its initial frame first
|
|
3248
|
+
setImmediate(() => {
|
|
3249
|
+
handleInput(continuationMessage);
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
// ── Wait for exit (Ctrl+C double-tap or Ctrl+D) ──
|
|
3253
|
+
const exitPromise = new Promise((resolve) => {
|
|
3254
|
+
const checkExitInterval = setInterval(() => {
|
|
3255
|
+
if (ctrlCCount >= 2) {
|
|
3256
|
+
clearInterval(checkExitInterval);
|
|
3257
|
+
resolve();
|
|
3258
|
+
}
|
|
3259
|
+
}, 200);
|
|
3260
|
+
});
|
|
3261
|
+
await exitPromise;
|
|
3262
|
+
// ── Cleanup ──
|
|
3263
|
+
tui.stop();
|
|
3264
|
+
await channelManager.stopAll();
|
|
3265
|
+
await loop.shutdown();
|
|
3266
|
+
// 清理插件钩子
|
|
3267
|
+
for (const cleanup of cleanupFns)
|
|
3268
|
+
cleanup();
|
|
3269
|
+
await supervisor.shutdownAll();
|
|
3270
|
+
process.stderr.write = origStderrWrite;
|
|
3271
|
+
logStream.end();
|
|
3272
|
+
}
|
|
3273
|
+
//# sourceMappingURL=tui.js.map
|