@vascend/talos 0.1.0
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/README.md +202 -0
- package/dist/backend/asset-events.d.ts +31 -0
- package/dist/backend/asset-events.js +149 -0
- package/dist/backend/assets.d.ts +31 -0
- package/dist/backend/assets.js +210 -0
- package/dist/backend/auth.d.ts +18 -0
- package/dist/backend/auth.js +59 -0
- package/dist/backend/config.d.ts +14 -0
- package/dist/backend/config.js +41 -0
- package/dist/backend/conversations.d.ts +21 -0
- package/dist/backend/conversations.js +141 -0
- package/dist/backend/crypto.d.ts +9 -0
- package/dist/backend/crypto.js +68 -0
- package/dist/backend/engine.d.ts +13 -0
- package/dist/backend/engine.js +78 -0
- package/dist/backend/http.d.ts +25 -0
- package/dist/backend/http.js +112 -0
- package/dist/backend/index.d.ts +1 -0
- package/dist/backend/index.js +24 -0
- package/dist/backend/routes.d.ts +14 -0
- package/dist/backend/routes.js +169 -0
- package/dist/backend/sdk.d.ts +12 -0
- package/dist/backend/sdk.js +11 -0
- package/dist/backend/server.d.ts +12 -0
- package/dist/backend/server.js +91 -0
- package/dist/backend/temp-image-store.d.ts +46 -0
- package/dist/backend/temp-image-store.js +197 -0
- package/dist/backend/types.d.ts +103 -0
- package/dist/backend/types.js +29 -0
- package/dist/backend/users.d.ts +13 -0
- package/dist/backend/users.js +76 -0
- package/dist/backend/ws-handler.d.ts +14 -0
- package/dist/backend/ws-handler.js +72 -0
- package/dist/backend/ws.d.ts +19 -0
- package/dist/backend/ws.js +146 -0
- package/dist/core/agent.d.ts +56 -0
- package/dist/core/agent.js +1783 -0
- package/dist/core/background.d.ts +45 -0
- package/dist/core/background.js +140 -0
- package/dist/core/bootstrap.d.ts +45 -0
- package/dist/core/bootstrap.js +294 -0
- package/dist/core/cli-args.d.ts +41 -0
- package/dist/core/cli-args.js +282 -0
- package/dist/core/collapsible-output.d.ts +38 -0
- package/dist/core/collapsible-output.js +143 -0
- package/dist/core/compaction-checkpoint.d.ts +9 -0
- package/dist/core/compaction-checkpoint.js +58 -0
- package/dist/core/compaction.d.ts +42 -0
- package/dist/core/compaction.js +270 -0
- package/dist/core/config.d.ts +49 -0
- package/dist/core/config.js +417 -0
- package/dist/core/context-ledger.d.ts +99 -0
- package/dist/core/context-ledger.js +222 -0
- package/dist/core/context-policy.d.ts +17 -0
- package/dist/core/context-policy.js +81 -0
- package/dist/core/continuation-review.d.ts +23 -0
- package/dist/core/continuation-review.js +64 -0
- package/dist/core/crash-report.d.ts +3 -0
- package/dist/core/crash-report.js +27 -0
- package/dist/core/credentials.d.ts +11 -0
- package/dist/core/credentials.js +183 -0
- package/dist/core/doctor.d.ts +24 -0
- package/dist/core/doctor.js +125 -0
- package/dist/core/effort.d.ts +5 -0
- package/dist/core/effort.js +42 -0
- package/dist/core/env-paths.d.ts +3 -0
- package/dist/core/env-paths.js +38 -0
- package/dist/core/execution-guidance.d.ts +9 -0
- package/dist/core/execution-guidance.js +73 -0
- package/dist/core/execution-target.d.ts +41 -0
- package/dist/core/execution-target.js +69 -0
- package/dist/core/file-coordination.d.ts +66 -0
- package/dist/core/file-coordination.js +176 -0
- package/dist/core/file-lease-store.d.ts +14 -0
- package/dist/core/file-lease-store.js +89 -0
- package/dist/core/image-limits.d.ts +26 -0
- package/dist/core/image-limits.js +205 -0
- package/dist/core/interruptible-completion.d.ts +3 -0
- package/dist/core/interruptible-completion.js +42 -0
- package/dist/core/kingdoms-mode-state.d.ts +24 -0
- package/dist/core/kingdoms-mode-state.js +139 -0
- package/dist/core/licenza.d.ts +38 -0
- package/dist/core/licenza.js +125 -0
- package/dist/core/model-limits.d.ts +2 -0
- package/dist/core/model-limits.js +36 -0
- package/dist/core/output-lifecycle.d.ts +44 -0
- package/dist/core/output-lifecycle.js +190 -0
- package/dist/core/package-update.d.ts +15 -0
- package/dist/core/package-update.js +121 -0
- package/dist/core/permission-queue.d.ts +2 -0
- package/dist/core/permission-queue.js +20 -0
- package/dist/core/prompt-attachment.d.ts +21 -0
- package/dist/core/prompt-attachment.js +123 -0
- package/dist/core/request-plan.d.ts +16 -0
- package/dist/core/request-plan.js +36 -0
- package/dist/core/run-timing.d.ts +17 -0
- package/dist/core/run-timing.js +123 -0
- package/dist/core/self-update.d.ts +17 -0
- package/dist/core/self-update.js +54 -0
- package/dist/core/session-refs.d.ts +27 -0
- package/dist/core/session-refs.js +126 -0
- package/dist/core/session-storage.d.ts +4 -0
- package/dist/core/session-storage.js +48 -0
- package/dist/core/session-title.d.ts +1 -0
- package/dist/core/session-title.js +21 -0
- package/dist/core/session-usage.d.ts +17 -0
- package/dist/core/session-usage.js +74 -0
- package/dist/core/session.d.ts +58 -0
- package/dist/core/session.js +296 -0
- package/dist/core/settings.d.ts +17 -0
- package/dist/core/settings.js +107 -0
- package/dist/core/storage-retention.d.ts +21 -0
- package/dist/core/storage-retention.js +227 -0
- package/dist/core/text-sanitize.d.ts +1 -0
- package/dist/core/text-sanitize.js +35 -0
- package/dist/core/tool-artifacts.d.ts +6 -0
- package/dist/core/tool-artifacts.js +51 -0
- package/dist/core/tool-compress.d.ts +8 -0
- package/dist/core/tool-compress.js +111 -0
- package/dist/core/tool-executor.d.ts +16 -0
- package/dist/core/tool-executor.js +97 -0
- package/dist/core/tool-result-index.d.ts +52 -0
- package/dist/core/tool-result-index.js +327 -0
- package/dist/core/tool-result-store.d.ts +38 -0
- package/dist/core/tool-result-store.js +190 -0
- package/dist/core/tool-scheduler.d.ts +16 -0
- package/dist/core/tool-scheduler.js +35 -0
- package/dist/core/trust.d.ts +21 -0
- package/dist/core/trust.js +95 -0
- package/dist/core/types.d.ts +402 -0
- package/dist/core/types.js +1 -0
- package/dist/core/update-check.d.ts +26 -0
- package/dist/core/update-check.js +89 -0
- package/dist/core/update-process.d.ts +10 -0
- package/dist/core/update-process.js +31 -0
- package/dist/core/user-inbox.d.ts +23 -0
- package/dist/core/user-inbox.js +56 -0
- package/dist/core/user-input.d.ts +10 -0
- package/dist/core/user-input.js +35 -0
- package/dist/core/version.d.ts +1 -0
- package/dist/core/version.js +10 -0
- package/dist/core/vibe-modes.d.ts +38 -0
- package/dist/core/vibe-modes.js +383 -0
- package/dist/core/workspace-roots.d.ts +13 -0
- package/dist/core/workspace-roots.js +91 -0
- package/dist/core/workspace-sessions.d.ts +51 -0
- package/dist/core/workspace-sessions.js +243 -0
- package/dist/cowork/client.d.ts +29 -0
- package/dist/cowork/client.js +49 -0
- package/dist/cowork/engine-bridge.d.ts +22 -0
- package/dist/cowork/engine-bridge.js +98 -0
- package/dist/cowork/index.d.ts +11 -0
- package/dist/cowork/index.js +16 -0
- package/dist/cowork/ipc.d.ts +97 -0
- package/dist/cowork/ipc.js +96 -0
- package/dist/cowork/manager.d.ts +66 -0
- package/dist/cowork/manager.js +325 -0
- package/dist/cowork/store.d.ts +23 -0
- package/dist/cowork/store.js +182 -0
- package/dist/cowork/sync.d.ts +20 -0
- package/dist/cowork/sync.js +164 -0
- package/dist/cowork/types.d.ts +78 -0
- package/dist/cowork/types.js +21 -0
- package/dist/extensions/ext-tools.d.ts +3 -0
- package/dist/extensions/ext-tools.js +73 -0
- package/dist/extensions/loader.d.ts +91 -0
- package/dist/extensions/loader.js +474 -0
- package/dist/extensions/settings.d.ts +20 -0
- package/dist/extensions/settings.js +86 -0
- package/dist/extensions/types.d.ts +219 -0
- package/dist/extensions/types.js +1 -0
- package/dist/horizon/async-queue.d.ts +9 -0
- package/dist/horizon/async-queue.js +46 -0
- package/dist/horizon/engine.d.ts +85 -0
- package/dist/horizon/engine.js +603 -0
- package/dist/horizon/index.d.ts +4 -0
- package/dist/horizon/index.js +2 -0
- package/dist/horizon/main-team.d.ts +163 -0
- package/dist/horizon/main-team.js +774 -0
- package/dist/horizon/types.d.ts +233 -0
- package/dist/horizon/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +233 -0
- package/dist/mcp/protocol.d.ts +81 -0
- package/dist/mcp/protocol.js +122 -0
- package/dist/package.json +1 -0
- package/dist/packages/packages.d.ts +12 -0
- package/dist/packages/packages.js +323 -0
- package/dist/prompts/templates.d.ts +13 -0
- package/dist/prompts/templates.js +139 -0
- package/dist/providers/anthropic-oauth.d.ts +13 -0
- package/dist/providers/anthropic-oauth.js +124 -0
- package/dist/providers/anthropic.d.ts +27 -0
- package/dist/providers/anthropic.js +526 -0
- package/dist/providers/catalog.d.ts +15 -0
- package/dist/providers/catalog.js +166 -0
- package/dist/providers/effort.d.ts +12 -0
- package/dist/providers/effort.js +102 -0
- package/dist/providers/model-catalog.d.ts +43 -0
- package/dist/providers/model-catalog.js +70 -0
- package/dist/providers/model-effort.d.ts +5 -0
- package/dist/providers/model-effort.js +43 -0
- package/dist/providers/model-registry.d.ts +39 -0
- package/dist/providers/model-registry.js +142 -0
- package/dist/providers/models-dev.d.ts +9 -0
- package/dist/providers/models-dev.js +97 -0
- package/dist/providers/oauth-callback-server.d.ts +28 -0
- package/dist/providers/oauth-callback-server.js +184 -0
- package/dist/providers/oauth-orchestrator.d.ts +58 -0
- package/dist/providers/oauth-orchestrator.js +159 -0
- package/dist/providers/oauth-store.d.ts +12 -0
- package/dist/providers/oauth-store.js +139 -0
- package/dist/providers/open-browser.d.ts +20 -0
- package/dist/providers/open-browser.js +92 -0
- package/dist/providers/openai-compat.d.ts +14 -0
- package/dist/providers/openai-compat.js +523 -0
- package/dist/providers/openai-oauth.d.ts +15 -0
- package/dist/providers/openai-oauth.js +382 -0
- package/dist/providers/openrouter-auth.d.ts +73 -0
- package/dist/providers/openrouter-auth.js +321 -0
- package/dist/providers/openrouter-manual-fallback.d.ts +25 -0
- package/dist/providers/openrouter-manual-fallback.js +161 -0
- package/dist/providers/openrouter.d.ts +15 -0
- package/dist/providers/openrouter.js +107 -0
- package/dist/providers/plan-limits.d.ts +6 -0
- package/dist/providers/plan-limits.js +175 -0
- package/dist/providers/provider-reset.d.ts +4 -0
- package/dist/providers/provider-reset.js +83 -0
- package/dist/providers/quota-limits.d.ts +4 -0
- package/dist/providers/quota-limits.js +33 -0
- package/dist/providers/registry.d.ts +33 -0
- package/dist/providers/registry.js +213 -0
- package/dist/providers/retry.d.ts +37 -0
- package/dist/providers/retry.js +347 -0
- package/dist/rpc/server.d.ts +23 -0
- package/dist/rpc/server.js +222 -0
- package/dist/sdk.d.ts +67 -0
- package/dist/sdk.js +64 -0
- package/dist/skills/skills.d.ts +17 -0
- package/dist/skills/skills.js +155 -0
- package/dist/text-sanitize.js +28 -0
- package/dist/tools/ask-user.d.ts +2 -0
- package/dist/tools/ask-user.js +60 -0
- package/dist/tools/builtin.d.ts +48 -0
- package/dist/tools/builtin.js +2494 -0
- package/dist/tools/capabilities.d.ts +5 -0
- package/dist/tools/capabilities.js +55 -0
- package/dist/tools/computer-capture.d.ts +2 -0
- package/dist/tools/computer-capture.js +91 -0
- package/dist/tools/computer-posix.d.ts +5 -0
- package/dist/tools/computer-posix.js +136 -0
- package/dist/tools/computer-use.d.ts +14 -0
- package/dist/tools/computer-use.js +108 -0
- package/dist/tools/discovery.d.ts +12 -0
- package/dist/tools/discovery.js +205 -0
- package/dist/tools/execute.d.ts +2 -0
- package/dist/tools/execute.js +73 -0
- package/dist/tools/normalize-input.d.ts +17 -0
- package/dist/tools/normalize-input.js +174 -0
- package/dist/tools/output-artifact.d.ts +3 -0
- package/dist/tools/output-artifact.js +80 -0
- package/dist/tools/pagination.d.ts +15 -0
- package/dist/tools/pagination.js +93 -0
- package/dist/tools/plan-status.d.ts +2 -0
- package/dist/tools/plan-status.js +109 -0
- package/dist/tools/program-input.d.ts +3 -0
- package/dist/tools/program-input.js +19 -0
- package/dist/tools/program.d.ts +3 -0
- package/dist/tools/program.js +567 -0
- package/dist/tools/protocol-catalog.d.ts +12 -0
- package/dist/tools/protocol-catalog.js +46 -0
- package/dist/tools/ramification-tools.d.ts +12 -0
- package/dist/tools/ramification-tools.js +528 -0
- package/dist/tools/regex-compat.d.ts +9 -0
- package/dist/tools/regex-compat.js +96 -0
- package/dist/tools/registry.d.ts +51 -0
- package/dist/tools/registry.js +185 -0
- package/dist/tools/resolve.d.ts +2 -0
- package/dist/tools/resolve.js +107 -0
- package/dist/tools/shell-env.d.ts +2 -0
- package/dist/tools/shell-env.js +7 -0
- package/dist/tools/team-input.d.ts +3 -0
- package/dist/tools/team-input.js +129 -0
- package/dist/tools/team-tools.d.ts +7 -0
- package/dist/tools/team-tools.js +348 -0
- package/dist/tools/workspace-sessions.d.ts +2 -0
- package/dist/tools/workspace-sessions.js +35 -0
- package/dist/tui/account-entry.d.ts +1 -0
- package/dist/tui/account-entry.js +93 -0
- package/dist/tui/attachment-highlight.d.ts +1 -0
- package/dist/tui/attachment-highlight.js +24 -0
- package/dist/tui/attachment-refs.d.ts +2 -0
- package/dist/tui/attachment-refs.js +3 -0
- package/dist/tui/bottom-input.d.ts +219 -0
- package/dist/tui/bottom-input.js +1958 -0
- package/dist/tui/branding.d.ts +107 -0
- package/dist/tui/branding.js +750 -0
- package/dist/tui/clipboard-image.d.ts +10 -0
- package/dist/tui/clipboard-image.js +163 -0
- package/dist/tui/clipboard-text.d.ts +9 -0
- package/dist/tui/clipboard-text.js +51 -0
- package/dist/tui/clipboard-windows.d.ts +8 -0
- package/dist/tui/clipboard-windows.js +84 -0
- package/dist/tui/collapsible-command.d.ts +15 -0
- package/dist/tui/collapsible-command.js +48 -0
- package/dist/tui/command-feedback.d.ts +10 -0
- package/dist/tui/command-feedback.js +46 -0
- package/dist/tui/command-output-registry.d.ts +28 -0
- package/dist/tui/command-output-registry.js +103 -0
- package/dist/tui/command-transcript.d.ts +12 -0
- package/dist/tui/command-transcript.js +90 -0
- package/dist/tui/commands.d.ts +20 -0
- package/dist/tui/commands.js +131 -0
- package/dist/tui/context-bar.d.ts +4 -0
- package/dist/tui/context-bar.js +74 -0
- package/dist/tui/context-meter.d.ts +25 -0
- package/dist/tui/context-meter.js +81 -0
- package/dist/tui/cost-binding.d.ts +8 -0
- package/dist/tui/cost-binding.js +43 -0
- package/dist/tui/cost-tracker.d.ts +49 -0
- package/dist/tui/cost-tracker.js +122 -0
- package/dist/tui/diff.d.ts +9 -0
- package/dist/tui/diff.js +119 -0
- package/dist/tui/i18n-catalog.d.ts +5 -0
- package/dist/tui/i18n-catalog.js +184 -0
- package/dist/tui/i18n.d.ts +6 -0
- package/dist/tui/i18n.js +10 -0
- package/dist/tui/image-attachments.d.ts +46 -0
- package/dist/tui/image-attachments.js +167 -0
- package/dist/tui/images.d.ts +12 -0
- package/dist/tui/images.js +87 -0
- package/dist/tui/intro.d.ts +13 -0
- package/dist/tui/intro.js +109 -0
- package/dist/tui/language-preference.d.ts +6 -0
- package/dist/tui/language-preference.js +24 -0
- package/dist/tui/live-output.d.ts +2 -0
- package/dist/tui/live-output.js +18 -0
- package/dist/tui/locales.d.ts +19 -0
- package/dist/tui/locales.js +22 -0
- package/dist/tui/login-menu.d.ts +17 -0
- package/dist/tui/login-menu.js +58 -0
- package/dist/tui/logo.d.ts +8 -0
- package/dist/tui/logo.js +305 -0
- package/dist/tui/markdown-inline.d.ts +3 -0
- package/dist/tui/markdown-inline.js +99 -0
- package/dist/tui/markdown-table.d.ts +4 -0
- package/dist/tui/markdown-table.js +63 -0
- package/dist/tui/markdown.d.ts +11 -0
- package/dist/tui/markdown.js +257 -0
- package/dist/tui/mcp-transcript.d.ts +7 -0
- package/dist/tui/mcp-transcript.js +40 -0
- package/dist/tui/model-details.d.ts +2 -0
- package/dist/tui/model-details.js +21 -0
- package/dist/tui/model-selection.d.ts +7 -0
- package/dist/tui/model-selection.js +41 -0
- package/dist/tui/native-scrollback.d.ts +27 -0
- package/dist/tui/native-scrollback.js +134 -0
- package/dist/tui/onboarding.d.ts +72 -0
- package/dist/tui/onboarding.js +207 -0
- package/dist/tui/paste-collapse.d.ts +39 -0
- package/dist/tui/paste-collapse.js +117 -0
- package/dist/tui/perm-mode.d.ts +8 -0
- package/dist/tui/perm-mode.js +41 -0
- package/dist/tui/prompt-queue-store.d.ts +26 -0
- package/dist/tui/prompt-queue-store.js +87 -0
- package/dist/tui/prompt-queue.d.ts +47 -0
- package/dist/tui/prompt-queue.js +187 -0
- package/dist/tui/provider-menu.d.ts +33 -0
- package/dist/tui/provider-menu.js +175 -0
- package/dist/tui/question.d.ts +24 -0
- package/dist/tui/question.js +32 -0
- package/dist/tui/queue-screen.d.ts +25 -0
- package/dist/tui/queue-screen.js +152 -0
- package/dist/tui/queue-team.d.ts +3 -0
- package/dist/tui/queue-team.js +28 -0
- package/dist/tui/queued-questions.d.ts +20 -0
- package/dist/tui/queued-questions.js +163 -0
- package/dist/tui/quick-help.d.ts +1 -0
- package/dist/tui/quick-help.js +53 -0
- package/dist/tui/repl.d.ts +78 -0
- package/dist/tui/repl.js +4130 -0
- package/dist/tui/restore-queued-input.d.ts +3 -0
- package/dist/tui/restore-queued-input.js +20 -0
- package/dist/tui/review-detail.d.ts +6 -0
- package/dist/tui/review-detail.js +56 -0
- package/dist/tui/review-files.d.ts +3 -0
- package/dist/tui/review-files.js +22 -0
- package/dist/tui/review-render.d.ts +22 -0
- package/dist/tui/review-render.js +98 -0
- package/dist/tui/review-screen.d.ts +38 -0
- package/dist/tui/review-screen.js +358 -0
- package/dist/tui/review-store.d.ts +60 -0
- package/dist/tui/review-store.js +270 -0
- package/dist/tui/review-team.d.ts +2 -0
- package/dist/tui/review-team.js +27 -0
- package/dist/tui/selector-mouse.d.ts +26 -0
- package/dist/tui/selector-mouse.js +65 -0
- package/dist/tui/selector.d.ts +51 -0
- package/dist/tui/selector.js +299 -0
- package/dist/tui/session-replay.d.ts +10 -0
- package/dist/tui/session-replay.js +78 -0
- package/dist/tui/session-usage.d.ts +22 -0
- package/dist/tui/session-usage.js +55 -0
- package/dist/tui/skill-transcript.d.ts +5 -0
- package/dist/tui/skill-transcript.js +31 -0
- package/dist/tui/startup.d.ts +5 -0
- package/dist/tui/startup.js +61 -0
- package/dist/tui/status-line-loader.d.ts +8 -0
- package/dist/tui/status-line-loader.js +33 -0
- package/dist/tui/status-line.d.ts +38 -0
- package/dist/tui/status-line.js +213 -0
- package/dist/tui/system-language.d.ts +8 -0
- package/dist/tui/system-language.js +41 -0
- package/dist/tui/task-labels.d.ts +2 -0
- package/dist/tui/task-labels.js +7 -0
- package/dist/tui/term-bg.d.ts +4 -0
- package/dist/tui/term-bg.js +67 -0
- package/dist/tui/terminal-screen.d.ts +12 -0
- package/dist/tui/terminal-screen.js +1 -0
- package/dist/tui/terminal-text.d.ts +7 -0
- package/dist/tui/terminal-text.js +123 -0
- package/dist/tui/theme.d.ts +25 -0
- package/dist/tui/theme.js +383 -0
- package/dist/tui/tool-style.d.ts +7 -0
- package/dist/tui/tool-style.js +119 -0
- package/dist/tui/update-prompt.d.ts +14 -0
- package/dist/tui/update-prompt.js +104 -0
- package/dist/tui/usage-footer.d.ts +9 -0
- package/dist/tui/usage-footer.js +123 -0
- package/dist/tui/vascend-login.d.ts +68 -0
- package/dist/tui/vascend-login.js +355 -0
- package/dist/tui/web-login.d.ts +28 -0
- package/dist/tui/web-login.js +115 -0
- package/dist/vascend/activity-status.d.ts +12 -0
- package/dist/vascend/activity-status.js +68 -0
- package/dist/vascend/architecture-map.d.ts +73 -0
- package/dist/vascend/architecture-map.js +424 -0
- package/dist/vascend/attention-layout.d.ts +19 -0
- package/dist/vascend/attention-layout.js +141 -0
- package/dist/vascend/budget-allocator.d.ts +31 -0
- package/dist/vascend/budget-allocator.js +79 -0
- package/dist/vascend/candidate-collector.d.ts +20 -0
- package/dist/vascend/candidate-collector.js +220 -0
- package/dist/vascend/code-events.d.ts +24 -0
- package/dist/vascend/code-events.js +304 -0
- package/dist/vascend/code-graph-memory.d.ts +38 -0
- package/dist/vascend/code-graph-memory.js +334 -0
- package/dist/vascend/code-graph-runtime.d.ts +19 -0
- package/dist/vascend/code-graph-runtime.js +190 -0
- package/dist/vascend/code-graph-types.d.ts +58 -0
- package/dist/vascend/code-graph-types.js +50 -0
- package/dist/vascend/code-parser.d.ts +4 -0
- package/dist/vascend/code-parser.js +257 -0
- package/dist/vascend/competitor-knowledge.d.ts +53 -0
- package/dist/vascend/competitor-knowledge.js +262 -0
- package/dist/vascend/completeness.d.ts +16 -0
- package/dist/vascend/completeness.js +90 -0
- package/dist/vascend/context-autotuner.d.ts +45 -0
- package/dist/vascend/context-autotuner.js +231 -0
- package/dist/vascend/context-candidate.d.ts +45 -0
- package/dist/vascend/context-candidate.js +150 -0
- package/dist/vascend/context-capsule.d.ts +51 -0
- package/dist/vascend/context-capsule.js +133 -0
- package/dist/vascend/context-fabric.d.ts +108 -0
- package/dist/vascend/context-fabric.js +480 -0
- package/dist/vascend/context-feedback-loop.d.ts +56 -0
- package/dist/vascend/context-feedback-loop.js +204 -0
- package/dist/vascend/context-learning-identity.d.ts +4 -0
- package/dist/vascend/context-learning-identity.js +8 -0
- package/dist/vascend/context-links.d.ts +14 -0
- package/dist/vascend/context-links.js +83 -0
- package/dist/vascend/context-lint.d.ts +22 -0
- package/dist/vascend/context-lint.js +91 -0
- package/dist/vascend/context-market-selection.d.ts +17 -0
- package/dist/vascend/context-market-selection.js +50 -0
- package/dist/vascend/context-metrics.d.ts +25 -0
- package/dist/vascend/context-metrics.js +57 -0
- package/dist/vascend/context-pack-render.d.ts +16 -0
- package/dist/vascend/context-pack-render.js +52 -0
- package/dist/vascend/context-pareto.d.ts +33 -0
- package/dist/vascend/context-pareto.js +165 -0
- package/dist/vascend/context-pattern-explorer.d.ts +41 -0
- package/dist/vascend/context-pattern-explorer.js +276 -0
- package/dist/vascend/context-regret.d.ts +32 -0
- package/dist/vascend/context-regret.js +93 -0
- package/dist/vascend/context-retriever.d.ts +22 -0
- package/dist/vascend/context-retriever.js +243 -0
- package/dist/vascend/context-scope-index.d.ts +5 -0
- package/dist/vascend/context-scope-index.js +96 -0
- package/dist/vascend/context-scorer.d.ts +35 -0
- package/dist/vascend/context-scorer.js +162 -0
- package/dist/vascend/context-search-index.d.ts +12 -0
- package/dist/vascend/context-search-index.js +25 -0
- package/dist/vascend/context-snapshot.d.ts +22 -0
- package/dist/vascend/context-snapshot.js +135 -0
- package/dist/vascend/context-window-compiler.d.ts +42 -0
- package/dist/vascend/context-window-compiler.js +237 -0
- package/dist/vascend/contract-memory.d.ts +61 -0
- package/dist/vascend/contract-memory.js +435 -0
- package/dist/vascend/council.d.ts +58 -0
- package/dist/vascend/council.js +337 -0
- package/dist/vascend/decision-promotion.d.ts +48 -0
- package/dist/vascend/decision-promotion.js +410 -0
- package/dist/vascend/delta-context.d.ts +12 -0
- package/dist/vascend/delta-context.js +36 -0
- package/dist/vascend/dev-context-compiler.d.ts +27 -0
- package/dist/vascend/dev-context-compiler.js +133 -0
- package/dist/vascend/dev-context-layout.d.ts +30 -0
- package/dist/vascend/dev-context-layout.js +262 -0
- package/dist/vascend/evidence-slots.d.ts +14 -0
- package/dist/vascend/evidence-slots.js +55 -0
- package/dist/vascend/expansion-levels.d.ts +15 -0
- package/dist/vascend/expansion-levels.js +178 -0
- package/dist/vascend/file-lock.d.ts +7 -0
- package/dist/vascend/file-lock.js +95 -0
- package/dist/vascend/granular-kingdom.d.ts +25 -0
- package/dist/vascend/granular-kingdom.js +259 -0
- package/dist/vascend/granular-launcher.d.ts +48 -0
- package/dist/vascend/granular-launcher.js +202 -0
- package/dist/vascend/kingdom-bus.d.ts +25 -0
- package/dist/vascend/kingdom-bus.js +54 -0
- package/dist/vascend/kingdom-channel.d.ts +22 -0
- package/dist/vascend/kingdom-channel.js +180 -0
- package/dist/vascend/kingdom-kpi.d.ts +48 -0
- package/dist/vascend/kingdom-kpi.js +172 -0
- package/dist/vascend/kingdom-live.d.ts +28 -0
- package/dist/vascend/kingdom-live.js +61 -0
- package/dist/vascend/kingdom-run.d.ts +18 -0
- package/dist/vascend/kingdom-run.js +506 -0
- package/dist/vascend/kingdom-scribe.d.ts +20 -0
- package/dist/vascend/kingdom-scribe.js +52 -0
- package/dist/vascend/kingdom.d.ts +79 -0
- package/dist/vascend/kingdom.js +870 -0
- package/dist/vascend/lexical-index.d.ts +10 -0
- package/dist/vascend/lexical-index.js +99 -0
- package/dist/vascend/lexical-search.d.ts +29 -0
- package/dist/vascend/lexical-search.js +262 -0
- package/dist/vascend/long-term-context.d.ts +51 -0
- package/dist/vascend/long-term-context.js +194 -0
- package/dist/vascend/mandate.d.ts +33 -0
- package/dist/vascend/mandate.js +107 -0
- package/dist/vascend/memory-packet.d.ts +13 -0
- package/dist/vascend/memory-packet.js +155 -0
- package/dist/vascend/memory-status.d.ts +22 -0
- package/dist/vascend/memory-status.js +73 -0
- package/dist/vascend/memory-store.d.ts +62 -0
- package/dist/vascend/memory-store.js +273 -0
- package/dist/vascend/objective-revision.d.ts +69 -0
- package/dist/vascend/objective-revision.js +319 -0
- package/dist/vascend/patch-plan-store.d.ts +33 -0
- package/dist/vascend/patch-plan-store.js +240 -0
- package/dist/vascend/patch-plan.d.ts +97 -0
- package/dist/vascend/patch-plan.js +345 -0
- package/dist/vascend/plan-progress.d.ts +11 -0
- package/dist/vascend/plan-progress.js +68 -0
- package/dist/vascend/plan-store.d.ts +64 -0
- package/dist/vascend/plan-store.js +460 -0
- package/dist/vascend/proof-chains.d.ts +19 -0
- package/dist/vascend/proof-chains.js +120 -0
- package/dist/vascend/ramification-store.d.ts +25 -0
- package/dist/vascend/ramification-store.js +94 -0
- package/dist/vascend/ramification.d.ts +193 -0
- package/dist/vascend/ramification.js +583 -0
- package/dist/vascend/recall-planner.d.ts +17 -0
- package/dist/vascend/recall-planner.js +109 -0
- package/dist/vascend/requirements.d.ts +31 -0
- package/dist/vascend/requirements.js +151 -0
- package/dist/vascend/reviewer.d.ts +9 -0
- package/dist/vascend/reviewer.js +93 -0
- package/dist/vascend/room-graph.d.ts +29 -0
- package/dist/vascend/room-graph.js +242 -0
- package/dist/vascend/room-memory.d.ts +57 -0
- package/dist/vascend/room-memory.js +631 -0
- package/dist/vascend/runtime.d.ts +84 -0
- package/dist/vascend/runtime.js +286 -0
- package/dist/vascend/self-improve.d.ts +104 -0
- package/dist/vascend/self-improve.js +396 -0
- package/dist/vascend/session-search.d.ts +52 -0
- package/dist/vascend/session-search.js +349 -0
- package/dist/vascend/supersession-graph.d.ts +14 -0
- package/dist/vascend/supersession-graph.js +70 -0
- package/dist/vascend/team-areas.d.ts +9 -0
- package/dist/vascend/team-areas.js +28 -0
- package/dist/vascend/team-handoff.d.ts +24 -0
- package/dist/vascend/team-handoff.js +37 -0
- package/dist/vascend/team-scheduler.d.ts +13 -0
- package/dist/vascend/team-scheduler.js +74 -0
- package/dist/vascend/team-session.d.ts +42 -0
- package/dist/vascend/team-session.js +142 -0
- package/dist/vascend/test-failure-memory.d.ts +76 -0
- package/dist/vascend/test-failure-memory.js +354 -0
- package/dist/vascend/token-market.d.ts +42 -0
- package/dist/vascend/token-market.js +144 -0
- package/dist/vascend/tuner-baseline-store.d.ts +52 -0
- package/dist/vascend/tuner-baseline-store.js +446 -0
- package/dist/vascend/user-profile.d.ts +44 -0
- package/dist/vascend/user-profile.js +131 -0
- package/dist/vascend/vision.d.ts +94 -0
- package/dist/vascend/vision.js +375 -0
- package/dist/vascend/work-agent.d.ts +12 -0
- package/dist/vascend/work-agent.js +83 -0
- package/dist/vascend/work-queue.d.ts +62 -0
- package/dist/vascend/work-queue.js +255 -0
- package/dist/vascend/work-runner.d.ts +64 -0
- package/dist/vascend/work-runner.js +137 -0
- package/dist/vascend/worktree.d.ts +38 -0
- package/dist/vascend/worktree.js +206 -0
- package/dist/vendor/computer-use/README.md +7 -0
- package/dist/vendor/computer-use/computer-desktop.js +211 -0
- package/dist/vendor/computer-use/computer-observe.js +315 -0
- package/dist/vendor/computer-use/computer-scripts.js +1230 -0
- package/dist/vendor/computer-use/computer-tools.js +2136 -0
- package/dist/vendor/computer-use/package.json +1 -0
- package/dist/vendor/computer-use/rac/atomic-input.js +91 -0
- package/dist/vendor/computer-use/rac/grammatica.js +101 -0
- package/dist/vendor/computer-use/rac/parser.js +185 -0
- package/dist/vendor/computer-use/rac/protocol.js +136 -0
- package/install.ps1 +30 -0
- package/install.sh +50 -0
- package/package.json +56 -0
package/dist/tui/repl.js
ADDED
|
@@ -0,0 +1,4130 @@
|
|
|
1
|
+
import { reviewContinuation } from "../core/continuation-review.js";
|
|
2
|
+
import * as readline from "node:readline";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { watch, existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { stdin, stdout } from "node:process";
|
|
6
|
+
import { repoRoot, currentBranch } from "../vascend/worktree.js";
|
|
7
|
+
import { runTurn, compactNow } from "../core/agent.js";
|
|
8
|
+
import { createUserInbox, pendingText } from "../core/user-inbox.js";
|
|
9
|
+
import { renderDiff } from "./diff.js";
|
|
10
|
+
import { renderMarkdown, wrapPlain } from "./markdown.js";
|
|
11
|
+
import { SessionUsageState } from "./session-usage.js";
|
|
12
|
+
import { ContextMeter, contextDetails } from "./context-meter.js";
|
|
13
|
+
import { buildUsageFooter, subscriptionDetails, SESSION_USAGE_EXPLANATION } from "./usage-footer.js";
|
|
14
|
+
import { copyText, lastAssistantText } from "./clipboard-text.js";
|
|
15
|
+
import { withCommandFeedback, notifyStatus } from "./command-feedback.js";
|
|
16
|
+
import { loadModelsDev, cachedModelCatalog } from "../providers/models-dev.js";
|
|
17
|
+
import { modelEfforts } from "../providers/model-effort.js";
|
|
18
|
+
import { modelDetail } from "./model-details.js";
|
|
19
|
+
import { applyModelChoice, pickEffort } from "./model-selection.js";
|
|
20
|
+
import { PROVIDER_CATALOG } from "../providers/catalog.js";
|
|
21
|
+
import { fetchOpenRouterModels, openRouterDetail } from "../providers/openrouter.js";
|
|
22
|
+
import { providerRows, actionsFor, rowStatusLabel, providerKeyHint, saveProviderKey, saveProviderBaseUrl, removeProvider, } from "./provider-menu.js";
|
|
23
|
+
import { validateApiKey, looksLikeApiKey, startOAuthFlow, buildAuthUrlForPort, callbackUrlForPort } from "../providers/openrouter-auth.js";
|
|
24
|
+
import { orchestrateOAuth } from "../providers/oauth-orchestrator.js";
|
|
25
|
+
import { startCallbackServer } from "../providers/oauth-callback-server.js";
|
|
26
|
+
import { openUrl } from "../providers/open-browser.js";
|
|
27
|
+
import { runManualCodeFallback } from "../providers/openrouter-manual-fallback.js";
|
|
28
|
+
import { frostPaint, makeTheme, renderAssistantSegment, renderAssistantTurnEnd, renderAssistantTurnStart, renderInputPrompt, renderStartup, renderUserMessage, renderVascendStatus, renderVascendNotice, renderCastleBorn, fileLink, osc8, } from "./branding.js";
|
|
29
|
+
import { renderVascendTerminalLogo } from "./logo.js";
|
|
30
|
+
import { enterAccount } from "./account-entry.js";
|
|
31
|
+
import { tr } from "./i18n.js";
|
|
32
|
+
import { LANGUAGES, normalizePreference } from "./locales.js";
|
|
33
|
+
import { languagePreference, saveLanguagePreference } from "./language-preference.js";
|
|
34
|
+
import { leggiOnboarding } from "./onboarding.js";
|
|
35
|
+
import { buildTerminalHeader, buildTerminalWelcome } from "./status-line.js";
|
|
36
|
+
import { buildTerminalStartup, buildStartupRuntime, permissionLabel } from "./startup.js";
|
|
37
|
+
import { loadStatusExt, statusLineFiles } from "./status-line-loader.js";
|
|
38
|
+
import { createCostTracker } from "./cost-binding.js";
|
|
39
|
+
import { formatCost, formatTokenCount, promptTokens } from "./cost-tracker.js";
|
|
40
|
+
import { BottomInput } from "./bottom-input.js";
|
|
41
|
+
import { replaySession, sessionConversation } from "./session-replay.js";
|
|
42
|
+
export { resumeReplay } from "./session-replay.js";
|
|
43
|
+
import { PromptQueueFile, recoverableQueue } from "./prompt-queue-store.js";
|
|
44
|
+
import { restoreQueuedInput } from "./restore-queued-input.js";
|
|
45
|
+
import { ReviewStore } from "./review-store.js";
|
|
46
|
+
import { ReviewScreen, reviewText } from "./review-screen.js";
|
|
47
|
+
import { fitLine, oneLine, terminalWidth, wrapText } from "./terminal-text.js";
|
|
48
|
+
import { livePreview, liveResultSummary } from "./live-output.js";
|
|
49
|
+
import { commandSource, commandPreview, commandHeader, commandOutput, commandResultState } from "./command-transcript.js";
|
|
50
|
+
import { mcpName, mcpHeader, mcpPreview, mcpOutput } from "./mcp-transcript.js";
|
|
51
|
+
import { loadedSkill, skillHeader, skillPreview } from "./skill-transcript.js";
|
|
52
|
+
import { selectOption } from "./selector.js";
|
|
53
|
+
import { renderQuestion } from "./question.js";
|
|
54
|
+
export { filterOptions, pickerPageSize } from "./selector.js";
|
|
55
|
+
import { ImageRegistry, detectImageSource, imageTokenSpanAt } from "./image-attachments.js";
|
|
56
|
+
import { PasteRegistry, pasteTokenSpanAt, shouldCollapsePaste, } from "./paste-collapse.js";
|
|
57
|
+
import { readClipboardImage } from "./clipboard-image.js";
|
|
58
|
+
import { queryBackgroundMode } from "./term-bg.js";
|
|
59
|
+
import { runKingdomWarm, warmConcurrency, decomposeKingdom, looksBigGoal, collectConflicts, formatConflictList } from "../vascend/kingdom.js";
|
|
60
|
+
import { worktreeMode } from "../vascend/worktree.js";
|
|
61
|
+
import { runKingdomForHost } from "../vascend/kingdom-run.js";
|
|
62
|
+
import { kingdomBus } from "../vascend/kingdom-bus.js";
|
|
63
|
+
import { resolveKingdomBus } from "../vascend/kingdom-channel.js";
|
|
64
|
+
import { completenessEnabled, completenessRounds, judgeCompleteness, buildCompletenessSteer } from "../vascend/completeness.js";
|
|
65
|
+
import { selfImproveEnabled } from "../vascend/self-improve.js";
|
|
66
|
+
import { runKingdomGate, formatKingdomGate } from "../vascend/kingdom-kpi.js";
|
|
67
|
+
import { RoomMemory } from "../vascend/room-memory.js";
|
|
68
|
+
import { renderArchitectureMap, scanArchitectureMap } from "../vascend/architecture-map.js";
|
|
69
|
+
import { isAbsolute, resolve } from "node:path";
|
|
70
|
+
import { defaultThemeName, findTheme, loadThemes, parseThemeFile, } from "./theme.js";
|
|
71
|
+
import { findTemplate, loadTemplates, renderTemplate, } from "../prompts/templates.js";
|
|
72
|
+
import { findSkill, renderSkillInvocation } from "../skills/skills.js";
|
|
73
|
+
import { loginRows, loginStatusLabel } from "./login-menu.js";
|
|
74
|
+
import { VascendRuntime, validateKingdom, buildValidationSteer } from "../vascend/runtime.js";
|
|
75
|
+
import { VascendPlanStore } from "../vascend/plan-store.js";
|
|
76
|
+
import { reviewOptions, shouldReview, buildRoomMetricsDigest, buildReviewerPrompt } from "../vascend/reviewer.js";
|
|
77
|
+
import { Session, listSessions, sessionFilePath } from "../core/session.js";
|
|
78
|
+
import { sessionTitle } from "../core/session-title.js";
|
|
79
|
+
import { ToolRegistry } from "../tools/registry.js";
|
|
80
|
+
import { saveGlobalSettings } from "../core/settings.js";
|
|
81
|
+
import { autoApproves, nextPermMode, permModeLabel } from "./perm-mode.js";
|
|
82
|
+
import { backgroundManager } from "../core/background.js";
|
|
83
|
+
import { BUILTIN_COMMANDS, visibleBuiltinNames, commandsByCategory, suggestCommands } from "./commands.js";
|
|
84
|
+
import { TASK_STATUS_LABELS } from "./task-labels.js";
|
|
85
|
+
import { rimuoviLicenzaLocale, rimuoviTokenVascend, revocaSessione, tokenVascend, } from "./vascend-login.js";
|
|
86
|
+
import { paintToolName, paintCategoryName, pulseDot } from "./tool-style.js";
|
|
87
|
+
import { listVibeModes, parseVibeMode } from "../core/vibe-modes.js";
|
|
88
|
+
import { contextProjectKey, loadContextFrame, renderContextFrame, resetContextFrame, updateContextPins, } from "../vascend/context-fabric.js";
|
|
89
|
+
// (BUILTIN_COMMANDS spostato in ./commands.ts con categorie: vedi import in testa.)
|
|
90
|
+
// Sorgente dinamica dei nomi comando (builtin + estensioni + template + skill).
|
|
91
|
+
function allCommandNames(harness, templates) {
|
|
92
|
+
return [
|
|
93
|
+
...visibleBuiltinNames(),
|
|
94
|
+
...[...harness.host.commands.keys()],
|
|
95
|
+
...templates.map((t) => t.name),
|
|
96
|
+
...harness.skills.map((s) => `skill:${s.name}`),
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
// Modalita' Vascend di default nella TUI: sticky (nativo) salvo override.
|
|
100
|
+
function vascendDefaultMode(env) {
|
|
101
|
+
const v = (env["TALOS_VASCEND"] ?? "").trim().toLowerCase();
|
|
102
|
+
if (v === "off" || v === "0" || v === "false" || v === "no")
|
|
103
|
+
return "off";
|
|
104
|
+
return "sticky";
|
|
105
|
+
}
|
|
106
|
+
// Casella del giro attualmente in esecuzione nella TUI. Vive qui, a livello di
|
|
107
|
+
// modulo, perche' chi la riempie (il lettore di righe) e chi la consuma (il giro
|
|
108
|
+
// dell'agente) sono su due percorsi diversi e non si conoscono.
|
|
109
|
+
let activeTurnInbox = null;
|
|
110
|
+
export function beginActiveTurnInbox() {
|
|
111
|
+
activeTurnInbox = createUserInbox();
|
|
112
|
+
return activeTurnInbox;
|
|
113
|
+
}
|
|
114
|
+
export function endActiveTurnInbox() {
|
|
115
|
+
activeTurnInbox = null;
|
|
116
|
+
}
|
|
117
|
+
// Consegna una riga al giro in corso. I comandi (che cambiano lo stato della
|
|
118
|
+
// TUI, non il lavoro dell'agente) restano fuori: quelli aspettano il loro turno.
|
|
119
|
+
export function deliverToActiveTurn(raw, parts) {
|
|
120
|
+
const text = String(raw ?? "").trim();
|
|
121
|
+
if (!activeTurnInbox || !text || text.startsWith("/"))
|
|
122
|
+
return false;
|
|
123
|
+
activeTurnInbox.push(parts ?? text);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
// `readline.Interface` emette gli eventi `line` senza attendere le callback
|
|
127
|
+
// async. Questa coda garantisce un solo turno agente alla volta, mantiene la
|
|
128
|
+
// catena utilizzabile dopo un errore e centralizza prompt/quit/close per evitare
|
|
129
|
+
// doppi prompt o righe eseguite dopo la chiusura.
|
|
130
|
+
export function createSerialReadlineQueue(options) {
|
|
131
|
+
let tail = Promise.resolve();
|
|
132
|
+
let stopped = false;
|
|
133
|
+
let active = null;
|
|
134
|
+
const report = (error) => {
|
|
135
|
+
try {
|
|
136
|
+
options.onError?.(error);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Anche un renderer d'errore difettoso non deve avvelenare la coda.
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const runOne = async (raw) => {
|
|
143
|
+
if (stopped)
|
|
144
|
+
return;
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
active = controller;
|
|
147
|
+
let removeAbort;
|
|
148
|
+
let result = undefined;
|
|
149
|
+
try {
|
|
150
|
+
try {
|
|
151
|
+
removeAbort = options.installAbort?.(() => controller.abort());
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
report(error);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
result = await options.processLine(raw, controller.signal);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
// close() abortisce intenzionalmente il turno: non e' un errore da
|
|
161
|
+
// mostrare. Un errore ordinario, invece, viene riportato e la coda
|
|
162
|
+
// prosegue con la riga successiva.
|
|
163
|
+
if (!stopped && !controller.signal.aborted)
|
|
164
|
+
report(error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
try {
|
|
169
|
+
removeAbort?.();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
report(error);
|
|
173
|
+
}
|
|
174
|
+
if (active === controller)
|
|
175
|
+
active = null;
|
|
176
|
+
}
|
|
177
|
+
if (stopped)
|
|
178
|
+
return;
|
|
179
|
+
if (result === "quit") {
|
|
180
|
+
stopped = true;
|
|
181
|
+
try {
|
|
182
|
+
options.requestClose();
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
report(error);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
options.prompt();
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
report(error);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
return {
|
|
197
|
+
enqueue(raw) {
|
|
198
|
+
if (stopped)
|
|
199
|
+
return tail;
|
|
200
|
+
// Turno in corso: se la riga puo' essere consegnata al giro che sta
|
|
201
|
+
// lavorando, ci va subito invece di aspettare in coda la fine della
|
|
202
|
+
// risposta. I comandi e tutto cio' che l'host non accetta restano in coda.
|
|
203
|
+
if (active && options.deliverToActive?.(raw))
|
|
204
|
+
return tail;
|
|
205
|
+
// `tail` viene sempre normalizzata a una Promise risolta: anche una
|
|
206
|
+
// callback inattesa che lanci non puo' bloccare le righe future.
|
|
207
|
+
const scheduled = tail.then(() => runOne(raw));
|
|
208
|
+
tail = scheduled.catch((error) => report(error));
|
|
209
|
+
return tail;
|
|
210
|
+
},
|
|
211
|
+
close() {
|
|
212
|
+
if (stopped)
|
|
213
|
+
return;
|
|
214
|
+
stopped = true;
|
|
215
|
+
active?.abort();
|
|
216
|
+
},
|
|
217
|
+
idle() {
|
|
218
|
+
return tail;
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export async function startRepl(deps) {
|
|
223
|
+
if (deps.recoverQueue && !deps.harness.session.head) {
|
|
224
|
+
const recovered = recoverableQueue(deps.harness.config.sessionDir);
|
|
225
|
+
if (recovered)
|
|
226
|
+
deps.harness.loadSession(recovered);
|
|
227
|
+
}
|
|
228
|
+
const { harness } = deps;
|
|
229
|
+
const env = harness.env;
|
|
230
|
+
let commandNames = [];
|
|
231
|
+
// Superficie di commit: instrada l'output (incluse le notify delle estensioni)
|
|
232
|
+
// nella regione viva quando la TUI ancorata e' attiva, altrimenti su stdout.
|
|
233
|
+
// Riassegnata dopo la creazione del BottomInput.
|
|
234
|
+
let commit = (t) => stdout.write(t);
|
|
235
|
+
// Modalita' TUI per le estensioni + UI reale (notify nella regione viva).
|
|
236
|
+
harness.host.bind({
|
|
237
|
+
mode: "tui",
|
|
238
|
+
ui: {
|
|
239
|
+
confirm: async () => false,
|
|
240
|
+
select: async () => null,
|
|
241
|
+
input: async () => null,
|
|
242
|
+
notify: (m, t) => commit(`\n ${t === "error" ? "✗" : t === "warn" ? "!" : "·"} ${m}\n`),
|
|
243
|
+
setStatus: () => { },
|
|
244
|
+
setWidget: () => { },
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
await harness.host.emit("session_start", { reason: "startup" });
|
|
248
|
+
// Tema: builtin + custom dalle dir; default da auto-detect background. Le
|
|
249
|
+
// dir di progetto richiedono project trust.
|
|
250
|
+
const store = loadThemes(harness.config.cwd, env, harness.projectTrusted, harness.packages.themeDirs);
|
|
251
|
+
let activeName = harness.config.ui.theme || defaultThemeName(env);
|
|
252
|
+
let activeSpec = findTheme(store.themes, activeName) ?? findTheme(store.themes, defaultThemeName(env));
|
|
253
|
+
let theme = makeTheme(harness.config.ui, activeSpec);
|
|
254
|
+
const themeCtl = {
|
|
255
|
+
available: store.themes,
|
|
256
|
+
activeName: () => activeName,
|
|
257
|
+
spec: () => activeSpec,
|
|
258
|
+
apply: (name) => {
|
|
259
|
+
const spec = findTheme(store.themes, name);
|
|
260
|
+
if (!spec)
|
|
261
|
+
return false;
|
|
262
|
+
activeName = name;
|
|
263
|
+
activeSpec = spec;
|
|
264
|
+
theme = makeTheme(harness.config.ui, spec);
|
|
265
|
+
// Persiste la scelta nel config globale: sopravvive al riavvio.
|
|
266
|
+
saveGlobalSettings(env, { ui: { theme: name } });
|
|
267
|
+
return true;
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
// Hot reload: aggiorna il tema attivo quando il suo file cambia. Niente
|
|
271
|
+
// stampa: una scrittura asincrona corromperebbe l'area di contenuto mentre
|
|
272
|
+
// l'utente digita; il footer riflette il nuovo tema al prossimo ridisegno.
|
|
273
|
+
for (const [name, path] of Object.entries(store.paths)) {
|
|
274
|
+
try {
|
|
275
|
+
watch(path, { persistent: false }, () => {
|
|
276
|
+
const fresh = parseThemeFile(path);
|
|
277
|
+
if (fresh && name === activeName) {
|
|
278
|
+
activeSpec = fresh;
|
|
279
|
+
theme = makeTheme(harness.config.ui, fresh);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// watch non disponibile: hot reload best-effort
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Theme auto-mode: se il tema non e' fissato (vuoto o "auto"), segue il
|
|
288
|
+
// background del terminale e si ri-rileva sui cambi (resize). Non persiste:
|
|
289
|
+
// resta una scelta automatica, non un override esplicito dell'utente.
|
|
290
|
+
const autoTheme = !harness.config.ui.theme || harness.config.ui.theme === "auto";
|
|
291
|
+
if (autoTheme && stdout.isTTY) {
|
|
292
|
+
stdout.on("resize", () => {
|
|
293
|
+
const want = defaultThemeName(env);
|
|
294
|
+
if (want === activeName)
|
|
295
|
+
return;
|
|
296
|
+
const spec = findTheme(store.themes, want);
|
|
297
|
+
if (spec) {
|
|
298
|
+
activeName = want;
|
|
299
|
+
activeSpec = spec;
|
|
300
|
+
theme = makeTheme(harness.config.ui, spec);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
// Prompt templates: discovery dalle dir + path extra via --prompt-template.
|
|
305
|
+
const extraTemplatePaths = [
|
|
306
|
+
...(env["TALOS_PROMPT_TEMPLATES"] ?? "").split(",").map((p) => p.trim()).filter(Boolean),
|
|
307
|
+
...harness.packages.prompts,
|
|
308
|
+
];
|
|
309
|
+
const templates = loadTemplates(harness.config.cwd, extraTemplatePaths, harness.projectTrusted);
|
|
310
|
+
// Vascend NATIVO: sticky attivo di default, senza dover digitare /vascend on.
|
|
311
|
+
// Si disattiva con TALOS_VASCEND=off|0|false oppure a runtime con /vascend off.
|
|
312
|
+
const vascend = new VascendRuntime(harness.config.cwd, vascendDefaultMode(env));
|
|
313
|
+
commandNames = allCommandNames(harness, templates);
|
|
314
|
+
// Modalita' permessi (Shift+Tab): normale -> accetta modifiche -> auto.
|
|
315
|
+
// Inizializzata dalla modalita' PERSISTITA nel config globale (ricaricata
|
|
316
|
+
// all'avvio): se l'ultima sessione era in "auto", riparte in "auto". Default
|
|
317
|
+
// "normal" come Claude Code.
|
|
318
|
+
let permMode = harness.config.permMode;
|
|
319
|
+
const getPermMode = () => permMode;
|
|
320
|
+
const terminalColumns = () => stdout.columns || 100;
|
|
321
|
+
// Ultimo usage per il tasso di cache mostrato nel footer.
|
|
322
|
+
const costTracker = createCostTracker(env);
|
|
323
|
+
const stats = new SessionUsageState(costTracker);
|
|
324
|
+
stats.restore(harness.config.sessionDir, harness.session.id, harness.session.tree().find(n => n.message.role === "assistant")?.ts);
|
|
325
|
+
const review = new ReviewStore();
|
|
326
|
+
review.reset(sessionConversation(harness.session));
|
|
327
|
+
const reviewContext = () => {
|
|
328
|
+
const cost = costTracker.snapshot(harness.config.model);
|
|
329
|
+
const tasks = vascend.state().kingdom.castles.flatMap(c => c.tasks);
|
|
330
|
+
return {
|
|
331
|
+
cwd: harness.config.cwd, model: harness.config.model,
|
|
332
|
+
permissions: permModeLabel(permMode), sessionId: harness.session.id,
|
|
333
|
+
usage: `Sessione · ${stats.incomplete || stats.missing ? "almeno " : ""}${formatTokenCount(cost.totalProviderTokens)} token · input ${formatTokenCount(cost.totalPromptTokens)} · output ${formatTokenCount(cost.totalOutputTokens)}` +
|
|
334
|
+
(cost.anyPriced ? ` · ${formatCost(cost.totalCost)}` : ""),
|
|
335
|
+
context: `~${formatTokenCount(stimaSessione().tokens)} ${tr("token stimati localmente · /context per i dettagli", "locally estimated tokens · /context for details")}`,
|
|
336
|
+
plan: tasks.length ? `${tasks.filter(t => t.status === "completed").length}/${tasks.length} attività completate · /piano per il dettaglio` : undefined,
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
const stimaSessione = () => {
|
|
340
|
+
const reading = stats.context.read(harness);
|
|
341
|
+
return { ...reading, nodi: reading.nodes };
|
|
342
|
+
};
|
|
343
|
+
const uiMeta = (status) => {
|
|
344
|
+
const current = harness.providers.resolve(harness.config.model);
|
|
345
|
+
const stima = stimaSessione();
|
|
346
|
+
const last = stats.last;
|
|
347
|
+
const cacheHitRate = last && last.cacheReadTokens !== undefined
|
|
348
|
+
? last.cacheReadTokens / Math.max(1, last.inputTokens + last.cacheReadTokens)
|
|
349
|
+
: undefined;
|
|
350
|
+
return {
|
|
351
|
+
provider: current ? (current.provider.label ?? current.provider.id) : "non configurato",
|
|
352
|
+
sessionId: harness.session.id,
|
|
353
|
+
sessionNodes: stima.nodi,
|
|
354
|
+
headId: harness.session.head,
|
|
355
|
+
status,
|
|
356
|
+
contextTokens: stima.tokens,
|
|
357
|
+
contextWindow: stima.window,
|
|
358
|
+
contextArchive: stima.archived,
|
|
359
|
+
cacheHitRate,
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
// Hint dei parametri attesi da un comando (in grigio quando il comando e'
|
|
363
|
+
// gia' completo).
|
|
364
|
+
const argHintFor = (name) => {
|
|
365
|
+
const b = BUILTIN_COMMANDS.find((c) => c.name === name);
|
|
366
|
+
if (b)
|
|
367
|
+
return b.hint;
|
|
368
|
+
const t = templates.find((tp) => tp.name === name);
|
|
369
|
+
if (t)
|
|
370
|
+
return t.argumentHint;
|
|
371
|
+
if (name.startsWith("skill:"))
|
|
372
|
+
return "[argomenti]";
|
|
373
|
+
return "";
|
|
374
|
+
};
|
|
375
|
+
// Suggerimento slash: stringa stilizzata (senza newline) mostrata a destra
|
|
376
|
+
// del cursore dall'editor in fondo.
|
|
377
|
+
const slashHint = (line) => {
|
|
378
|
+
if (!line.startsWith("/"))
|
|
379
|
+
return "";
|
|
380
|
+
if (line.includes(" ")) {
|
|
381
|
+
const name = line.slice(1, line.indexOf(" "));
|
|
382
|
+
const typed = line.slice(line.indexOf(" ") + 1);
|
|
383
|
+
const ah = argHintFor(name);
|
|
384
|
+
return ah && !typed.trim() ? theme.dim(` ${ah}`) : "";
|
|
385
|
+
}
|
|
386
|
+
const frag = line.slice(1).toLowerCase();
|
|
387
|
+
const matches = commandNames.filter((n) => n.toLowerCase().startsWith(frag));
|
|
388
|
+
if (!matches.length)
|
|
389
|
+
return "";
|
|
390
|
+
const exact = matches.find((m) => `/${m}` === line);
|
|
391
|
+
if (exact) {
|
|
392
|
+
const ah = argHintFor(exact);
|
|
393
|
+
const others = matches.filter((m) => `/${m}` !== line).slice(0, 6).map((m) => `/${m}`);
|
|
394
|
+
const parts = [ah ? ` ${ah}` : "", others.length ? ` ${others.join(" ")}` : ""].filter(Boolean).join("");
|
|
395
|
+
return parts ? theme.dim(parts) : "";
|
|
396
|
+
}
|
|
397
|
+
return theme.dim(" " + matches.slice(0, 8).map((m) => `/${m}`).join(" ") + " · Tab");
|
|
398
|
+
};
|
|
399
|
+
// Completamento Tab: ritorna il nuovo buffer (miglior match) o null.
|
|
400
|
+
const completion = (line) => {
|
|
401
|
+
if (!line.startsWith("/") || line.includes(" "))
|
|
402
|
+
return null;
|
|
403
|
+
const frag = line.slice(1).toLowerCase();
|
|
404
|
+
const matches = commandNames.filter((c) => c.toLowerCase().startsWith(frag));
|
|
405
|
+
const best = matches[0];
|
|
406
|
+
if (!best)
|
|
407
|
+
return null;
|
|
408
|
+
const full = `/${best}`;
|
|
409
|
+
return full.toLowerCase() === line.toLowerCase() ? null : full;
|
|
410
|
+
};
|
|
411
|
+
const startupMeta = () => {
|
|
412
|
+
const resolved = harness.providers.resolve(harness.config.model);
|
|
413
|
+
return {
|
|
414
|
+
provider: resolved ? (resolved.provider.label ?? resolved.provider.id) : "non configurato",
|
|
415
|
+
sessionId: harness.session.id,
|
|
416
|
+
sessionNodes: harness.session.tree().length,
|
|
417
|
+
headId: harness.session.head,
|
|
418
|
+
};
|
|
419
|
+
};
|
|
420
|
+
const startupBanner = () => renderStartup(harness.config, startupMeta(), { columns: terminalColumns(), compact: true }, activeSpec);
|
|
421
|
+
// Nota d'accesso da mostrare nel banner (impostata dall'ingresso pre-TUI).
|
|
422
|
+
let accessoNota = "";
|
|
423
|
+
// Ingresso PRIMA di qualsiasi UI (stile Claude Code/Codex), in tre passi:
|
|
424
|
+
// 1) lingua;
|
|
425
|
+
// 2) termini d'uso, con i collegamenti ufficiali di vascend.it (gli stessi
|
|
426
|
+
// mostrati nelle email e nella schermata di accesso di Horizon);
|
|
427
|
+
// 3) accesso con l'account Vascend (lo stesso di Horizon).
|
|
428
|
+
// Lingua e termini si chiedono UNA volta (persistiti e versionati in
|
|
429
|
+
// ~/.talos/onboarding.json); il login solo quando non c'e' una sessione
|
|
430
|
+
// valida. Dentro l'host Horizon l'utente e' GIA' autenticato dall'app;
|
|
431
|
+
// script/test saltano con TALOS_NO_LOGIN=1; senza TTY niente flusso
|
|
432
|
+
// interattivo. Rete giu': conta SOLO la prova offline firmata (Ed25519,
|
|
433
|
+
// chiave pubblica compilata, orologio ancorato): una "sessione" scritta a
|
|
434
|
+
// mano nel config non supera la firma.
|
|
435
|
+
const ingressoPrimaDellaTui = async () => {
|
|
436
|
+
const note = await enterAccount(env);
|
|
437
|
+
accessoNota = note ? theme.muted(" " + note) : "";
|
|
438
|
+
};
|
|
439
|
+
// Registro degli allegati immagine della sessione TUI: incollare un path/data
|
|
440
|
+
// URI immagine (o usare /img) crea un riferimento compatto "img #N" (stile
|
|
441
|
+
// Claude) e ne mostra l'anteprima inline quando il messaggio viene inviato.
|
|
442
|
+
const imageRegistry = new ImageRegistry();
|
|
443
|
+
// Stesso principio per il testo incollato: un blocco grande diventa "txt #N"
|
|
444
|
+
// e il contenuto vero resta qui fino all'invio. I riferimenti valgono per
|
|
445
|
+
// questa sessione soltanto: dopo /new, `txt #1` sarebbe un altro testo.
|
|
446
|
+
const pasteRegistry = new PasteRegistry();
|
|
447
|
+
const resetSessionUi = (ctl) => {
|
|
448
|
+
ctl.clearConversation?.();
|
|
449
|
+
review.reset(sessionConversation(harness.session));
|
|
450
|
+
imageRegistry.clear();
|
|
451
|
+
pasteRegistry.reset();
|
|
452
|
+
ctl.restoreQueue?.();
|
|
453
|
+
stats.restore(harness.config.sessionDir, harness.session.id, harness.session.tree().find(n => n.message.role === "assistant")?.ts);
|
|
454
|
+
for (const channel of collapsibleChannels.values())
|
|
455
|
+
ctl.removeFold(channel.foldId);
|
|
456
|
+
collapsibleChannels.clear();
|
|
457
|
+
ctl.setKingdomBoard(null);
|
|
458
|
+
ctl.setKingdomVassals(null);
|
|
459
|
+
ctl.setTokens(0, 0, 0);
|
|
460
|
+
ctl.refresh?.();
|
|
461
|
+
};
|
|
462
|
+
// Hook di incollo del BottomInput. Due trasformazioni, stessa idea: un
|
|
463
|
+
// allegato grande non deve invadere la riga di input.
|
|
464
|
+
// - immagine (path esistente o data URI) -> "img #N";
|
|
465
|
+
// - testo lungo -> "txt #N", col contenuto vero tenuto da parte e rimesso
|
|
466
|
+
// al suo posto solo all'invio.
|
|
467
|
+
const onPasteInput = (text) => {
|
|
468
|
+
const src = detectImageSource(text, harness.config.cwd);
|
|
469
|
+
if (src)
|
|
470
|
+
return imageRegistry.add(src).ref;
|
|
471
|
+
if (!shouldCollapsePaste(text))
|
|
472
|
+
return null;
|
|
473
|
+
return pasteRegistry.add(text).ref;
|
|
474
|
+
};
|
|
475
|
+
// Hook Alt+V del BottomInput: legge i byte dell'immagine dalla CLIPBOARD DI
|
|
476
|
+
// SISTEMA (i terminali non mandano binari via stdin) e la registra come "img
|
|
477
|
+
// #N". L'allegato compare come riferimento evidenziato nel testo di input.
|
|
478
|
+
// Ritorna il riferimento da inserire nel
|
|
479
|
+
// buffer, o null se non c'era un'immagine (mostrando un avviso esplicativo).
|
|
480
|
+
const onCaptureImage = async (ctl) => {
|
|
481
|
+
const res = await readClipboardImage();
|
|
482
|
+
if (!res.ok) {
|
|
483
|
+
if (res.reason !== "empty") {
|
|
484
|
+
notifyStatus(ctl, theme.yellow(`\n clipboard: ${res.detail}\n`));
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
notifyStatus(ctl, theme.dim(`\n clipboard: ${res.detail}\n`));
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const att = imageRegistry.addData(res.bytes, res.mime, "clipboard");
|
|
492
|
+
return att.ref;
|
|
493
|
+
};
|
|
494
|
+
// Dispatch di una riga (slash o prompt). Ritorna "quit" per uscire. Tutto il
|
|
495
|
+
// corpo e' protetto: un errore non deve uccidere la sessione (era la causa di
|
|
496
|
+
// "sessione terminata (codice undefined)").
|
|
497
|
+
const processLine = async (raw, ctl, signal, fromQueue = false, queued) => {
|
|
498
|
+
const line = raw.trim();
|
|
499
|
+
if (!line)
|
|
500
|
+
return;
|
|
501
|
+
try {
|
|
502
|
+
if (!line.startsWith("/")) {
|
|
503
|
+
ctl.write(renderUserMessage(line, theme, terminalColumns(), fromQueue ? tr("Dalla coda · consegnato", "From queue · delivered") : undefined));
|
|
504
|
+
const imageParts = queued ? queued.parts.filter(part => part.type === "image") : imageRegistry.imagePartsFor(line);
|
|
505
|
+
// In chat resta il riferimento compatto (`txt #1`); al modello va il
|
|
506
|
+
// testo per intero. L'espansione avviene qui, dopo l'echo e prima del
|
|
507
|
+
// giro: e' l'ultimo punto in cui le due viste possono divergere.
|
|
508
|
+
const prepared = vascend.prepareUserInput(queued ? pendingText(queued) : pasteRegistry.expand(line));
|
|
509
|
+
await drive(deps, prepared.input, ctl, theme, terminalColumns, stats, signal, getPermMode, prepared.active ? vascend : undefined, line, imageParts);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const reviewCommand = /^\/(?:review|revisione)(?:\s+(.*))?$/i.exec(line);
|
|
513
|
+
if (reviewCommand) {
|
|
514
|
+
const section = (reviewCommand[1] ?? "").trim();
|
|
515
|
+
if (ctl.showReview)
|
|
516
|
+
ctl.showReview(section);
|
|
517
|
+
else
|
|
518
|
+
ctl.write(reviewText(review, reviewContext(), theme, terminalColumns(), section));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
// /img <path>: allega esplicitamente un'immagine. La registra come "img #N"
|
|
522
|
+
// e ne mostra l'anteprima inline; non avvia un turno (e' solo l'allegato).
|
|
523
|
+
const imgCmd = /^\/img(?:\s+(.+))?$/i.exec(line);
|
|
524
|
+
if (imgCmd) {
|
|
525
|
+
const arg = (imgCmd[1] ?? "").trim();
|
|
526
|
+
if (!arg) {
|
|
527
|
+
// Senza argomento: leggi l'immagine dalla CLIPBOARD DI SISTEMA. E' il
|
|
528
|
+
// fallback affidabile ad Alt+V quando il terminale non inoltra la
|
|
529
|
+
// combinazione (Windows Terminal/VS Code la intercettano spesso).
|
|
530
|
+
const ref = await onCaptureImage(ctl);
|
|
531
|
+
if (ref)
|
|
532
|
+
notifyStatus(ctl, theme.accent(`\n + ${ref}`) + theme.dim(" · citalo nel messaggio per usarlo\n"));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const src = detectImageSource(arg, harness.config.cwd);
|
|
536
|
+
if (!src) {
|
|
537
|
+
notifyStatus(ctl, theme.yellow(`\n non riconosciuta come immagine: ${truncate(arg, 60)}\n`) +
|
|
538
|
+
theme.dim(" serve un path a un file immagine esistente (png/jpg/gif/webp/bmp) o un data URI.\n"));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const att = imageRegistry.add(src);
|
|
542
|
+
notifyStatus(ctl, theme.accent(`\n + ${att.ref}`) + theme.dim(` · ${att.label} — citalo nel messaggio per usarlo\n`));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// "/" da solo apre la palette dei comandi (stessa via di /commands).
|
|
546
|
+
const handled = await handleSlash(line === "/" ? "/commands" : line, deps, ctl, theme, terminalColumns, themeCtl, templates, vascend, stats, signal, () => resetSessionUi(ctl));
|
|
547
|
+
if (handled === "quit")
|
|
548
|
+
return "quit";
|
|
549
|
+
if (handled !== null) {
|
|
550
|
+
await drive(deps, handled.input, ctl, theme, terminalColumns, stats, signal, getPermMode, handled.vascend ? vascend : undefined);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
ctl.pauseQueuedMessages?.();
|
|
555
|
+
notifyStatus(ctl, `Errore: ${err.message}`, true);
|
|
556
|
+
}
|
|
557
|
+
finally {
|
|
558
|
+
// Fine del giro: da qui in poi le righe tornano a fare turni nuovi.
|
|
559
|
+
endActiveTurnInbox();
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
// L'ingresso viene PRIMA di qualsiasi UI: lingua, termini d'uso e accesso
|
|
563
|
+
// (schermata dedicata, stile Claude Code). Da qui in poi l'utente ha scelto
|
|
564
|
+
// la lingua, accettato i termini ed e' autenticato (o esente: host Horizon,
|
|
565
|
+
// script, no-TTY).
|
|
566
|
+
await ingressoPrimaDellaTui();
|
|
567
|
+
// Tema auto: rileva il background REALE del terminale via OSC 11 (piu'
|
|
568
|
+
// preciso dell'euristica env). Va fatto prima di avviare l'editor (che prende
|
|
569
|
+
// possesso di stdin). Fallback: l'euristica gia' applicata sopra.
|
|
570
|
+
if (autoTheme && BottomInput.supported() && env["TALOS_CLASSIC_INPUT"] !== "1") {
|
|
571
|
+
const mode = await queryBackgroundMode();
|
|
572
|
+
const want = mode === "light" ? "vascend-light" : mode === "dark" ? "vascend-dark" : null;
|
|
573
|
+
if (want && want !== activeName) {
|
|
574
|
+
const spec = findTheme(store.themes, want);
|
|
575
|
+
if (spec) {
|
|
576
|
+
activeName = want;
|
|
577
|
+
activeSpec = spec;
|
|
578
|
+
theme = makeTheme(harness.config.ui, spec);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// Barra di input ANCORATA in fondo (default in TTY): contenuto scrollabile
|
|
583
|
+
// sopra, footer + input fissi in basso, a tutto terminale.
|
|
584
|
+
if (BottomInput.supported() && env["TALOS_CLASSIC_INPUT"] !== "1") {
|
|
585
|
+
// Carica un eventuale regno persistito (sessione ripresa) cosi' la status
|
|
586
|
+
// line lo mostra gia' prima del primo turno.
|
|
587
|
+
vascend.loadPlan(harness.config.sessionDir, harness.session.id);
|
|
588
|
+
// Status line: ordine dei segmenti da config.json (statusLine) + estensione
|
|
589
|
+
// utente (.talos/status-line.js) per segmenti custom o override. Entrambi
|
|
590
|
+
// ricaricabili A CALDO via file watch.
|
|
591
|
+
let statusOrder = harness.config.statusLine;
|
|
592
|
+
let statusExt = await loadStatusExt(harness.config.cwd, harness.projectTrusted);
|
|
593
|
+
const statusOpts = () => ({ order: statusOrder, custom: statusExt.custom, render: statusExt.render });
|
|
594
|
+
// Hot reload del file estensione.
|
|
595
|
+
for (const f of statusLineFiles(harness.config.cwd, harness.projectTrusted)) {
|
|
596
|
+
try {
|
|
597
|
+
watch(f, { persistent: false }, () => {
|
|
598
|
+
void loadStatusExt(harness.config.cwd, harness.projectTrusted).then((ext) => { statusExt = ext; });
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
// il file potrebbe non esistere ancora: watch best-effort
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// Hot reload di statusLine da config.json (rilegge l'ordine dei segmenti).
|
|
606
|
+
for (const cf of harness.config.loadedConfigFiles) {
|
|
607
|
+
try {
|
|
608
|
+
watch(cf, { persistent: false }, () => {
|
|
609
|
+
try {
|
|
610
|
+
const raw = JSON.parse(readFileSync(cf, "utf8"));
|
|
611
|
+
if (Array.isArray(raw.statusLine))
|
|
612
|
+
statusOrder = raw.statusLine.map(String);
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
// file in scrittura: ignora
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
// best effort
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const statusData = () => {
|
|
624
|
+
const m = uiMeta("pronto");
|
|
625
|
+
const castles = vascend.state().kingdom.castles;
|
|
626
|
+
const all = castles.flatMap((c) => c.tasks);
|
|
627
|
+
const regno = castles.length
|
|
628
|
+
? { done: all.filter((t) => t.status === "completed").length, total: all.length, castles: castles.length }
|
|
629
|
+
: null;
|
|
630
|
+
return {
|
|
631
|
+
model: harness.config.model,
|
|
632
|
+
provider: m.provider,
|
|
633
|
+
cwd: harness.config.cwd,
|
|
634
|
+
effort: harness.config.effort || "default",
|
|
635
|
+
fast: Boolean(harness.config.fast && harness.providers.resolve(harness.config.model)?.provider.supportsFastMode),
|
|
636
|
+
vibeMode: harness.config.vibeMode,
|
|
637
|
+
permissions: permMode,
|
|
638
|
+
sessionLabel: harness.session.name || undefined,
|
|
639
|
+
contextTokens: m.contextTokens,
|
|
640
|
+
contextWindow: m.contextWindow,
|
|
641
|
+
contextArchive: m.contextArchive,
|
|
642
|
+
regno,
|
|
643
|
+
cost: costTracker.snapshot(harness.config.model),
|
|
644
|
+
bg: (() => {
|
|
645
|
+
const l = backgroundManager.list();
|
|
646
|
+
return l.length ? { running: l.filter((t) => t.status === "running").length, total: l.length } : null;
|
|
647
|
+
})(),
|
|
648
|
+
width: terminalColumns(),
|
|
649
|
+
};
|
|
650
|
+
};
|
|
651
|
+
const input = new BottomInput({
|
|
652
|
+
review,
|
|
653
|
+
createReviewScreen: (section) => new ReviewScreen(review, () => theme, reviewContext, section),
|
|
654
|
+
nativeScroll: env["TALOS_NATIVE_SCROLL"] !== "0",
|
|
655
|
+
headerLines: () => buildTerminalHeader(statusData(), theme),
|
|
656
|
+
startupLines: () => buildTerminalStartup(statusData(), theme),
|
|
657
|
+
promptText: () => theme.accent(harness.config.ui.prompt),
|
|
658
|
+
placeholder: () => theme.muted(tr("Chiedi a Talos di fare qualcosa", "Ask Talos to do anything")),
|
|
659
|
+
composerPadding: () => !harness.session.head,
|
|
660
|
+
composerStyle: (s) => theme.composerText?.(s) ?? theme.chatText(s),
|
|
661
|
+
attachmentStyle: (s) => theme.attachmentRef?.(s) ?? s,
|
|
662
|
+
belowInputLines: () => {
|
|
663
|
+
if (!harness.session.head)
|
|
664
|
+
return [buildStartupRuntime(statusData(), theme)];
|
|
665
|
+
const providerId = harness.providers.resolve(harness.config.model)?.provider.id ?? "";
|
|
666
|
+
return buildUsageFooter(statusData(), theme, stats, providerId, statusOpts()).slice(1);
|
|
667
|
+
},
|
|
668
|
+
footerLines: () => {
|
|
669
|
+
if (!harness.session.head)
|
|
670
|
+
return [];
|
|
671
|
+
const providerId = harness.providers.resolve(harness.config.model)?.provider.id ?? "";
|
|
672
|
+
return buildUsageFooter(statusData(), theme, stats, providerId, statusOpts()).slice(0, 1);
|
|
673
|
+
},
|
|
674
|
+
completion,
|
|
675
|
+
hint: slashHint,
|
|
676
|
+
statusStyle: (s) => theme.dark(s),
|
|
677
|
+
boxStyle: (s) => theme.chatBorder(s),
|
|
678
|
+
onHotkey: (name) => (name === "todo" || name === "regno" ? "/todo" : null),
|
|
679
|
+
// Ctrl+G in ogni momento: snapshot del piano con la stanza corrente
|
|
680
|
+
// evidenziata (funziona anche mentre l'agente lavora).
|
|
681
|
+
onShowPlan: () => {
|
|
682
|
+
vascend.loadPlan(harness.config.sessionDir, harness.session.id);
|
|
683
|
+
input.write(renderPlanSnapshot(vascend.state(), theme, terminalColumns()));
|
|
684
|
+
},
|
|
685
|
+
onCycleMode: () => {
|
|
686
|
+
permMode = nextPermMode(permMode);
|
|
687
|
+
// Persiste la scelta nel config globale: la modalita' (es. "auto")
|
|
688
|
+
// sopravvive al riavvio, come modello/effort/tema.
|
|
689
|
+
saveGlobalSettings(env, { permMode });
|
|
690
|
+
input.setRoomStatus(tr("Permessi: {mode}", "Permissions: {mode}", { mode: permissionLabel(permMode) }), "done");
|
|
691
|
+
},
|
|
692
|
+
// Ctrl+B: manda in background il comando shell in corso; se non ce n'e'
|
|
693
|
+
// uno, mostra la lista dei task in background.
|
|
694
|
+
onBackground: () => {
|
|
695
|
+
const task = backgroundManager.requestBackground();
|
|
696
|
+
if (task)
|
|
697
|
+
input.write(theme.dim(`\n ⤓ in background [${task.id}]: ${truncate(task.command, 60)}\n`));
|
|
698
|
+
else
|
|
699
|
+
void showBackgroundPanel(input, theme, terminalColumns);
|
|
700
|
+
},
|
|
701
|
+
// Suffisso spinner: suggerisce Ctrl+B solo quando c'e' un comando in corso.
|
|
702
|
+
runningHint: () => backgroundManager.hasForeground() ? "Ctrl+B sfondo" : null,
|
|
703
|
+
prepareQueuedInput: text => [{ type: "text", text: pasteRegistry.expand(text) }, ...imageRegistry.imagePartsFor(text)],
|
|
704
|
+
queuePersistence: () => new PromptQueueFile(harness.config.sessionDir, harness.session.id),
|
|
705
|
+
restoreQueuedInput: entry => restoreQueuedInput(entry, imageRegistry),
|
|
706
|
+
queueStyle: { heading: s => theme.accent(s), muted: s => theme.muted(s), selected: s => theme.chatText(s), text: s => theme.silver(s) },
|
|
707
|
+
onSubmit: async (line, signal, fromQueue, queued) => {
|
|
708
|
+
const result = await processLine(line, input, signal, fromQueue, queued);
|
|
709
|
+
if (result === "quit")
|
|
710
|
+
input.requestQuit();
|
|
711
|
+
},
|
|
712
|
+
// Incollo: un path/data-URI immagine diventa "img #N", un testo lungo
|
|
713
|
+
// diventa "txt #N", il resto si inserisce come testo normale.
|
|
714
|
+
onPaste: onPasteInput,
|
|
715
|
+
// Alt+V: cattura l'immagine dalla clipboard di sistema -> "img #N".
|
|
716
|
+
onCaptureImage: () => onCaptureImage(input),
|
|
717
|
+
// Backspace su un token "img #N" o "txt #N": cancellazione atomica (tolgo
|
|
718
|
+
// tutto il riferimento) cosi' l'allegato si scollega per intero in un colpo.
|
|
719
|
+
onBackspaceToken: (buffer, cursor) => {
|
|
720
|
+
const span = imageTokenSpanAt(buffer, cursor) ?? pasteTokenSpanAt(buffer, cursor);
|
|
721
|
+
return span ? { start: span.start, end: span.end } : null;
|
|
722
|
+
},
|
|
723
|
+
onReady: () => {
|
|
724
|
+
input.write(buildTerminalWelcome(terminalColumns(), theme));
|
|
725
|
+
if (accessoNota)
|
|
726
|
+
input.write(wrapText(accessoNota, Math.max(2, terminalColumns() - 3), true).map((line) => theme.muted(" " + line.trim())).join("\n") + "\n");
|
|
727
|
+
input.write("\n");
|
|
728
|
+
if (harness.session.head)
|
|
729
|
+
replaySession(harness.session, input, theme, terminalColumns());
|
|
730
|
+
if (deps.resumePicker)
|
|
731
|
+
void processLine("/resume", input, new AbortController().signal);
|
|
732
|
+
},
|
|
733
|
+
onQuit: () => {
|
|
734
|
+
void harness.host.emit("session_shutdown", {});
|
|
735
|
+
stdout.write("\n");
|
|
736
|
+
process.exit(0);
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
// Le notify delle estensioni vanno nella regione viva (no corruzione).
|
|
740
|
+
commit = (t) => input.write(t);
|
|
741
|
+
// UI reale per le estensioni: confirm/select/input passano dalla TUI (cosi'
|
|
742
|
+
// un'estensione puo' fare domande in chat come il tool builtin ask_user).
|
|
743
|
+
harness.host.bind({
|
|
744
|
+
mode: "tui",
|
|
745
|
+
ui: {
|
|
746
|
+
confirm: async (title, message) => {
|
|
747
|
+
const c = await selectOption({
|
|
748
|
+
title,
|
|
749
|
+
hint: message,
|
|
750
|
+
options: [
|
|
751
|
+
{ key: "yes", label: "Sì", detail: "", value: true },
|
|
752
|
+
{ key: "no", label: "No", detail: "", value: false },
|
|
753
|
+
],
|
|
754
|
+
currentKey: "yes",
|
|
755
|
+
ctl: input, theme, terminalColumns,
|
|
756
|
+
});
|
|
757
|
+
return c?.value === true;
|
|
758
|
+
},
|
|
759
|
+
select: async (title, items) => {
|
|
760
|
+
const c = await selectOption({
|
|
761
|
+
title,
|
|
762
|
+
hint: "↑/↓ scegli · Enter conferma · Esc annulla",
|
|
763
|
+
options: items.map((it, i) => ({ key: String(i), label: it.label, detail: "", value: it.value })),
|
|
764
|
+
ctl: input, theme, terminalColumns,
|
|
765
|
+
});
|
|
766
|
+
return c ? c.value : null;
|
|
767
|
+
},
|
|
768
|
+
input: async (title) => input.promptInput(title),
|
|
769
|
+
notify: (m, t) => commit(`\n ${t === "error" ? "✗" : t === "warn" ? "!" : "·"} ${m}\n`),
|
|
770
|
+
setStatus: () => { },
|
|
771
|
+
setWidget: () => { },
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
// Vista LIVE: ad ogni cambio di stato dei task in background ridisegna la
|
|
775
|
+
// status-line (il segmento `bg` mostra running/totale aggiornato).
|
|
776
|
+
backgroundManager.onChange(() => input.refresh());
|
|
777
|
+
// Notifica quando un task STACCATO (Ctrl+B) termina, cosi' si puo' rivedere.
|
|
778
|
+
backgroundManager.onFinish((t) => {
|
|
779
|
+
if (!t.detached)
|
|
780
|
+
return; // i comandi in primo piano mostrano gia' il risultato inline
|
|
781
|
+
const mark = t.status === "done" ? theme.green("✓") : theme.red("✗");
|
|
782
|
+
input.write(`\n ${mark} background ${t.id} ` +
|
|
783
|
+
theme.dim(`[${t.status}${t.exitCode != null ? ` · uscita ${t.exitCode}` : ""}] ${truncate(t.command, 50)} · /bg ${t.id} per l'output\n`));
|
|
784
|
+
});
|
|
785
|
+
await input.start();
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
// Fallback (non-TTY o TALOS_CLASSIC_INPUT=1): loop readline classico inline.
|
|
789
|
+
stdout.write(startupBanner());
|
|
790
|
+
if (harness.session.head)
|
|
791
|
+
replaySession(harness.session, { write: text => stdout.write(text) }, theme, terminalColumns());
|
|
792
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
793
|
+
const ctl = {
|
|
794
|
+
review,
|
|
795
|
+
pause: () => rl.pause(),
|
|
796
|
+
resume: () => rl.resume(),
|
|
797
|
+
write: (t) => stdout.write(t),
|
|
798
|
+
clearConversation: () => { if (stdout.isTTY)
|
|
799
|
+
stdout.write("\x1b[0m\x1b[2J\x1b[3J\x1b[H"); },
|
|
800
|
+
setStatus: () => { },
|
|
801
|
+
setTokens: () => { },
|
|
802
|
+
setOverlay: () => { },
|
|
803
|
+
promptInput: (label) => new Promise((resolve) => {
|
|
804
|
+
rl.question(theme.silver(` ${label} `), (ans) => resolve(ans.length ? ans : null));
|
|
805
|
+
}),
|
|
806
|
+
logLength: () => -1, // fold non supportato nel fallback classico
|
|
807
|
+
fold: () => -1,
|
|
808
|
+
openFold: () => -1,
|
|
809
|
+
updateFoldSummary: () => { },
|
|
810
|
+
updateFold: () => { },
|
|
811
|
+
closeFold: () => { },
|
|
812
|
+
expandFold: () => false,
|
|
813
|
+
expandAllFolds: () => 0,
|
|
814
|
+
removeFold: () => false,
|
|
815
|
+
setKingdomVassals: () => { },
|
|
816
|
+
setKingdomBoard: () => { },
|
|
817
|
+
hasKingdomBoard: () => false,
|
|
818
|
+
};
|
|
819
|
+
const prompt = () => {
|
|
820
|
+
stdout.write(renderInputPrompt(harness.config, uiMeta("pronto"), theme, terminalColumns()));
|
|
821
|
+
rl.setPrompt(theme.silver(` ${harness.config.ui.prompt}`));
|
|
822
|
+
rl.prompt();
|
|
823
|
+
};
|
|
824
|
+
prompt();
|
|
825
|
+
const lineQueue = createSerialReadlineQueue({
|
|
826
|
+
processLine: (raw, signal) => processLine(raw, ctl, signal),
|
|
827
|
+
prompt,
|
|
828
|
+
requestClose: () => rl.close(),
|
|
829
|
+
onError: (error) => notifyStatus(ctl, theme.red(`\n errore: ${error.message}\n`)),
|
|
830
|
+
installAbort: (abort) => {
|
|
831
|
+
const onSigint = () => abort();
|
|
832
|
+
process.once("SIGINT", onSigint);
|
|
833
|
+
return () => process.removeListener("SIGINT", onSigint);
|
|
834
|
+
},
|
|
835
|
+
// Scrivere mentre l'agente lavora non mette piu' la riga in fila dietro
|
|
836
|
+
// alla risposta: entra nel giro in corso al primo confine sicuro.
|
|
837
|
+
deliverToActive: (raw) => {
|
|
838
|
+
try {
|
|
839
|
+
return deliverToActiveTurn(raw, [{ type: "text", text: pasteRegistry.expand(raw) }, ...imageRegistry.imagePartsFor(raw)]);
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
notifyStatus(ctl, theme.red(`\n ${error instanceof Error ? error.message : String(error)}\n`));
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
rl.on("line", (raw) => {
|
|
848
|
+
void lineQueue.enqueue(raw);
|
|
849
|
+
});
|
|
850
|
+
rl.on("close", () => {
|
|
851
|
+
lineQueue.close();
|
|
852
|
+
harness.dispose?.();
|
|
853
|
+
void harness.host.emit("session_shutdown", {});
|
|
854
|
+
stdout.write("\n");
|
|
855
|
+
process.exit(0);
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
// Esegue un giro dell'agente con render degli eventi e steering via Ctrl+C.
|
|
859
|
+
async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, getPermMode, vascend, rawGoal, imageParts) {
|
|
860
|
+
const env = deps.harness.env;
|
|
861
|
+
if (!deps.harness.providers.resolve(deps.harness.config.model)) {
|
|
862
|
+
ctl.write(theme.yellow(`\n modello non configurato: ${deps.harness.config.model}\n`) +
|
|
863
|
+
theme.dim(" scegli un modello disponibile per continuare.\n"));
|
|
864
|
+
const selected = await pickModel(deps.harness, ctl, theme, terminalColumns);
|
|
865
|
+
if (!selected)
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
// Gate permessi: selezione Sì/No dalla UI con le frecce (stile Claude Code),
|
|
869
|
+
// niente piu' digitazione di y/n. Le modalita' "accetta modifiche"/"auto"
|
|
870
|
+
// (Shift+Tab) saltano il prompt per le azioni che approvano.
|
|
871
|
+
const confirm = async (action, detail) => {
|
|
872
|
+
if (autoApproves(getPermMode(), action))
|
|
873
|
+
return true;
|
|
874
|
+
const choice = await selectOption({
|
|
875
|
+
title: `consenti: ${action}`,
|
|
876
|
+
hint: detail,
|
|
877
|
+
options: [
|
|
878
|
+
{ key: "yes", label: "Sì, esegui", detail: "consenti questa azione", value: true },
|
|
879
|
+
{ key: "no", label: "No, annulla", detail: "blocca l'azione", value: false },
|
|
880
|
+
],
|
|
881
|
+
currentKey: "yes",
|
|
882
|
+
ctl,
|
|
883
|
+
theme,
|
|
884
|
+
terminalColumns,
|
|
885
|
+
});
|
|
886
|
+
return choice?.value === true;
|
|
887
|
+
};
|
|
888
|
+
// Domanda all'utente in chat (tool ask_user): opzioni -> picker; libera -> input.
|
|
889
|
+
const ask = async (question, options) => {
|
|
890
|
+
if (ctl.queueQuestion)
|
|
891
|
+
return ctl.queueQuestion(question, options, signal);
|
|
892
|
+
if (options && options.length) {
|
|
893
|
+
const choice = await selectOption({
|
|
894
|
+
title: question,
|
|
895
|
+
question: true,
|
|
896
|
+
options: options.map((o) => ({ label: o, detail: "", value: o })),
|
|
897
|
+
ctl,
|
|
898
|
+
theme,
|
|
899
|
+
terminalColumns,
|
|
900
|
+
});
|
|
901
|
+
return choice?.value ?? null;
|
|
902
|
+
}
|
|
903
|
+
ctl.write(renderQuestion(question, theme, terminalColumns()));
|
|
904
|
+
return ctl.promptInput("rispondi qui (Invio conferma · Esc annulla)");
|
|
905
|
+
};
|
|
906
|
+
// Kingdoms Mode invocabile dall'agente (tool kingdoms_warm): esegue i castelli
|
|
907
|
+
// in parallelo (vassalli) e ritorna i report. Il consenso e' gia' stato dato
|
|
908
|
+
// al gate del tool (ctx.confirm); qui i vassalli eseguono i tool in blocco.
|
|
909
|
+
const spawnKingdom = async (castles, kopts) => {
|
|
910
|
+
if (kopts?.detach)
|
|
911
|
+
return runKingdomForHost({
|
|
912
|
+
harness: deps.harness, castles, signal, sink: wrappedSink, confirm, ask, env, detach: true,
|
|
913
|
+
...(kopts.baseBranch ? { baseBranch: kopts.baseBranch } : {}),
|
|
914
|
+
});
|
|
915
|
+
// Obiettivo complessivo del regno: esplicito > goal grezzo del turno.
|
|
916
|
+
const kingdomGoal = (kopts?.goal ?? rawGoal ?? "").trim();
|
|
917
|
+
const conc = warmConcurrency(env);
|
|
918
|
+
const total = castles.length;
|
|
919
|
+
const wt = worktreeMode(env);
|
|
920
|
+
// Bus pulito per il nuovo regno: sia in-memory sia il canale persistente
|
|
921
|
+
// file-based (cross-process, kingdoms granulari), se attivo.
|
|
922
|
+
kingdomBus.clear(`kw-${deps.harness.session.id}`);
|
|
923
|
+
resolveKingdomBus(env, deps.harness.config.sessionDir).clear(`kw-${deps.harness.session.id}`);
|
|
924
|
+
notifyStatus(ctl, `${total} gruppi · max ${conc} collaboratori`);
|
|
925
|
+
// Organizzazione del regno (decisa in progettazione): chi parte subito e chi
|
|
926
|
+
// dipende da chi. I dipendenti si coordinano/aspettano via i messaggi.
|
|
927
|
+
for (const c of castles) {
|
|
928
|
+
const dep = c.after && c.after.length ? theme.yellow(` ⟸ dopo ${c.after.join(", ")}`) : theme.dim(" · base");
|
|
929
|
+
ctl.refresh?.();
|
|
930
|
+
}
|
|
931
|
+
// La panoramica live (avvii, attività, messaggi, completamenti) va nella
|
|
932
|
+
// BOARD della regione viva, NON nella chat: si aggiorna in place senza
|
|
933
|
+
// riempire lo scroll. Il dettaglio per-vassallo resta nel viewer (Ctrl+G).
|
|
934
|
+
const live = createKingdomLive(ctl, theme, castles);
|
|
935
|
+
const reports = await runKingdomWarm({
|
|
936
|
+
harness: deps.harness,
|
|
937
|
+
kingdomId: `kw-${deps.harness.session.id}`,
|
|
938
|
+
castles,
|
|
939
|
+
signal,
|
|
940
|
+
confirm,
|
|
941
|
+
concurrency: conc,
|
|
942
|
+
ask,
|
|
943
|
+
onEvent: live.onEvent,
|
|
944
|
+
worktree: wt,
|
|
945
|
+
...(kopts?.baseBranch ? { baseBranch: kopts.baseBranch } : {}),
|
|
946
|
+
onNotice: (text) => notifyStatus(ctl, theme.accent(" ⎇ ") + theme.dim(text) + "\n"),
|
|
947
|
+
});
|
|
948
|
+
live.finish();
|
|
949
|
+
const isolated = reports.some((r) => r.branch);
|
|
950
|
+
const lines = reports
|
|
951
|
+
.map((r) => {
|
|
952
|
+
const mg = r.merge ? (r.merge.ok ? " · merge ✓" : ` · merge ✗ (${r.merge.conflict ? "conflitto" : "errore"}, ramo ${r.branch} conservato)`) : "";
|
|
953
|
+
return `[${r.slug}] ${r.ok ? "✓" : "✗"} (${r.rooms} attivita)${r.error ? ` errore: ${r.error}` : ""}${mg}: ${truncate(r.summary, 300)}`;
|
|
954
|
+
})
|
|
955
|
+
.join("\n");
|
|
956
|
+
// INDICE del contesto del regno per l'agente in chat: cosi' sa cosa hanno
|
|
957
|
+
// prodotto i vassalli e puo' recuperarne il dettaglio con recall.
|
|
958
|
+
let index = "";
|
|
959
|
+
try {
|
|
960
|
+
const mem = RoomMemory.open(deps.harness.config.sessionDir, `kw-${deps.harness.session.id}`);
|
|
961
|
+
const block = mem.indexBlock();
|
|
962
|
+
if (block.trim())
|
|
963
|
+
index = `\n\nATTIVITA DEL PIANO (recall per il dettaglio):\n${block}`;
|
|
964
|
+
}
|
|
965
|
+
catch { /* nessuna memoria */ }
|
|
966
|
+
const conflicts = collectConflicts(reports);
|
|
967
|
+
const stashList = formatConflictList(conflicts);
|
|
968
|
+
const gitNote = isolated
|
|
969
|
+
? `\n\n[NOTA_GIT]
|
|
970
|
+
@stato[rami]: ogni collaboratore in worktree isolato; rami merge-ati nel ramo base
|
|
971
|
+
@conflitti[merge]: ${conflicts.length ? `${conflicts.map((r) => r.slug).join(", ")} NON merge-ati (rami conservati, risoluzione manuale)` : "nessuno"}
|
|
972
|
+
@azione[consegna]: verifica diff e controlli nel ramo base; push solo se richiesto dall'utente`
|
|
973
|
+
: `\n\n[NOTA_GIT]
|
|
974
|
+
@stato[modifiche]: nel working tree, NON committate (i collaboratori non fanno git: working dir condivisa)
|
|
975
|
+
@azione[consegna]: verifica diff e controlli; commit e push solo se richiesti dall'utente`;
|
|
976
|
+
const stashNote = stashList ? `\n\n${stashList}` : "";
|
|
977
|
+
// GATE KPI DI REGNO (opt-in: solo se self-improve e' abilitato). Misura
|
|
978
|
+
// l'esecuzione (KPI di processo) e l'allineamento alla Vision/obiettivo
|
|
979
|
+
// finale, e produce un verdetto. Fail-open: in errore non blocca. Il
|
|
980
|
+
// verdetto viene mostrato all'utente senza innescare altri turni automatici.
|
|
981
|
+
if (selfImproveEnabled(env) && reports.length && !signal.aborted) {
|
|
982
|
+
ctl.setStatus(tr("verifica finale del piano", "final plan review"));
|
|
983
|
+
try {
|
|
984
|
+
const idxBlock = index ? index.replace(/^[\s\S]*?:\n/, "") : "";
|
|
985
|
+
const verdict = await runKingdomGate({
|
|
986
|
+
harness: deps.harness,
|
|
987
|
+
reports,
|
|
988
|
+
goal: kingdomGoal,
|
|
989
|
+
...(idxBlock ? { roomIndex: idxBlock } : {}),
|
|
990
|
+
signal,
|
|
991
|
+
});
|
|
992
|
+
ctl.setStatus(null);
|
|
993
|
+
ctl.write("\n " + formatKingdomGate(verdict).split("\n").join("\n ") + "\n");
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
ctl.setStatus(null); // fail-open: nessun gate
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return `PIANO COMPLETATO · ${reports.length} gruppi\n${lines}${index}${gitNote}${stashNote}`;
|
|
1000
|
+
};
|
|
1001
|
+
const sdir = deps.harness.config.sessionDir;
|
|
1002
|
+
const sid = deps.harness.session.id;
|
|
1003
|
+
// Quando vascend e' attivo, il sink attribuisce i token alle stanze (delta
|
|
1004
|
+
// cumulativo) e mostra il riepilogo per castello a castello completato.
|
|
1005
|
+
const baseSink = makeSink(theme, terminalColumns, deps.harness.config.cwd, env, ctl, vascend ? { sdir, sid } : undefined);
|
|
1006
|
+
ctl.review?.begin(rawGoal ?? input);
|
|
1007
|
+
ctl.setStatus(tr("avvio", "starting"));
|
|
1008
|
+
let turnFailed = false;
|
|
1009
|
+
const wrappedSink = (event) => {
|
|
1010
|
+
if (event.type === "error" || (event.type === "turn_end" && event.stopReason === "error"))
|
|
1011
|
+
turnFailed = true;
|
|
1012
|
+
ctl.review?.record(event);
|
|
1013
|
+
if (event.type === "usage" && stats.observe)
|
|
1014
|
+
stats.observe(event);
|
|
1015
|
+
if (event.type === "context_window")
|
|
1016
|
+
stats.context?.observe(deps.harness, event.ledger);
|
|
1017
|
+
if (event.type === "turn_end" && event.usage && !stats.observe) {
|
|
1018
|
+
stats.last = event.usage;
|
|
1019
|
+
stats.cost?.record(deps.harness.config.model, event.usage);
|
|
1020
|
+
}
|
|
1021
|
+
baseSink(event);
|
|
1022
|
+
};
|
|
1023
|
+
// Casella dello STEERING: quello che l'utente scrive mentre questo giro
|
|
1024
|
+
// lavora entra qui e viene preso in carico al primo confine sicuro. Resta
|
|
1025
|
+
// registrata come casella attiva per tutta la durata del giro (la chiude
|
|
1026
|
+
// processLine, che sa quando il giro e' finito).
|
|
1027
|
+
const inbox = ctl.userInbox ?? beginActiveTurnInbox();
|
|
1028
|
+
const mayContinue = async (trigger) => {
|
|
1029
|
+
const resolved = deps.harness.providers.resolve(deps.harness.config.model);
|
|
1030
|
+
if (!resolved)
|
|
1031
|
+
return false;
|
|
1032
|
+
const tasks = vascend?.state().kingdom.castles.flatMap(c => c.tasks.map(t => ({ ref: t.id, title: t.title, status: t.status }))) ?? [];
|
|
1033
|
+
const decision = await reviewContinuation({ provider: resolved.provider, model: resolved.modelId,
|
|
1034
|
+
sessionId: sid, userRequest: rawGoal ?? input, history: deps.harness.session.history(), openTasks: tasks,
|
|
1035
|
+
trigger, signal, inbox });
|
|
1036
|
+
if (decision?.decision === "continue")
|
|
1037
|
+
return true;
|
|
1038
|
+
if (decision)
|
|
1039
|
+
notifyStatus(ctl, decision.reason, decision.decision === "unavailable");
|
|
1040
|
+
return false;
|
|
1041
|
+
};
|
|
1042
|
+
const completionEvidence = () => {
|
|
1043
|
+
const rows = deps.harness.session.history().slice(-24).flatMap((message) => message.content.flatMap((part) => {
|
|
1044
|
+
if (part.type === "tool_call")
|
|
1045
|
+
return [`TOOL ${part.name} input=${JSON.stringify(part.input).slice(0, 1_500)}`];
|
|
1046
|
+
if (part.type === "tool_result")
|
|
1047
|
+
return [`TOOL_RESULT ${part.callId} ${part.isError ? "ERROR" : "OK"}: ${part.output.slice(0, 2_500)}`];
|
|
1048
|
+
if (part.type === "text" && message.role === "assistant")
|
|
1049
|
+
return [`ASSISTANT: ${part.text.slice(0, 2_000)}`];
|
|
1050
|
+
return [];
|
|
1051
|
+
}));
|
|
1052
|
+
return rows.join("\n").slice(-16_000);
|
|
1053
|
+
};
|
|
1054
|
+
try {
|
|
1055
|
+
// Se ci sono immagini allegate (ref "img #N" risolti), il turno parte con
|
|
1056
|
+
// parti miste testo+immagini, cosi' i bytes raggiungono il modello.
|
|
1057
|
+
const turnInput = imageParts && imageParts.length
|
|
1058
|
+
? [{ type: "text", text: input }, ...imageParts]
|
|
1059
|
+
: input;
|
|
1060
|
+
await runTurn({ ...deps.harness.deps(confirm), ask, spawnKingdom, inbox }, turnInput, wrappedSink, signal);
|
|
1061
|
+
if (turnFailed || signal.aborted)
|
|
1062
|
+
return;
|
|
1063
|
+
// REVISORE di team: per i task complessi (molte stanze) con stanze fatte e
|
|
1064
|
+
// altre da fare, un agente separato analizza le metriche (token/durata) e
|
|
1065
|
+
// riorganizza/aggiunge le stanze rimaste prima di proseguire.
|
|
1066
|
+
if (vascend && !signal.aborted) {
|
|
1067
|
+
const ro = reviewOptions(env);
|
|
1068
|
+
vascend.loadPlan(sdir, sid);
|
|
1069
|
+
if (ro.enabled && shouldReview(vascend.state(), ro.minRooms)) {
|
|
1070
|
+
if (!await mayContinue("Review and reorganize unfinished tasks after the assistant finished."))
|
|
1071
|
+
return;
|
|
1072
|
+
await runTeamReviewer(deps, vascend, sdir, sid, ctl, theme, terminalColumns, signal);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
// VALIDAZIONE Danilov: se l'agente ha creato un castello ma ha lasciato
|
|
1076
|
+
// stanze AL BUIO, svegliarlo per completarle (deterministico: il regno valida
|
|
1077
|
+
// solo se tutte illuminate). Cap di wake-up per non ciclare/sprecare token.
|
|
1078
|
+
if (vascend) {
|
|
1079
|
+
const MAX_WAKE = 2;
|
|
1080
|
+
for (let wake = 0; wake < MAX_WAKE && !signal.aborted; wake += 1) {
|
|
1081
|
+
vascend.loadPlan(sdir, sid);
|
|
1082
|
+
const v = validateKingdom(vascend.state());
|
|
1083
|
+
if (v.ok || v.pending.length === 0)
|
|
1084
|
+
break; // tutte risolte (done o failed)
|
|
1085
|
+
if (!await mayContinue("The TUI found unfinished plan tasks after the assistant finished."))
|
|
1086
|
+
return;
|
|
1087
|
+
notifyStatus(ctl, theme.yellow(`\n ⚠ validazione: ${v.done}/${v.total} attivita · da completare: ${v.pending.join(", ")} → riprendo il lavoro\n`));
|
|
1088
|
+
ctl.setStatus(tr("verifica", "checking"));
|
|
1089
|
+
await runTurn({ ...deps.harness.deps(confirm), ask, spawnKingdom, inbox }, buildValidationSteer(v), wrappedSink, signal);
|
|
1090
|
+
if (turnFailed || signal.aborted)
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
// COMPLETEZZA: piano "validato" != goal consegnato. Per un obiettivo grande,
|
|
1095
|
+
// un critico valuta se il GOAL REALE e' completo; se mancano pezzi essenziali
|
|
1096
|
+
// l'agente continua (altri castelli/stanze). Cosi' non si ferma al primo piano.
|
|
1097
|
+
if (vascend && rawGoal && completenessEnabled(env) && looksBigGoal(rawGoal)) {
|
|
1098
|
+
const rounds = completenessRounds(env);
|
|
1099
|
+
for (let r = 0; r < rounds && !signal.aborted; r += 1) {
|
|
1100
|
+
vascend.loadPlan(sdir, sid);
|
|
1101
|
+
const castles = vascend.state().kingdom.castles;
|
|
1102
|
+
const planDigest = castles.map((c) => `${c.title}: ${c.tasks.map((t) => `${t.id} [${t.status}] ${t.title}${t.evidence?.length ? ` evidence=${t.evidence.join(" | ")}` : ""}`).join("; ")}`).join("\n");
|
|
1103
|
+
const evidence = `${completionEvidence()}\n\nSTATO PIANO:\n${planDigest}`;
|
|
1104
|
+
ctl.setStatus(tr("verifico la completezza del goal", "checking goal completion"));
|
|
1105
|
+
const verdict = await judgeCompleteness({ harness: deps.harness, goal: rawGoal, evidence, signal });
|
|
1106
|
+
ctl.setStatus(null);
|
|
1107
|
+
if (verdict.uncertain) {
|
|
1108
|
+
notifyStatus(ctl, theme.red(`\n ⚠ completezza non verificabile: ${verdict.reason || "evidenze insufficienti"}\n`));
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
if (verdict.complete)
|
|
1112
|
+
break;
|
|
1113
|
+
if (!await mayContinue(`A completeness check suggests: ${verdict.missing.join("; ")}`))
|
|
1114
|
+
return;
|
|
1115
|
+
notifyStatus(ctl, theme.yellow(`\n ⚠ completezza: il goal non e' completo, mancano ${verdict.missing.length} pezzi → continuo\n`));
|
|
1116
|
+
for (const m of verdict.missing)
|
|
1117
|
+
notifyStatus(ctl, theme.dim(` · ${m}\n`));
|
|
1118
|
+
await runTurn({ ...deps.harness.deps(confirm), ask, spawnKingdom, inbox }, buildCompletenessSteer(rawGoal, verdict.missing), wrappedSink, signal);
|
|
1119
|
+
if (turnFailed || signal.aborted)
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
catch (err) {
|
|
1125
|
+
ctl.review?.record({ type: "error", message: err.message });
|
|
1126
|
+
notifyStatus(ctl, theme.red(`\n errore: ${err.message}\n`));
|
|
1127
|
+
}
|
|
1128
|
+
finally {
|
|
1129
|
+
ctl.review?.finish(signal.aborted);
|
|
1130
|
+
ctl.setStatus(null);
|
|
1131
|
+
if (vascend) {
|
|
1132
|
+
// Ricarica il piano: mostra le stanze reali (e i loro stati) se l'agente
|
|
1133
|
+
// ha pianificato durante il turno; altrimenti nessun riquadro.
|
|
1134
|
+
vascend.loadPlan(sdir, sid);
|
|
1135
|
+
// Niente riquadro inline se la board live del regno e' gia' pinned: si
|
|
1136
|
+
// sovrapporrebbero (stessa vista del regno due volte).
|
|
1137
|
+
const box = ctl.hasKingdomBoard() ? "" : renderVascendStatus(vascend.state(), theme, terminalColumns());
|
|
1138
|
+
if (box.trim())
|
|
1139
|
+
ctl.refresh?.();
|
|
1140
|
+
// Verdetto deterministico del regno (validate == state==MASK_TARGET).
|
|
1141
|
+
const v = validateKingdom(vascend.state());
|
|
1142
|
+
if (v.total > 0) {
|
|
1143
|
+
if (v.ok) {
|
|
1144
|
+
notifyStatus(ctl, theme.green(`\n ✓ Lista verificata: ${v.done}/${v.total} attività completate\n`));
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
const pending = v.pending.map((p) => `${p} da completare`);
|
|
1148
|
+
const ko = v.failed.map((f) => `${f} fallita`);
|
|
1149
|
+
ctl.refresh?.();
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
// Revisore di team: agente isolato (sessione propria) instradato sullo STESSO
|
|
1156
|
+
// piano via roomStoreId, con tool ridotti (sola lettura + editing-piano sicuro,
|
|
1157
|
+
// niente bash/scrittura/mark). Analizza le metriche e migliora le stanze rimaste.
|
|
1158
|
+
async function runTeamReviewer(deps, vascend, sdir, sid, ctl, theme, terminalColumns, signal) {
|
|
1159
|
+
const digest = buildRoomMetricsDigest(vascend.state());
|
|
1160
|
+
notifyStatus(ctl, theme.accent("\n ◳ revisore di team") + theme.dim(" · analizzo token/durata delle attivita fatte e riorganizzo le rimaste\n"));
|
|
1161
|
+
const base = deps.harness.deps(async () => true); // revisore: auto-consenso (tool senza effetti pericolosi)
|
|
1162
|
+
const allow = new Set(["recall", "read_file", "list_dir", "glob", "grep", "web_fetch", "vascend_note", "vascend_skill", "vascend_addtasks"]);
|
|
1163
|
+
const tools = new ToolRegistry();
|
|
1164
|
+
for (const name of base.tools.names()) {
|
|
1165
|
+
if (allow.has(name)) {
|
|
1166
|
+
const t = base.tools.get(name);
|
|
1167
|
+
if (t)
|
|
1168
|
+
tools.register(t);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
const session = Session.create(base.config.sessionDir, `rev-${sid}-${Date.now()}`);
|
|
1172
|
+
// roomStoreId=sid: il revisore EDITA lo stesso piano e puo' fare recall sulle
|
|
1173
|
+
// stanze; perRoom=false: la sua sessione NON archivia nella memoria condivisa.
|
|
1174
|
+
const config = { ...base.config, roomStoreId: sid, compaction: { ...base.config.compaction, perRoom: false } };
|
|
1175
|
+
const rdeps = { ...base, tools, session, config };
|
|
1176
|
+
let buf = "";
|
|
1177
|
+
const revCalls = new Map();
|
|
1178
|
+
const sink = (event) => {
|
|
1179
|
+
ctl.review?.recordActor("revisore", event);
|
|
1180
|
+
if (event.type === "text") {
|
|
1181
|
+
buf += event.delta;
|
|
1182
|
+
ctl.setStatus(tr("revisione", "reviewing"));
|
|
1183
|
+
}
|
|
1184
|
+
else if (event.type === "tool_call") {
|
|
1185
|
+
if (buf.trim()) {
|
|
1186
|
+
notifyStatus(ctl, theme.dim(" " + buf.trim().split("\n").join("\n ")) + "\n");
|
|
1187
|
+
buf = "";
|
|
1188
|
+
}
|
|
1189
|
+
revCalls.set(event.call.id, event.call);
|
|
1190
|
+
notifyStatus(ctl, theme.dim(` ◦ [revisore] ${toolLabel(event.call.name)}(${toolArg(event.call)})`));
|
|
1191
|
+
}
|
|
1192
|
+
else if (event.type === "tool_result") {
|
|
1193
|
+
const rcall = revCalls.get(event.result.callId);
|
|
1194
|
+
const rsum = event.result.isError ? firstLine(event.result.output) : (resultSummary(rcall, event.result.output) ?? firstLine(event.result.output));
|
|
1195
|
+
notifyStatus(ctl, theme.dim(` ⎿ ${rsum}`));
|
|
1196
|
+
}
|
|
1197
|
+
else if (event.type === "done") {
|
|
1198
|
+
if (buf.trim()) {
|
|
1199
|
+
notifyStatus(ctl, theme.dim(" " + buf.trim().split("\n").join("\n ")) + "\n");
|
|
1200
|
+
buf = "";
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
try {
|
|
1205
|
+
await runTurn(rdeps, buildReviewerPrompt(digest), sink, signal);
|
|
1206
|
+
}
|
|
1207
|
+
catch (err) {
|
|
1208
|
+
notifyStatus(ctl, theme.dim(` revisore interrotto: ${err.message}\n`));
|
|
1209
|
+
}
|
|
1210
|
+
vascend.loadPlan(sdir, sid);
|
|
1211
|
+
}
|
|
1212
|
+
export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
|
|
1213
|
+
let textBuffer = "";
|
|
1214
|
+
let assistantSegments = 0;
|
|
1215
|
+
let assistantStarted = false;
|
|
1216
|
+
let assistantClosed = false;
|
|
1217
|
+
const writeNotice = (text, failed = false) => {
|
|
1218
|
+
notifyStatus(ctl, text, failed);
|
|
1219
|
+
};
|
|
1220
|
+
// Correlazione call->risultato e contenuto PRE-scrittura, per gli avvisi del
|
|
1221
|
+
// regno e per il diff dei tool di scrittura.
|
|
1222
|
+
const calls = new Map();
|
|
1223
|
+
const preContent = new Map();
|
|
1224
|
+
const commandCards = new Map();
|
|
1225
|
+
const wrappers = new Map();
|
|
1226
|
+
const mcpCards = new Map();
|
|
1227
|
+
const toolCards = new Map();
|
|
1228
|
+
const skillCards = new Map();
|
|
1229
|
+
let newInputTok = 0; // input non in cache del run (cumulativo)
|
|
1230
|
+
let cacheTok = 0; // cache letta/scritta nel run (cumulativo)
|
|
1231
|
+
let outTok = 0; // token generati nel run (cumulativi)
|
|
1232
|
+
// Traccia se l'ultimo turno e' stato troncato (stop=max_tokens): se il run
|
|
1233
|
+
// chiude (done) ancora in questo stato, l'auto-continuazione si e' esaurita
|
|
1234
|
+
// e avvisiamo di spezzare il compito.
|
|
1235
|
+
let lastTruncated = false;
|
|
1236
|
+
// Attribuzione token per stanza: snapshot di outTok all'avvio (running) ->
|
|
1237
|
+
// costo a stanza completata (delta). Castelli gia' riepilogati (per non
|
|
1238
|
+
// ripetere il riepilogo a ogni mark successivo).
|
|
1239
|
+
const roomStart = new Map();
|
|
1240
|
+
const summarized = new Set();
|
|
1241
|
+
const flushText = (phase = "progress") => {
|
|
1242
|
+
// Rimuovi eventuale markup di tool-call emesso come testo (viene recuperato
|
|
1243
|
+
// ed eseguito dal loop agente): non deve comparire in chat.
|
|
1244
|
+
const text = stripToolMarkup(textBuffer).trimEnd();
|
|
1245
|
+
if (text) {
|
|
1246
|
+
const sequence = assistantSegments + 1;
|
|
1247
|
+
const kind = !assistantStarted
|
|
1248
|
+
? (phase === "final" ? "only" : "start")
|
|
1249
|
+
: phase === "final" ? "final" : "progress";
|
|
1250
|
+
ctl.write(renderAssistantSegment(text, theme, terminalColumns(), { cwd, env }, kind, sequence));
|
|
1251
|
+
assistantSegments = sequence;
|
|
1252
|
+
assistantStarted = true;
|
|
1253
|
+
}
|
|
1254
|
+
textBuffer = "";
|
|
1255
|
+
};
|
|
1256
|
+
const closeAssistant = (status = "completed") => {
|
|
1257
|
+
if (assistantClosed || !assistantStarted)
|
|
1258
|
+
return;
|
|
1259
|
+
const ending = renderAssistantTurnEnd(theme, assistantSegments, status);
|
|
1260
|
+
if (ending)
|
|
1261
|
+
ctl.write(ending);
|
|
1262
|
+
assistantClosed = true;
|
|
1263
|
+
};
|
|
1264
|
+
const ensureAssistantStarted = () => {
|
|
1265
|
+
if (assistantStarted)
|
|
1266
|
+
return;
|
|
1267
|
+
ctl.write(renderAssistantTurnStart(theme, terminalColumns()));
|
|
1268
|
+
assistantStarted = true;
|
|
1269
|
+
};
|
|
1270
|
+
// Animazione del bullet ● accanto al tool IN CORSO nello scrollback: si tiene
|
|
1271
|
+
// l'indice della sua riga di log, il nome del tool e la coda statica della riga
|
|
1272
|
+
// (tutto cio' che segue il bullet, gia' renderizzato). Un timer riscrive solo
|
|
1273
|
+
// quella riga col bullet pulsante (pulseDot) finche' non arriva il risultato.
|
|
1274
|
+
let liveDot = null;
|
|
1275
|
+
let liveFrame = 0;
|
|
1276
|
+
let dotTimer;
|
|
1277
|
+
const stopDot = (settle = true) => {
|
|
1278
|
+
if (dotTimer) {
|
|
1279
|
+
clearInterval(dotTimer);
|
|
1280
|
+
dotTimer = undefined;
|
|
1281
|
+
}
|
|
1282
|
+
// Riposa il bullet allo stato statico (accent) come il resto dello scrollback.
|
|
1283
|
+
if (settle && liveDot && ctl.setLogLine) {
|
|
1284
|
+
ctl.setLogLine(liveDot.index, fitLine(theme.dark(" │ ") + theme.accent("● ") + liveDot.tail, terminalColumns() - 1));
|
|
1285
|
+
}
|
|
1286
|
+
liveDot = null;
|
|
1287
|
+
};
|
|
1288
|
+
const startDot = (index, name, tail) => {
|
|
1289
|
+
stopDot(false); // chiudi l'eventuale precedente senza ridisegnare (verra' sovrascritto)
|
|
1290
|
+
if (!ctl.setLogLine)
|
|
1291
|
+
return; // fallback readline: niente animazione scrollback
|
|
1292
|
+
liveDot = { index, name, tail };
|
|
1293
|
+
liveFrame = 0;
|
|
1294
|
+
dotTimer = setInterval(() => {
|
|
1295
|
+
if (!liveDot || !ctl.setLogLine)
|
|
1296
|
+
return;
|
|
1297
|
+
liveFrame += 1;
|
|
1298
|
+
ctl.setLogLine(liveDot.index, fitLine(theme.dark(" │ ") + pulseDot(liveDot.name, liveFrame) + " " + liveDot.tail, terminalColumns() - 1));
|
|
1299
|
+
}, 140);
|
|
1300
|
+
};
|
|
1301
|
+
const beginCommand = () => {
|
|
1302
|
+
flushText();
|
|
1303
|
+
ensureAssistantStarted();
|
|
1304
|
+
stopDot();
|
|
1305
|
+
ctl.write("\n");
|
|
1306
|
+
};
|
|
1307
|
+
return (event) => {
|
|
1308
|
+
switch (event.type) {
|
|
1309
|
+
case "context_window":
|
|
1310
|
+
ctl.refresh?.();
|
|
1311
|
+
break;
|
|
1312
|
+
case "turn_start":
|
|
1313
|
+
if (assistantClosed) {
|
|
1314
|
+
assistantSegments = 0;
|
|
1315
|
+
assistantStarted = false;
|
|
1316
|
+
assistantClosed = false;
|
|
1317
|
+
}
|
|
1318
|
+
ctl.setStatus(tr("elaboro", "working"));
|
|
1319
|
+
ctl.setStatusTool?.(null);
|
|
1320
|
+
stopDot();
|
|
1321
|
+
break;
|
|
1322
|
+
case "turn_end":
|
|
1323
|
+
// Token provider cumulativi del run, separati tra input nuovo, cache e
|
|
1324
|
+
// output. Sono valori API, non la stima locale del contesto.
|
|
1325
|
+
if (event.usage) {
|
|
1326
|
+
newInputTok += event.usage.inputTokens;
|
|
1327
|
+
cacheTok += (event.usage.cacheReadTokens ?? 0) + (event.usage.cacheWriteTokens ?? 0);
|
|
1328
|
+
outTok += event.usage.outputTokens;
|
|
1329
|
+
ctl.setTokens(newInputTok, cacheTok, outTok);
|
|
1330
|
+
}
|
|
1331
|
+
// Output troncato: il modello ha raggiunto il tetto di token. L'agente
|
|
1332
|
+
// auto-continua (vedi runTurn); qui solo un avviso discreto che la
|
|
1333
|
+
// risposta proseguira' nel turno seguente. Se l'auto-continuazione e'
|
|
1334
|
+
// disabilitata o esaurita, il messaggio "output ancora troncato" arriva
|
|
1335
|
+
// come evento status separato.
|
|
1336
|
+
lastTruncated = event.stopReason === "max_tokens";
|
|
1337
|
+
if (event.stopReason === "max_tokens") {
|
|
1338
|
+
notifyStatus(ctl, theme.dim("\n ⟳ output al limite del modello: continuo automaticamente…\n"));
|
|
1339
|
+
}
|
|
1340
|
+
break;
|
|
1341
|
+
case "status":
|
|
1342
|
+
// Il contesto ha gia' barra e conteggi; non sostituisce l'attivita' viva.
|
|
1343
|
+
if (event.category === "context")
|
|
1344
|
+
break;
|
|
1345
|
+
// Attivita' del modello senza testo (es. streaming di un input grande).
|
|
1346
|
+
ctl.setStatus(event.label);
|
|
1347
|
+
break;
|
|
1348
|
+
case "text":
|
|
1349
|
+
ctl.setStatus(tr("risposta in corso", "responding"));
|
|
1350
|
+
ctl.setStatusTool?.(null);
|
|
1351
|
+
stopDot();
|
|
1352
|
+
textBuffer += event.delta;
|
|
1353
|
+
break;
|
|
1354
|
+
case "tool_call": {
|
|
1355
|
+
if (event.parentId) {
|
|
1356
|
+
const parent = wrappers.get(event.parentId);
|
|
1357
|
+
if (parent)
|
|
1358
|
+
parent.child = true;
|
|
1359
|
+
}
|
|
1360
|
+
calls.set(event.call.id, event.call);
|
|
1361
|
+
// Compact protocol wrappers emit real child calls. Show those commands
|
|
1362
|
+
// once, without a duplicate execute(args) envelope in the transcript.
|
|
1363
|
+
if (event.call.name === "execute" || event.call.name === "tool_invoke" || event.call.name === "run_program") {
|
|
1364
|
+
if (textBuffer.trim()) {
|
|
1365
|
+
flushText();
|
|
1366
|
+
}
|
|
1367
|
+
wrappers.set(event.call.id, { child: false });
|
|
1368
|
+
ctl.setStatus(tr("avvio operazione", "starting operation"));
|
|
1369
|
+
break;
|
|
1370
|
+
}
|
|
1371
|
+
const skillName = loadedSkill(event.call);
|
|
1372
|
+
if (skillName !== undefined) {
|
|
1373
|
+
beginCommand();
|
|
1374
|
+
const rows = skillPreview(skillName, theme, terminalColumns());
|
|
1375
|
+
const index = ctl.logLength();
|
|
1376
|
+
ctl.write(rows.join("\n") + "\n");
|
|
1377
|
+
skillCards.set(event.call.id, { index, header: rows[0], name: skillName });
|
|
1378
|
+
ctl.setStatus(tr("Lettura skill {name}", "Reading skill {name}", { name: skillName }));
|
|
1379
|
+
ctl.setStatusTool?.(event.call.name);
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
if (event.call.name === "ask_user" && ctl.queueQuestion) {
|
|
1383
|
+
flushText();
|
|
1384
|
+
ctl.setStatus(tr("Risposta in attesa", "Awaiting answer"));
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
if (mcpName(event.call.name)) {
|
|
1388
|
+
beginCommand();
|
|
1389
|
+
const rows = mcpPreview(event.call, theme, terminalColumns());
|
|
1390
|
+
const index = ctl.logLength();
|
|
1391
|
+
ctl.write(rows.join("\n") + "\n");
|
|
1392
|
+
mcpCards.set(event.call.id, { index, end: ctl.logLength(), header: rows[0] });
|
|
1393
|
+
ctl.setStatus(tr("Esegue {name}", "Calling {name}", { name: mcpName(event.call.name) ?? event.call.name }));
|
|
1394
|
+
ctl.setStatusTool?.(event.call.name);
|
|
1395
|
+
break;
|
|
1396
|
+
}
|
|
1397
|
+
// Shell commands have a short source/output preview; programs keep a
|
|
1398
|
+
// compact header with their source available in review.
|
|
1399
|
+
const source = event.call.name === "bash" ? commandSource(event.call) : undefined;
|
|
1400
|
+
if (source !== undefined) {
|
|
1401
|
+
beginCommand();
|
|
1402
|
+
const columns = terminalColumns();
|
|
1403
|
+
const preview = commandPreview(source, theme, columns);
|
|
1404
|
+
ctl.write(preview.lines.join("\n") + "\n");
|
|
1405
|
+
commandCards.set(event.call.id, { index: ctl.logLength() - preview.lines.length,
|
|
1406
|
+
firstRow: preview.firstRow, columns, end: ctl.logLength(), header: preview.lines[0] });
|
|
1407
|
+
ctl.setStatus(tr("esegue comando terminale", "running terminal command"));
|
|
1408
|
+
ctl.setStatusTool?.(event.call.name);
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
// vascend_mark: NON scrive nulla in chat (ne' bullet ne' connettore) —
|
|
1412
|
+
// lo stato della stanza vive solo nella status-line, animato. Registra
|
|
1413
|
+
// la call per il tool_result e mostra subito l'esito sulla status-line.
|
|
1414
|
+
if (event.call.name === "vascend_mark") {
|
|
1415
|
+
if (textBuffer.trim()) {
|
|
1416
|
+
flushText();
|
|
1417
|
+
}
|
|
1418
|
+
calls.set(event.call.id, event.call);
|
|
1419
|
+
const task = String(event.call.input["task"] ?? "");
|
|
1420
|
+
const slug = String(event.call.input["group"] ?? event.call.input["castle"] ?? "master");
|
|
1421
|
+
const st = String(event.call.input["status"] ?? "completed");
|
|
1422
|
+
const where = slug === "master" ? "" : `${slug}/`;
|
|
1423
|
+
if (st === "running")
|
|
1424
|
+
ctl.setRoomStatus?.(`attività ${where}${task} avviata`, "running");
|
|
1425
|
+
else if (st === "failed")
|
|
1426
|
+
ctl.setRoomStatus?.(`attività ${where}${task} non riuscita`, "failed");
|
|
1427
|
+
else
|
|
1428
|
+
ctl.setRoomStatus?.(`attività ${where}${task} completata`, "done");
|
|
1429
|
+
break;
|
|
1430
|
+
}
|
|
1431
|
+
// Each operation owns a small separate block. Full data stays in review.
|
|
1432
|
+
flushText();
|
|
1433
|
+
ensureAssistantStarted();
|
|
1434
|
+
ctl.write("\n");
|
|
1435
|
+
const gitSum = event.call.name === "bash" ? gitSummary(String(event.call.input["command"] ?? "")) : null;
|
|
1436
|
+
ctl.setStatus(toolLabel(event.call.name));
|
|
1437
|
+
ctl.setStatusTool?.(event.call.name);
|
|
1438
|
+
calls.set(event.call.id, event.call);
|
|
1439
|
+
// Cattura il contenuto PRIMA della scrittura, per il diff (il file non
|
|
1440
|
+
// e' ancora stato toccato a questo punto del loop).
|
|
1441
|
+
if (event.call.name === "write_file") {
|
|
1442
|
+
const p = event.call.input["path"];
|
|
1443
|
+
if (typeof p === "string" && p) {
|
|
1444
|
+
const abs = isAbsolute(p) ? p : resolve(cwd, p);
|
|
1445
|
+
preContent.set(event.call.id, existsSync(abs) ? safeRead(abs) : "");
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
// Stile Claude Code: ● Etichetta(argomento). Se l'argomento E' un path,
|
|
1449
|
+
// l'argomento STESSO diventa il link cliccabile (sottolineato dal
|
|
1450
|
+
// terminale) — niente piu' path duplicato dopo le parentesi.
|
|
1451
|
+
const pathArg = event.call.input["path"];
|
|
1452
|
+
const label = gitSum ? "Git" : toolLabel(event.call.name);
|
|
1453
|
+
const argText = livePreview(gitSum ?? toolArg(event.call), terminalColumns() - terminalWidth(label) - 9);
|
|
1454
|
+
const argRendered = typeof pathArg === "string" && pathArg && argText === pathArg
|
|
1455
|
+
? fileLink(isAbsolute(pathArg) ? pathArg : resolve(cwd, pathArg), theme.dim(pathArg))
|
|
1456
|
+
: theme.dim(argText);
|
|
1457
|
+
// Nome del tool COLORATO per categoria (read/write/shell/search/...): a
|
|
1458
|
+
// colpo d'occhio si distingue il tipo di azione. Git eredita la categoria
|
|
1459
|
+
// shell (e' un comando bash).
|
|
1460
|
+
const nameRendered = gitSum
|
|
1461
|
+
? paintCategoryName("shell", "Git")
|
|
1462
|
+
: paintToolName(event.call.name, toolLabel(event.call.name));
|
|
1463
|
+
// Coda statica della riga (tutto cio' che segue il bullet): la riusiamo
|
|
1464
|
+
// sia per la scrittura iniziale sia per l'animazione del bullet.
|
|
1465
|
+
const tail = nameRendered + theme.dim("(") + argRendered + theme.dim(")");
|
|
1466
|
+
const header = fitLine(theme.dark(" │ ") + theme.accent("● ") + tail, terminalColumns() - 1);
|
|
1467
|
+
ctl.write(header + "\n");
|
|
1468
|
+
toolCards.set(event.call.id, { end: ctl.logLength(), header });
|
|
1469
|
+
if (event.call.name === "ask_user")
|
|
1470
|
+
startDot(ctl.logLength() - 1, event.call.name, tail);
|
|
1471
|
+
break;
|
|
1472
|
+
}
|
|
1473
|
+
case "tool_result": {
|
|
1474
|
+
ctl.setStatus(tr("elaboro", "working"));
|
|
1475
|
+
stopDot(); // ferma l'animazione del bullet: il tool e' finito
|
|
1476
|
+
const call = calls.get(event.result.callId);
|
|
1477
|
+
const toolCard = toolCards.get(event.result.callId);
|
|
1478
|
+
toolCards.delete(event.result.callId);
|
|
1479
|
+
if (event.parentId) {
|
|
1480
|
+
const parent = wrappers.get(event.parentId);
|
|
1481
|
+
if (parent) {
|
|
1482
|
+
parent.output = event.result.output;
|
|
1483
|
+
parent.isError = event.result.isError;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
const skillCard = skillCards.get(event.result.callId);
|
|
1487
|
+
if (skillCard) {
|
|
1488
|
+
skillCards.delete(event.result.callId);
|
|
1489
|
+
const failed = Boolean(event.result.isError || /^(?:errore:|skill sconosciuta:)/i.test(event.result.output));
|
|
1490
|
+
const state = failed ? "error" : "ok";
|
|
1491
|
+
const retained = !ctl.getLogLine || ctl.getLogLine(skillCard.index) === skillCard.header;
|
|
1492
|
+
if (retained && ctl.setLogLine)
|
|
1493
|
+
ctl.setLogLine(skillCard.index, skillHeader(state, theme, terminalColumns()));
|
|
1494
|
+
else
|
|
1495
|
+
ctl.write("\n" + skillPreview(skillCard.name, theme, terminalColumns(), state).join("\n") + "\n");
|
|
1496
|
+
if (failed)
|
|
1497
|
+
ctl.write(commandOutput(event.result.output, theme, terminalColumns(), true).join("\n") + "\n");
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
const wrapper = wrappers.get(event.result.callId);
|
|
1501
|
+
if (wrapper) {
|
|
1502
|
+
wrappers.delete(event.result.callId);
|
|
1503
|
+
// An identical child failure is already visible; a distinct wrapper
|
|
1504
|
+
// failure (validation, postprocessing) must still be shown.
|
|
1505
|
+
if (wrapper.child && (!event.result.isError || wrapper.isError && wrapper.output === event.result.output))
|
|
1506
|
+
break;
|
|
1507
|
+
flushText();
|
|
1508
|
+
ensureAssistantStarted();
|
|
1509
|
+
const source = call && commandSource(call);
|
|
1510
|
+
if (source !== undefined) {
|
|
1511
|
+
ctl.write("\n" + commandPreview(source, theme, terminalColumns(), commandResultState(event.result.output, event.result.isError)).lines.join("\n") + "\n");
|
|
1512
|
+
}
|
|
1513
|
+
else {
|
|
1514
|
+
ctl.write("\n" + fitLine((event.result.isError ? theme.red(" × ") : theme.green(" ● "))
|
|
1515
|
+
+ toolLabel(call?.name ?? "execute"), terminalColumns() - 1) + "\n");
|
|
1516
|
+
}
|
|
1517
|
+
ctl.write(commandOutput(event.result.output, theme, terminalColumns(), event.result.isError).join("\n") + "\n");
|
|
1518
|
+
break;
|
|
1519
|
+
}
|
|
1520
|
+
const mcpCard = mcpCards.get(event.result.callId);
|
|
1521
|
+
if (mcpCard && call) {
|
|
1522
|
+
flushText();
|
|
1523
|
+
const state = event.result.isError ? "error" : "ok";
|
|
1524
|
+
const retained = !ctl.getLogLine || ctl.getLogLine(mcpCard.index) === mcpCard.header;
|
|
1525
|
+
if (retained)
|
|
1526
|
+
ctl.setLogLine?.(mcpCard.index, mcpHeader(state, theme, terminalColumns()));
|
|
1527
|
+
if (!retained || !ctl.setLogLine || ctl.logLength() !== mcpCard.end) {
|
|
1528
|
+
ctl.write("\n" + mcpPreview(call, theme, terminalColumns(), state).join("\n") + "\n");
|
|
1529
|
+
}
|
|
1530
|
+
ctl.write(mcpOutput(event.result.output, event.result.isError, theme, terminalColumns()).join("\n") + "\n");
|
|
1531
|
+
mcpCards.delete(event.result.callId);
|
|
1532
|
+
break;
|
|
1533
|
+
}
|
|
1534
|
+
const card = commandCards.get(event.result.callId);
|
|
1535
|
+
if (card && call) {
|
|
1536
|
+
const state = commandResultState(event.result.output, event.result.isError);
|
|
1537
|
+
const retained = !ctl.getLogLine || ctl.getLogLine(card.index) === card.header;
|
|
1538
|
+
if (retained)
|
|
1539
|
+
ctl.setLogLine?.(card.index, commandHeader(card.firstRow, state, theme, card.columns));
|
|
1540
|
+
// Parallel calls can complete in a different order. Repeat the
|
|
1541
|
+
// command above its result when another block intervened.
|
|
1542
|
+
if (ctl.logLength() !== card.end || !ctl.setLogLine || !retained) {
|
|
1543
|
+
flushText();
|
|
1544
|
+
ctl.write("\n" + commandPreview(commandSource(call), theme, terminalColumns(), state).lines.join("\n") + "\n");
|
|
1545
|
+
}
|
|
1546
|
+
ctl.write(commandOutput(event.result.output, theme, terminalColumns(), event.result.isError).join("\n") + "\n");
|
|
1547
|
+
commandCards.delete(event.result.callId);
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1550
|
+
if (call && !event.result.isError && call.name === "write_file") {
|
|
1551
|
+
ctl.review?.change(call.id, { before: call.input["append"] === true ? "" : preContent.get(call.id),
|
|
1552
|
+
after: String(call.input["content"] ?? ""), scope: call.input["append"] === true ? "append" : "file" });
|
|
1553
|
+
}
|
|
1554
|
+
// Token per stanza/castello (solo con vascend attivo). Snapshot a
|
|
1555
|
+
// "running", costo a "completed"/"failed", riepilogo a castello completo.
|
|
1556
|
+
if (plan && call && call.name === "vascend_mark" && !event.result.isError) {
|
|
1557
|
+
trackRoomTokens(plan, call, event.result.output, outTok, roomStart, summarized, ctl, theme, terminalColumns());
|
|
1558
|
+
}
|
|
1559
|
+
// Stanze Danilov (vascend_mark): NIENTE riga in chat (era rumoroso) —
|
|
1560
|
+
// avvio/illuminazione/fallimento vanno nella status-line, animati.
|
|
1561
|
+
if (call && call.name === "vascend_mark" && !event.result.isError) {
|
|
1562
|
+
const task = String(call.input["task"] ?? "");
|
|
1563
|
+
const slug = String(call.input["group"] ?? call.input["castle"] ?? "master");
|
|
1564
|
+
const st = String(call.input["status"] ?? "completed");
|
|
1565
|
+
const where = slug === "master" ? "" : `${slug}/`;
|
|
1566
|
+
if (st === "running")
|
|
1567
|
+
ctl.setRoomStatus?.(`attività ${where}${task} avviata`, "running");
|
|
1568
|
+
else if (st === "failed")
|
|
1569
|
+
ctl.setRoomStatus?.(`attività ${where}${task} non riuscita`, "failed");
|
|
1570
|
+
else
|
|
1571
|
+
ctl.setRoomStatus?.(`attività ${where}${task} completata`, "done");
|
|
1572
|
+
break;
|
|
1573
|
+
}
|
|
1574
|
+
// Detailed plans are available in the task panel.
|
|
1575
|
+
const notice = call && !event.result.isError
|
|
1576
|
+
? renderVascendToolNotice(call, event.result.output, theme, terminalColumns())
|
|
1577
|
+
: null;
|
|
1578
|
+
if (notice) {
|
|
1579
|
+
notifyStatus(ctl, notice);
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1582
|
+
// Associate out-of-order results with their own operation rather than
|
|
1583
|
+
// appending several anonymous result lines below the newest call.
|
|
1584
|
+
if (toolCard && toolCard.end !== ctl.logLength())
|
|
1585
|
+
ctl.write("\n" + toolCard.header + "\n");
|
|
1586
|
+
// Stile Claude Code: connettore ad albero col risultato sintetico. Il
|
|
1587
|
+
// connettore e' in accent (ancora visivo della riga tool), il testo in
|
|
1588
|
+
// muted (piu' leggibile del dim slavato, ma chiaramente secondario).
|
|
1589
|
+
{
|
|
1590
|
+
const summary = liveResultSummary(event.result.output, event.result.isError ? null : resultSummary(call, event.result.output), terminalColumns());
|
|
1591
|
+
ctl.write(theme.dark(" │ ") + (event.result.isError ? theme.red("× ") : theme.dark("⎿ ")) +
|
|
1592
|
+
(event.result.isError ? theme.red(summary) : theme.dim(summary)) + "\n");
|
|
1593
|
+
}
|
|
1594
|
+
// Keep the change preview alongside its operation.
|
|
1595
|
+
if (call && !event.result.isError) {
|
|
1596
|
+
const diff = diffForCall(call, preContent.get(event.result.callId), event.result.output, theme, terminalColumns());
|
|
1597
|
+
if (diff) {
|
|
1598
|
+
ctl.write(previewDiff(diff, theme, terminalColumns() - 1));
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
break;
|
|
1602
|
+
}
|
|
1603
|
+
case "compacted":
|
|
1604
|
+
stopDot();
|
|
1605
|
+
flushText();
|
|
1606
|
+
notifyStatus(ctl, `Contesto aggiornato: ~${formatTokenCount(event.tokensAfter)} token`);
|
|
1607
|
+
break;
|
|
1608
|
+
case "workspace_message":
|
|
1609
|
+
flushText();
|
|
1610
|
+
writeNotice(`sessione ${event.message.fromLabel}: ${oneLine(event.message.text)}`);
|
|
1611
|
+
break;
|
|
1612
|
+
case "user_message":
|
|
1613
|
+
// Un messaggio scritto durante il lavoro e' appena entrato nel giro:
|
|
1614
|
+
// l'agente lo legge adesso, non a fine risposta.
|
|
1615
|
+
stopDot();
|
|
1616
|
+
flushText();
|
|
1617
|
+
closeAssistant();
|
|
1618
|
+
ctl.write(renderUserMessage(ctl.acceptQueuedMessage?.(event.id) ?? event.text, theme, terminalColumns(), event.immediate ? tr("Inviato subito · preso in carico", "Sent immediately · received") : tr("Dalla coda · consegnato durante il lavoro", "From queue · delivered while working")));
|
|
1619
|
+
assistantSegments = 0;
|
|
1620
|
+
assistantStarted = false;
|
|
1621
|
+
assistantClosed = false;
|
|
1622
|
+
break;
|
|
1623
|
+
case "error":
|
|
1624
|
+
for (const card of mcpCards.values()) {
|
|
1625
|
+
if (!ctl.getLogLine || ctl.getLogLine(card.index) === card.header)
|
|
1626
|
+
ctl.setLogLine?.(card.index, mcpHeader("stopped", theme, terminalColumns()));
|
|
1627
|
+
}
|
|
1628
|
+
mcpCards.clear();
|
|
1629
|
+
ctl.pauseQueuedMessages?.();
|
|
1630
|
+
stopDot();
|
|
1631
|
+
flushText("final");
|
|
1632
|
+
closeAssistant("error");
|
|
1633
|
+
for (const card of commandCards.values()) {
|
|
1634
|
+
if (!ctl.getLogLine || ctl.getLogLine(card.index) === card.header)
|
|
1635
|
+
ctl.setLogLine?.(card.index, commandHeader(card.firstRow, "stopped", theme, card.columns));
|
|
1636
|
+
}
|
|
1637
|
+
commandCards.clear();
|
|
1638
|
+
writeNotice(`errore: ${event.message}`, true);
|
|
1639
|
+
break;
|
|
1640
|
+
case "done":
|
|
1641
|
+
for (const card of mcpCards.values()) {
|
|
1642
|
+
if (!ctl.getLogLine || ctl.getLogLine(card.index) === card.header)
|
|
1643
|
+
ctl.setLogLine?.(card.index, mcpHeader("stopped", theme, terminalColumns()));
|
|
1644
|
+
}
|
|
1645
|
+
mcpCards.clear();
|
|
1646
|
+
stopDot();
|
|
1647
|
+
flushText("final");
|
|
1648
|
+
closeAssistant();
|
|
1649
|
+
for (const card of commandCards.values()) {
|
|
1650
|
+
if (!ctl.getLogLine || ctl.getLogLine(card.index) === card.header)
|
|
1651
|
+
ctl.setLogLine?.(card.index, commandHeader(card.firstRow, "stopped", theme, card.columns));
|
|
1652
|
+
}
|
|
1653
|
+
commandCards.clear();
|
|
1654
|
+
// Auto-continuazione esaurita: il run finisce ancora troncato. Avvisa di
|
|
1655
|
+
// spezzare il compito (o alzare TALOS_MAX_AUTOCONTINUE).
|
|
1656
|
+
if (lastTruncated) {
|
|
1657
|
+
notifyStatus(ctl, theme.yellow("\n ⚠ output ancora al limite del modello dopo le continuazioni automatiche: spezza il compito o alza TALOS_MAX_AUTOCONTINUE.\n"));
|
|
1658
|
+
}
|
|
1659
|
+
break;
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
// Attribuzione token alle stanze + riepilogo per castello (metodo Danilov).
|
|
1664
|
+
// All'avvio della stanza (running) salva lo snapshot di outTok; a stanza
|
|
1665
|
+
// completata/fallita calcola il delta (token generati), lo persiste nel piano e,
|
|
1666
|
+
// se il castello e' completo, mostra il riepilogo micro (per stanza) + totale.
|
|
1667
|
+
function trackRoomTokens(plan, call, output, outTok, roomStart, summarized, ctl, theme, columns) {
|
|
1668
|
+
const slug = String(call.input["group"] ?? call.input["castle"] ?? "master");
|
|
1669
|
+
const m = /\]\s+(T\d+)\s+0x[0-9a-f]+\s*->\s*(\w+)/i.exec(output);
|
|
1670
|
+
if (!m)
|
|
1671
|
+
return;
|
|
1672
|
+
const id = m[1];
|
|
1673
|
+
const status = (m[2] ?? "").toLowerCase();
|
|
1674
|
+
const key = `${slug}/${id}`;
|
|
1675
|
+
if (status === "running") {
|
|
1676
|
+
roomStart.set(key, outTok);
|
|
1677
|
+
summarized.delete(slug); // castello tornato in lavorazione
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
if (status !== "completed" && status !== "failed")
|
|
1681
|
+
return;
|
|
1682
|
+
const start = roomStart.get(key);
|
|
1683
|
+
const store = VascendPlanStore.open(plan.sdir, plan.sid);
|
|
1684
|
+
if (start !== undefined) {
|
|
1685
|
+
store.setRoomTokens(slug, id, Math.max(0, outTok - start));
|
|
1686
|
+
roomStart.delete(key);
|
|
1687
|
+
}
|
|
1688
|
+
// Riepilogo se il castello e' completo (nessuna stanza pending/running).
|
|
1689
|
+
const castle = store.castle(slug);
|
|
1690
|
+
if (!castle || summarized.has(slug))
|
|
1691
|
+
return;
|
|
1692
|
+
const open = castle.tasks.some((t) => t.status === "pending" || t.status === "running");
|
|
1693
|
+
if (open)
|
|
1694
|
+
return;
|
|
1695
|
+
summarized.add(slug);
|
|
1696
|
+
notifyStatus(ctl, renderCastleTokens(castle, theme, columns));
|
|
1697
|
+
}
|
|
1698
|
+
// Riepilogo token di un castello su una sola riga. Le metriche per stanza sono
|
|
1699
|
+
// nel piano Ctrl+G: ripeterle qui rendeva il flusso molto rumoroso.
|
|
1700
|
+
function renderCastleTokens(castle, theme, columns) {
|
|
1701
|
+
const total = castle.tasks.reduce((s, t) => s + (t.tokens ?? 0), 0);
|
|
1702
|
+
const failed = castle.tasks.filter((task) => task.status !== "completed").length;
|
|
1703
|
+
const available = Math.max(20, columns - 56);
|
|
1704
|
+
const title = castle.title.length > available ? castle.title.slice(0, available - 1) + "…" : castle.title;
|
|
1705
|
+
const result = failed ? theme.yellow(`${castle.tasks.length - failed}/${castle.tasks.length}`) : theme.green(`${castle.tasks.length}/${castle.tasks.length}`);
|
|
1706
|
+
return theme.green(`\n ◆ ${title}`) + theme.dim(" · completato · ") + result +
|
|
1707
|
+
theme.dim(` attività · ↓${fmtTok(total)} · Ctrl+G dettagli\n`);
|
|
1708
|
+
}
|
|
1709
|
+
// Snapshot del piano (Ctrl+G): castelli, stanze e STANZA CORRENTE evidenziata.
|
|
1710
|
+
// Disponibile in ogni momento, anche durante un run.
|
|
1711
|
+
function renderPlanSnapshot(state, theme, columns) {
|
|
1712
|
+
const castles = state.kingdom.castles;
|
|
1713
|
+
if (!castles.length) {
|
|
1714
|
+
return theme.dim("\n lista delle attività vuota: comparirà quando Talos suddivide il lavoro.\n");
|
|
1715
|
+
}
|
|
1716
|
+
const width = Math.max(30, columns - 6);
|
|
1717
|
+
const all = castles.flatMap((c) => c.tasks);
|
|
1718
|
+
const done = all.filter((t) => t.status === "completed").length;
|
|
1719
|
+
// Stanza corrente: la prima "running"; altrimenti la prima "pending".
|
|
1720
|
+
let cur = null;
|
|
1721
|
+
for (const c of castles) {
|
|
1722
|
+
const r = c.tasks.find((t) => t.status === "running");
|
|
1723
|
+
if (r) {
|
|
1724
|
+
cur = { slug: c.slug, task: r };
|
|
1725
|
+
break;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (!cur)
|
|
1729
|
+
for (const c of castles) {
|
|
1730
|
+
const r = c.tasks.find((t) => t.status === "pending");
|
|
1731
|
+
if (r) {
|
|
1732
|
+
cur = { slug: c.slug, task: r };
|
|
1733
|
+
break;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
const out = [];
|
|
1737
|
+
out.push(theme.accent(`\n ◆ lista delle attività`) + theme.dim(` · ${castles.length} grupp${castles.length === 1 ? "o" : "i"} · ${done}/${all.length} attività`));
|
|
1738
|
+
for (const c of castles) {
|
|
1739
|
+
const cd = c.tasks.filter((t) => t.status === "completed").length;
|
|
1740
|
+
const dep = c.after ? theme.dim(` · dopo ${c.after}`) : "";
|
|
1741
|
+
out.push(theme.silver(` ◆ ${c.title || c.slug}`) + theme.dim(` [${c.slug}] · ${cd}/${c.tasks.length}`) + dep);
|
|
1742
|
+
for (const t of c.tasks) {
|
|
1743
|
+
const isCur = cur && cur.slug === c.slug && cur.task.id === t.id;
|
|
1744
|
+
const tok = t.tokens !== undefined ? theme.dim(` ↓${fmtTok(t.tokens)}`) : "";
|
|
1745
|
+
const title = t.title.length > width - 18 ? t.title.slice(0, width - 21) + "…" : t.title;
|
|
1746
|
+
const row = ` ${vascendRoomGlyph(t.status)} ${t.id} ${title}${tok}`;
|
|
1747
|
+
out.push(isCur ? theme.accent(` →`) + theme.silver(row.slice(5)) : theme.dim(row));
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
if (cur) {
|
|
1751
|
+
out.push(theme.accent(` → attività corrente: `) + theme.silver(`[${cur.slug}] ${cur.task.id} — ${cur.task.title}`));
|
|
1752
|
+
}
|
|
1753
|
+
else {
|
|
1754
|
+
out.push(theme.green(" ✓ tutte le attività completate"));
|
|
1755
|
+
}
|
|
1756
|
+
return out.join("\n") + "\n";
|
|
1757
|
+
}
|
|
1758
|
+
// Vista temporanea di /bg: vive nella regione dell'input e non nello storico
|
|
1759
|
+
// della chat. Esc, Invio o q la chiudono senza lasciare righe residue.
|
|
1760
|
+
async function showBackgroundPanel(ctl, theme, terminalColumns) {
|
|
1761
|
+
const list = backgroundManager.list();
|
|
1762
|
+
const width = Math.max(40, terminalColumns());
|
|
1763
|
+
const lines = list.length
|
|
1764
|
+
? list.map((t) => {
|
|
1765
|
+
const mark = t.status === "running" ? "●" : t.status === "done" ? "✓" : "✗";
|
|
1766
|
+
const dur = t.endedAt
|
|
1767
|
+
? `${Math.round((t.endedAt - t.startedAt) / 1000)}s`
|
|
1768
|
+
: `${Math.round((Date.now() - t.startedAt) / 1000)}s`;
|
|
1769
|
+
return `${mark} ${t.id} ${t.status} · ${dur} ${truncate(t.command, Math.max(24, width - 30))}`;
|
|
1770
|
+
})
|
|
1771
|
+
: ["Nessuna attivita' shell in background."];
|
|
1772
|
+
await showReadonlyOverlay({
|
|
1773
|
+
title: list.length ? `attivita' in background · ${list.length}` : "attivita' in background",
|
|
1774
|
+
lines,
|
|
1775
|
+
ctl,
|
|
1776
|
+
theme,
|
|
1777
|
+
terminalColumns,
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
// Riconosce un comando git dentro un comando bash e ne ritorna un riassunto
|
|
1781
|
+
// leggibile (es. "push origin master", "commit", "checkout main"), o null se non
|
|
1782
|
+
// e' git. Estrae il primo segmento git (gestisce "cd x && git ...").
|
|
1783
|
+
export function gitSummary(command) {
|
|
1784
|
+
const m = /\bgit\s+(?:-C\s+\S+\s+)?([a-z][\w-]*)/i.exec(command);
|
|
1785
|
+
if (!m)
|
|
1786
|
+
return null;
|
|
1787
|
+
const sub = (m[1] ?? "").toLowerCase();
|
|
1788
|
+
const withTarget = new Set(["push", "pull", "fetch", "checkout", "switch", "merge", "rebase", "branch", "clone", "remote"]);
|
|
1789
|
+
if (withTarget.has(sub)) {
|
|
1790
|
+
const after = command.slice(m.index + m[0].length).split(/&&|;|\||\n/)[0] ?? "";
|
|
1791
|
+
const tokens = after.split(/\s+/).filter((t) => t && !t.startsWith("-")).slice(0, 2);
|
|
1792
|
+
return tokens.length ? `${sub} ${tokens.join(" ")}` : sub;
|
|
1793
|
+
}
|
|
1794
|
+
return sub; // commit/add/status/log/diff/stash/...: niente messaggio/heredoc
|
|
1795
|
+
}
|
|
1796
|
+
function fmtTok(n) {
|
|
1797
|
+
if (n >= 1_000_000) {
|
|
1798
|
+
const v = n / 1_000_000;
|
|
1799
|
+
return `${v >= 10 ? Math.round(v) : v.toFixed(1)}M`;
|
|
1800
|
+
}
|
|
1801
|
+
if (n >= 1000) {
|
|
1802
|
+
const v = n / 1000;
|
|
1803
|
+
return `${v >= 10 ? Math.round(v) : v.toFixed(1)}k`;
|
|
1804
|
+
}
|
|
1805
|
+
return `${n}`;
|
|
1806
|
+
}
|
|
1807
|
+
// Anteprima su una riga: serve alle notifiche brevi (per esempio il messaggio
|
|
1808
|
+
// consegnato al turno in corso), che non devono spezzare il flusso a schermo.
|
|
1809
|
+
export function oneLinePreview(text, max) {
|
|
1810
|
+
const line = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
1811
|
+
return line.length > max ? `${line.slice(0, max - 1).trimEnd()}…` : line;
|
|
1812
|
+
}
|
|
1813
|
+
// Rimuove il markup di tool-call (<invoke>/<function_calls>) eventualmente
|
|
1814
|
+
// emesso come testo dal modello, per non mostrarlo in chat.
|
|
1815
|
+
function stripToolMarkup(text) {
|
|
1816
|
+
if (!/<(?:antml:)?(?:invoke|function_calls)\b/.test(text))
|
|
1817
|
+
return text;
|
|
1818
|
+
return text
|
|
1819
|
+
.replace(/<(?:antml:)?function_calls>[\s\S]*?<\/(?:antml:)?function_calls>/g, "")
|
|
1820
|
+
.replace(/<(?:antml:)?invoke\s+name="[^"]*"\s*>[\s\S]*?<\/(?:antml:)?invoke>/g, "")
|
|
1821
|
+
.replace(/^\s*call\s*$/gm, "")
|
|
1822
|
+
.trim();
|
|
1823
|
+
}
|
|
1824
|
+
function safeRead(abs) {
|
|
1825
|
+
try {
|
|
1826
|
+
return readFileSync(abs, "utf8");
|
|
1827
|
+
}
|
|
1828
|
+
catch {
|
|
1829
|
+
return "";
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
// Avviso di chat per i tool del regno. Ritorna true se l'evento e' stato reso
|
|
1833
|
+
// come avviso (e quindi NON serve la riga generica). Usa l'INPUT della call
|
|
1834
|
+
// (autorevole) e completa col conteggio castelli dall'output.
|
|
1835
|
+
function renderVascendToolNotice(call, output, theme, columns) {
|
|
1836
|
+
if (call.name === "vascend_plan") {
|
|
1837
|
+
const title = String(call.input["title"] ?? "");
|
|
1838
|
+
const slug = String(call.input["group"] ?? call.input["castle"] ?? "master");
|
|
1839
|
+
const tasks = Array.isArray(call.input["tasks"]) ? call.input["tasks"].map(String) : [];
|
|
1840
|
+
const m = /(?:piano|regno):\s*(\d+)/.exec(output);
|
|
1841
|
+
const kingdom = m ? Number(m[1]) : 1;
|
|
1842
|
+
return renderCastleBorn({ title, slug, tasks, kingdomCount: kingdom }, theme, columns);
|
|
1843
|
+
}
|
|
1844
|
+
if (call.name === "vascend_mark") {
|
|
1845
|
+
const task = String(call.input["task"] ?? "");
|
|
1846
|
+
const slug = String(call.input["group"] ?? call.input["castle"] ?? "master");
|
|
1847
|
+
const st = String(call.input["status"] ?? "completed");
|
|
1848
|
+
const where = slug === "master" ? "" : `${slug}/`;
|
|
1849
|
+
if (st === "running")
|
|
1850
|
+
return renderVascendNotice("start", `attività ${where}${task} avviata`, theme);
|
|
1851
|
+
if (st === "failed")
|
|
1852
|
+
return renderVascendNotice("fail", `attività ${where}${task} non riuscita`, theme);
|
|
1853
|
+
return renderVascendNotice("done", `attività ${where}${task} completata`, theme);
|
|
1854
|
+
}
|
|
1855
|
+
return null;
|
|
1856
|
+
}
|
|
1857
|
+
// Diff per i tool di scrittura: write_file (vecchio file su disco -> nuovo
|
|
1858
|
+
// contenuto) ed edit_file (old_string -> new_string, cioe' la regione cambiata).
|
|
1859
|
+
function diffForCall(call, pre, output, theme, columns) {
|
|
1860
|
+
if (call.name === "write_file" && (output.startsWith("scritto:") || output.startsWith("accodato:"))) {
|
|
1861
|
+
const path = typeof call.input["path"] === "string" ? call.input["path"] : undefined;
|
|
1862
|
+
const append = call.input["append"] === true;
|
|
1863
|
+
const next = String(call.input["content"] ?? "");
|
|
1864
|
+
const existed = (pre ?? "") !== "";
|
|
1865
|
+
const verb = append ? "aggiunto" : existed ? "modificato" : "creato";
|
|
1866
|
+
// In append mostriamo solo l'aggiunta (base vuota); altrimenti il delta vs file.
|
|
1867
|
+
return renderDiff(append ? "" : (pre ?? ""), next, theme, { path, verb, columns: columns - 1, wrapLines: true });
|
|
1868
|
+
}
|
|
1869
|
+
if (call.name === "edit_file" && output.startsWith("modificato:")) {
|
|
1870
|
+
const path = typeof call.input["path"] === "string" ? call.input["path"] : undefined;
|
|
1871
|
+
const oldS = String(call.input["old_string"] ?? "");
|
|
1872
|
+
const newS = String(call.input["new_string"] ?? "");
|
|
1873
|
+
return renderDiff(oldS, newS, theme, { path, verb: "modificato", context: 2, columns: columns - 1, wrapLines: true });
|
|
1874
|
+
}
|
|
1875
|
+
return "";
|
|
1876
|
+
}
|
|
1877
|
+
// Soglia riferita alle righe del confronto, escluse intestazione e separatore.
|
|
1878
|
+
function previewDiff(full, theme, width) {
|
|
1879
|
+
const lines = full.replace(/\n$/, "").split("\n");
|
|
1880
|
+
const limit = 100 + 2;
|
|
1881
|
+
if (lines.length <= limit)
|
|
1882
|
+
return full;
|
|
1883
|
+
const head = lines.slice(0, limit);
|
|
1884
|
+
const hidden = lines.length - limit;
|
|
1885
|
+
const hint = ` … altre ${hidden} righe · Ctrl+O scegli file`;
|
|
1886
|
+
head.push(theme.dim(fitLine(terminalWidth(hint) <= width ? hint : ` Ctrl+O · +${hidden} righe`, width)));
|
|
1887
|
+
return head.join("\n") + "\n";
|
|
1888
|
+
}
|
|
1889
|
+
// Board live del regno (Kingdoms Mode): panoramica di TUTTI i vassalli aggiornata
|
|
1890
|
+
// in place nella regione viva, senza riempire la chat di righe. Tiene per ogni
|
|
1891
|
+
// vassallo lo stato (in coda/attivo/fatto/errore) + l'attività corrente, una coda
|
|
1892
|
+
// dei messaggi di cooperazione, e alimenta anche il viewer per-vassallo (Ctrl+G).
|
|
1893
|
+
function createKingdomLive(ctl, theme, castles) {
|
|
1894
|
+
const total = castles.length;
|
|
1895
|
+
const order = castles.map((c) => c.slug);
|
|
1896
|
+
const afterOf = new Map(castles.map((c) => [c.slug, c.after ?? []]));
|
|
1897
|
+
const vstate = new Map(order.map((s) => [s, { st: "pending", act: "" }]));
|
|
1898
|
+
const msgs = [];
|
|
1899
|
+
const startAt = Date.now();
|
|
1900
|
+
const started = new Set();
|
|
1901
|
+
let done = 0;
|
|
1902
|
+
const logs = new Map(order.map((s) => [s, []]));
|
|
1903
|
+
const textBuf = new Map();
|
|
1904
|
+
const pushLine = (slug, line) => {
|
|
1905
|
+
const a = logs.get(slug);
|
|
1906
|
+
if (!a)
|
|
1907
|
+
return;
|
|
1908
|
+
a.push(line);
|
|
1909
|
+
if (a.length > 400)
|
|
1910
|
+
a.splice(0, a.length - 400);
|
|
1911
|
+
};
|
|
1912
|
+
const flushVassalText = (slug) => {
|
|
1913
|
+
const t = (textBuf.get(slug) ?? "").trim();
|
|
1914
|
+
if (t) {
|
|
1915
|
+
for (const l of t.split("\n"))
|
|
1916
|
+
pushLine(slug, theme.dim(` ${l}`));
|
|
1917
|
+
textBuf.set(slug, "");
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
const refreshView = () => ctl.setKingdomVassals(order.map((s) => ({ slug: s, lines: logs.get(s) ?? [] })));
|
|
1921
|
+
const refreshStatus = (slug, act) => ctl.setStatus(`esecuzione parallela · ${done}/${total} gruppi · ${started.size} attivi · [${slug}] ${act}`);
|
|
1922
|
+
const slugW = Math.min(22, Math.max(6, ...order.map((s) => s.length)));
|
|
1923
|
+
const fmtClock = (sec) => `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, "0")}`;
|
|
1924
|
+
const buildBoard = () => {
|
|
1925
|
+
const states = order.map((s) => (vstate.get(s) ?? { st: "pending", act: "" }).st);
|
|
1926
|
+
const active = states.filter((st) => st === "active").length;
|
|
1927
|
+
const ko = states.filter((st) => st === "error").length;
|
|
1928
|
+
const pend = states.filter((st) => st === "pending").length;
|
|
1929
|
+
const el = Math.round((Date.now() - startAt) / 1000);
|
|
1930
|
+
// Mini barra di avanzamento del regno (castelli conclusi su totale).
|
|
1931
|
+
const barW = 14;
|
|
1932
|
+
const fill = total > 0 ? Math.round((done / total) * barW) : 0;
|
|
1933
|
+
const bar = theme.yellow("━".repeat(fill)) + theme.dim("─".repeat(Math.max(0, barW - fill)));
|
|
1934
|
+
const counters = [
|
|
1935
|
+
active ? theme.accent(`▸${active}`) : "",
|
|
1936
|
+
pend ? theme.dim(`◇${pend}`) : "",
|
|
1937
|
+
ko ? theme.red(`✗${ko}`) : "",
|
|
1938
|
+
theme.yellow(`✦${done}`),
|
|
1939
|
+
].filter(Boolean).join(theme.dim(" "));
|
|
1940
|
+
const innerW = slugW + 2 + 56;
|
|
1941
|
+
const out = [
|
|
1942
|
+
theme.accent(" ◆ esecuzione parallela") +
|
|
1943
|
+
theme.dim(` ${bar} `) + theme.silver(`${done}/${total}`) +
|
|
1944
|
+
theme.dim(` ${counters}`) + (el >= 1 ? theme.dim(` ⏱ ${fmtClock(el)}`) : ""),
|
|
1945
|
+
theme.dim(" " + "┄".repeat(Math.min(innerW, 64))),
|
|
1946
|
+
];
|
|
1947
|
+
for (const s of order) {
|
|
1948
|
+
const v = vstate.get(s) ?? { st: "pending", act: "" };
|
|
1949
|
+
const icon = v.st === "done" ? theme.yellow("✦") :
|
|
1950
|
+
v.st === "error" ? theme.red("✗") :
|
|
1951
|
+
v.st === "active" ? theme.accent("▸") : theme.dim("◇");
|
|
1952
|
+
const nameColor = v.st === "active" ? theme.silver : theme.dim;
|
|
1953
|
+
const name = nameColor(`${s}`.padEnd(slugW).slice(0, slugW));
|
|
1954
|
+
let detail;
|
|
1955
|
+
if (v.st === "pending") {
|
|
1956
|
+
const af = afterOf.get(s) ?? [];
|
|
1957
|
+
detail = af.length ? theme.yellow(`⟸ dopo ${af.join(", ")}`) : theme.dim("in coda");
|
|
1958
|
+
}
|
|
1959
|
+
else if (v.st === "done")
|
|
1960
|
+
detail = theme.yellow("completato");
|
|
1961
|
+
else if (v.st === "error")
|
|
1962
|
+
detail = theme.red(truncate(v.act || "errore", 52));
|
|
1963
|
+
else
|
|
1964
|
+
detail = theme.silver(truncate(v.act || "…", 54));
|
|
1965
|
+
out.push(` ${icon} ${name} ${theme.dim("·")} ${detail}`);
|
|
1966
|
+
}
|
|
1967
|
+
if (msgs.length) {
|
|
1968
|
+
out.push(theme.dim(" " + "┄".repeat(Math.min(innerW, 64))));
|
|
1969
|
+
out.push(theme.accent(" ✉ cooperazione"));
|
|
1970
|
+
for (const m of msgs.slice(-3))
|
|
1971
|
+
out.push(theme.dim(` ${m}`));
|
|
1972
|
+
}
|
|
1973
|
+
out.push(theme.dim(" " + "┄".repeat(Math.min(innerW, 64))));
|
|
1974
|
+
out.push(theme.dim(" Ctrl+G dettaglio collaboratore · Esc ferma"));
|
|
1975
|
+
return out;
|
|
1976
|
+
};
|
|
1977
|
+
const refreshBoard = () => ctl.setKingdomBoard(buildBoard());
|
|
1978
|
+
ctl.setStatus(`esecuzione parallela · 0/${total} gruppi`);
|
|
1979
|
+
refreshView();
|
|
1980
|
+
refreshBoard();
|
|
1981
|
+
const vassalCalls = new Map();
|
|
1982
|
+
const onEvent = (slug, e) => {
|
|
1983
|
+
ctl.review?.recordActor(slug, e);
|
|
1984
|
+
if (!started.has(slug)) {
|
|
1985
|
+
started.add(slug);
|
|
1986
|
+
vstate.set(slug, { st: "active", act: "avvio" });
|
|
1987
|
+
pushLine(slug, theme.accent("▸ avviato"));
|
|
1988
|
+
refreshBoard();
|
|
1989
|
+
}
|
|
1990
|
+
if (e.type === "text") {
|
|
1991
|
+
textBuf.set(slug, (textBuf.get(slug) ?? "") + e.delta);
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
if (e.type === "tool_call") {
|
|
1995
|
+
flushVassalText(slug);
|
|
1996
|
+
vassalCalls.set(e.call.id, e.call);
|
|
1997
|
+
pushLine(slug, theme.accent(`● ${toolLabel(e.call.name)}`) + theme.dim(`(${toolArg(e.call)})`));
|
|
1998
|
+
const arg = toolArg(e.call);
|
|
1999
|
+
vstate.set(slug, { st: "active", act: `${toolLabel(e.call.name)}${arg ? ` ${arg}` : ""}` });
|
|
2000
|
+
if (e.call.name === "kingdom_send") {
|
|
2001
|
+
const to = typeof e.call.input["to"] === "string" && e.call.input["to"] ? e.call.input["to"] : "tutti";
|
|
2002
|
+
msgs.push(`✉ ${slug}→${to} ${truncate(String(e.call.input["text"] ?? ""), 54)}`);
|
|
2003
|
+
if (msgs.length > 16)
|
|
2004
|
+
msgs.splice(0, msgs.length - 16);
|
|
2005
|
+
}
|
|
2006
|
+
refreshStatus(slug, e.call.name);
|
|
2007
|
+
refreshBoard();
|
|
2008
|
+
}
|
|
2009
|
+
else if (e.type === "tool_result") {
|
|
2010
|
+
const vcall = vassalCalls.get(e.result.callId);
|
|
2011
|
+
const vsum = e.result.isError ? firstLine(e.result.output) : (resultSummary(vcall, e.result.output) ?? firstLine(e.result.output));
|
|
2012
|
+
pushLine(slug, (e.result.isError ? theme.red(" ⎿ ") : theme.dim(" ⎿ ")) + theme.dim(vsum));
|
|
2013
|
+
}
|
|
2014
|
+
else if (e.type === "turn_start") {
|
|
2015
|
+
const v = vstate.get(slug);
|
|
2016
|
+
if (v && v.st === "active")
|
|
2017
|
+
vstate.set(slug, { st: "active", act: "ragiona" });
|
|
2018
|
+
refreshStatus(slug, "ragiona");
|
|
2019
|
+
refreshBoard();
|
|
2020
|
+
}
|
|
2021
|
+
else if (e.type === "done") {
|
|
2022
|
+
flushVassalText(slug);
|
|
2023
|
+
done += 1;
|
|
2024
|
+
vstate.set(slug, { st: "done", act: "" });
|
|
2025
|
+
pushLine(slug, theme.yellow("✦ completato"));
|
|
2026
|
+
refreshBoard();
|
|
2027
|
+
}
|
|
2028
|
+
else if (e.type === "error") {
|
|
2029
|
+
flushVassalText(slug);
|
|
2030
|
+
done += 1;
|
|
2031
|
+
vstate.set(slug, { st: "error", act: e.message });
|
|
2032
|
+
pushLine(slug, theme.red(`✗ ${e.message}`));
|
|
2033
|
+
refreshBoard();
|
|
2034
|
+
}
|
|
2035
|
+
refreshView();
|
|
2036
|
+
};
|
|
2037
|
+
const finish = () => {
|
|
2038
|
+
ctl.setKingdomBoard(null);
|
|
2039
|
+
ctl.setKingdomVassals(null);
|
|
2040
|
+
ctl.setStatus(null);
|
|
2041
|
+
};
|
|
2042
|
+
return { onEvent, finish };
|
|
2043
|
+
}
|
|
2044
|
+
// Eta' leggibile di un timestamp (per il picker delle sessioni).
|
|
2045
|
+
function fmtAge(ts) {
|
|
2046
|
+
const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
|
|
2047
|
+
if (s < 60)
|
|
2048
|
+
return tr("{count}s fa", "{count}s ago", { count: s });
|
|
2049
|
+
const m = Math.round(s / 60);
|
|
2050
|
+
if (m < 60)
|
|
2051
|
+
return tr("{count}m fa", "{count}m ago", { count: m });
|
|
2052
|
+
const h = Math.round(m / 60);
|
|
2053
|
+
if (h < 24)
|
|
2054
|
+
return tr("{count}h fa", "{count}h ago", { count: h });
|
|
2055
|
+
return tr("{count}g fa", "{count}d ago", { count: Math.round(h / 24) });
|
|
2056
|
+
}
|
|
2057
|
+
// Opzioni del picker /resume a partire dalle sintesi delle sessioni.
|
|
2058
|
+
function sessionOptions(list) {
|
|
2059
|
+
return list.map((s) => ({
|
|
2060
|
+
key: s.id,
|
|
2061
|
+
label: sessionTitle(s.name ?? "") || s.preview || s.id,
|
|
2062
|
+
detail: `${fmtAge(s.updatedAt)} · ${s.messages} msg · ${s.id}`,
|
|
2063
|
+
value: s.id,
|
|
2064
|
+
}));
|
|
2065
|
+
}
|
|
2066
|
+
// Riprende una sessione: la carica nell'harness (le closure vedono il nuovo
|
|
2067
|
+
// valore), ricarica il piano Vascend e sostituisce la conversazione visibile
|
|
2068
|
+
// nello scrollback cosi' l'utente ritrova il contesto. Le sessioni sopravvivono
|
|
2069
|
+
// ai riavvii/crash: il prompt non va piu' ridigitato.
|
|
2070
|
+
function resumeInto(harness, vascend, id, ctl, theme, tcols, resetSessionUi) {
|
|
2071
|
+
let loaded;
|
|
2072
|
+
try {
|
|
2073
|
+
if (!existsSync(sessionFilePath(harness.config.sessionDir, id)))
|
|
2074
|
+
throw new Error(tr("sessione non trovata", "session not found"));
|
|
2075
|
+
loaded = harness.loadSession(id);
|
|
2076
|
+
}
|
|
2077
|
+
catch (err) {
|
|
2078
|
+
ctl.write(theme.yellow("\n " + tr("impossibile aprire la sessione {id}: {error}", "Unable to open session {id}: {error}", { id, error: err.message }) + "\n"));
|
|
2079
|
+
return false;
|
|
2080
|
+
}
|
|
2081
|
+
vascend.loadPlan(harness.config.sessionDir, loaded.id);
|
|
2082
|
+
resetSessionUi();
|
|
2083
|
+
replaySession(loaded, ctl, theme, tcols());
|
|
2084
|
+
return true;
|
|
2085
|
+
}
|
|
2086
|
+
// Etichetta leggibile del tool (stile Claude Code: Read/Write/Update/Bash...).
|
|
2087
|
+
function toolLabel(name) {
|
|
2088
|
+
const map = {
|
|
2089
|
+
read_file: "Read",
|
|
2090
|
+
write_file: "Write",
|
|
2091
|
+
edit_file: "Update",
|
|
2092
|
+
list_dir: "List",
|
|
2093
|
+
bash: "Bash",
|
|
2094
|
+
bash_output: "BashOutput",
|
|
2095
|
+
web_fetch: "Fetch",
|
|
2096
|
+
ask_user: "Ask",
|
|
2097
|
+
wait: "Wait",
|
|
2098
|
+
tool_search: "ToolSearch",
|
|
2099
|
+
tool_invoke: "Invoke",
|
|
2100
|
+
skill_search: "SkillSearch",
|
|
2101
|
+
skill_load: "SkillLoad",
|
|
2102
|
+
output_page: "Page",
|
|
2103
|
+
kingdoms_warm: tr("Collaboratori", "Collaborators"),
|
|
2104
|
+
kingdom_send: "Send",
|
|
2105
|
+
kingdom_inbox: "Inbox",
|
|
2106
|
+
kingdom_wait: "WaitMsg",
|
|
2107
|
+
glob: "Glob",
|
|
2108
|
+
grep: "Grep",
|
|
2109
|
+
recall: "Recall",
|
|
2110
|
+
image_generate: "Image",
|
|
2111
|
+
vascend_plan: tr("Lista attività", "Task list"),
|
|
2112
|
+
vascend_mark: tr("Stato attività", "Task status"),
|
|
2113
|
+
vascend_note: "Note",
|
|
2114
|
+
vascend_skill: "Skill",
|
|
2115
|
+
vascend_addtasks: tr("Aggiungi attività", "Add tasks"),
|
|
2116
|
+
vascend_addrooms: tr("Aggiungi attività", "Add tasks"),
|
|
2117
|
+
vascend_status: tr("Lista attività", "Task list"),
|
|
2118
|
+
};
|
|
2119
|
+
return map[name] ?? name;
|
|
2120
|
+
}
|
|
2121
|
+
// Argomento mostrato accanto al tool: il path se c'e', il comando per bash,
|
|
2122
|
+
// altrimenti gli argomenti compatti.
|
|
2123
|
+
function toolArg(call) {
|
|
2124
|
+
const p = call.input["path"];
|
|
2125
|
+
if (call.name === "run_program") {
|
|
2126
|
+
const code = String(call.input["code"] ?? "");
|
|
2127
|
+
return tr("JavaScript · {count} righe · Ctrl+R dettagli", "JavaScript · {count} lines · Ctrl+R details", { count: code.split("\n").length });
|
|
2128
|
+
}
|
|
2129
|
+
if (call.name === "tool_invoke") {
|
|
2130
|
+
const inner = String(call.input["name"] ?? "");
|
|
2131
|
+
const a = call.input["args"] && typeof call.input["args"] === "object" ? compactArgs(call.input["args"]) : "";
|
|
2132
|
+
return truncate(`${inner}${a && a !== "{}" ? ` ${a}` : ""}`, 56);
|
|
2133
|
+
}
|
|
2134
|
+
if (call.name === "tool_search" || call.name === "skill_search")
|
|
2135
|
+
return truncate(String(call.input["query"] ?? ""), 50);
|
|
2136
|
+
if (call.name === "skill_load")
|
|
2137
|
+
return String(call.input["name"] ?? "");
|
|
2138
|
+
if (call.name === "grep" || call.name === "glob")
|
|
2139
|
+
return truncate(String(call.input["pattern"] ?? ""), 50);
|
|
2140
|
+
if (call.name === "wait") {
|
|
2141
|
+
const until = call.input["until"];
|
|
2142
|
+
return typeof until === "string" && until ? `until ${truncate(until, 44)}` : `${call.input["seconds"] ?? 5}s`;
|
|
2143
|
+
}
|
|
2144
|
+
// Tool del metodo Danilov: argomento LEGGIBILE invece del JSON grezzo
|
|
2145
|
+
// (es. Mark(T01 → running) al posto di Mark({"task":"T01","status":"running"})).
|
|
2146
|
+
const danilov = vascendArg(call);
|
|
2147
|
+
if (danilov !== null)
|
|
2148
|
+
return danilov;
|
|
2149
|
+
if (typeof p === "string" && p)
|
|
2150
|
+
return p;
|
|
2151
|
+
if (call.name === "bash")
|
|
2152
|
+
return truncate(oneLine(String(call.input["command"] ?? "")), 60);
|
|
2153
|
+
return compactArgs(call.input);
|
|
2154
|
+
}
|
|
2155
|
+
// Argomento LEGGIBILE per i tool del metodo Danilov: trasforma l'input grezzo
|
|
2156
|
+
// in una sintesi human-friendly. Ritorna null se non e' un tool Danilov noto
|
|
2157
|
+
// (cosi' toolArg prosegue con la sua logica standard).
|
|
2158
|
+
function vascendArg(call) {
|
|
2159
|
+
const i = call.input;
|
|
2160
|
+
const castle = typeof i["castle"] === "string" && i["castle"] && i["castle"] !== "master" ? ` @${i["castle"]}` : "";
|
|
2161
|
+
switch (call.name) {
|
|
2162
|
+
case "vascend_mark": {
|
|
2163
|
+
const task = String(i["task"] ?? "");
|
|
2164
|
+
const status = String(i["status"] ?? "completed");
|
|
2165
|
+
return truncate(`${task} → ${status}${castle}`, 56);
|
|
2166
|
+
}
|
|
2167
|
+
case "vascend_plan": {
|
|
2168
|
+
const tasks = Array.isArray(i["tasks"]) ? i["tasks"].length : 0;
|
|
2169
|
+
const title = typeof i["title"] === "string" ? i["title"] : "";
|
|
2170
|
+
const n = tr("{count} attività", "{count} tasks", { count: tasks });
|
|
2171
|
+
return truncate(title ? `${title} · ${n}${castle}` : `${n}${castle}`, 60);
|
|
2172
|
+
}
|
|
2173
|
+
case "vascend_addtasks": {
|
|
2174
|
+
const tasks = Array.isArray(i["tasks"]) ? i["tasks"].length : 0;
|
|
2175
|
+
return truncate("+" + tr("{count} attività", "{count} tasks", { count: tasks }) + castle, 56);
|
|
2176
|
+
}
|
|
2177
|
+
case "vascend_note": {
|
|
2178
|
+
const task = typeof i["task"] === "string" && i["task"] ? `${i["task"]}: ` : "";
|
|
2179
|
+
const text = String(i["text"] ?? i["note"] ?? "");
|
|
2180
|
+
return truncate(`${task}${text}`, 56);
|
|
2181
|
+
}
|
|
2182
|
+
case "vascend_skill": {
|
|
2183
|
+
const task = typeof i["task"] === "string" && i["task"] ? `${i["task"]}: ` : "";
|
|
2184
|
+
const skills = Array.isArray(i["skills"]) ? i["skills"].join(", ") : String(i["skills"] ?? i["skill"] ?? "");
|
|
2185
|
+
return truncate(`${task}${skills}`, 56);
|
|
2186
|
+
}
|
|
2187
|
+
default:
|
|
2188
|
+
return null;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
function compactArgs(input) {
|
|
2192
|
+
const s = JSON.stringify(input);
|
|
2193
|
+
return s.length > 80 ? s.slice(0, 77) + "..." : s;
|
|
2194
|
+
}
|
|
2195
|
+
function firstLine(s) {
|
|
2196
|
+
const line = s.split("\n")[0] ?? "";
|
|
2197
|
+
return line.length > 100 ? line.slice(0, 97) + "..." : line;
|
|
2198
|
+
}
|
|
2199
|
+
// Riepilogo sintetico del risultato per i tool di LETTURA: il loro output e' il
|
|
2200
|
+
// CONTENUTO grezzo (es. il file letto), la cui prima riga puo' essere vuota o
|
|
2201
|
+
// un solo "}", quindi inutile da mostrare sotto il connettore. Per questi tool
|
|
2202
|
+
// si mostra invece un conteggio leggibile (N righe, eventuale range). Ritorna
|
|
2203
|
+
// null per i tool dove la prima riga e' gia' un buon riassunto (read errori,
|
|
2204
|
+
// write/edit, bash, vascend...). `n` plurale italiano semplice.
|
|
2205
|
+
function resultSummary(call, output) {
|
|
2206
|
+
if (!call)
|
|
2207
|
+
return null;
|
|
2208
|
+
const out = output ?? "";
|
|
2209
|
+
// Conta le righe NON vuote del corpo, ignorando le note tra parentesi quadre
|
|
2210
|
+
// che i tool aggiungono in coda (es. "[righe 10-40 di 200]", paginazione).
|
|
2211
|
+
const lineWord = (n) => n === 1 ? tr("1 riga", "1 line") : tr("{count} righe", "{count} lines", { count: n });
|
|
2212
|
+
switch (call.name) {
|
|
2213
|
+
case "read_file": {
|
|
2214
|
+
if (out.startsWith("errore"))
|
|
2215
|
+
return null;
|
|
2216
|
+
// Path letto in TESTA (e' l'informazione chiave: "cosa ho letto"); il
|
|
2217
|
+
// conteggio righe segue dopo il separatore.
|
|
2218
|
+
const path = String(call.input["path"] ?? "").trim();
|
|
2219
|
+
const head = path ? `${path} · ` : "";
|
|
2220
|
+
// Range esplicito aggiunto dal tool per le letture parziali.
|
|
2221
|
+
const range = /\[righe (\d+)-(\d+) di (\d+)\]/.exec(out);
|
|
2222
|
+
if (range) {
|
|
2223
|
+
const from = Number(range[1]);
|
|
2224
|
+
const to = Number(range[2]);
|
|
2225
|
+
const tot = Number(range[3]);
|
|
2226
|
+
return `${head}${lineWord(to - from + 1)} ` + tr("(righe {from}-{to} di {total})", "(lines {from}-{to} of {total})", { from, to, total: tot });
|
|
2227
|
+
}
|
|
2228
|
+
// Lettura intera (eventualmente paginata): conta le righe del corpo,
|
|
2229
|
+
// escludendo l'eventuale nota di paginazione finale.
|
|
2230
|
+
const body = out.replace(/\n\[file [^\]]*lungo: [^\]]*\]\s*$/s, "");
|
|
2231
|
+
const trimmed = body.replace(/\s+$/s, "");
|
|
2232
|
+
const lines = trimmed.length === 0 ? 0 : trimmed.split("\n").length;
|
|
2233
|
+
const paged = out !== body ? tr(" (parziale)", " (partial)") : "";
|
|
2234
|
+
if (lines === 0)
|
|
2235
|
+
return head + tr("file vuoto", "empty file") + paged;
|
|
2236
|
+
return `${head}${lineWord(lines)}${paged}`;
|
|
2237
|
+
}
|
|
2238
|
+
case "list_dir": {
|
|
2239
|
+
if (out.startsWith("errore"))
|
|
2240
|
+
return null;
|
|
2241
|
+
if (out === "(vuota)")
|
|
2242
|
+
return tr("cartella vuota", "empty folder");
|
|
2243
|
+
const items = out.split("\n").filter(Boolean);
|
|
2244
|
+
const dirs = items.filter((i) => i.endsWith("/")).length;
|
|
2245
|
+
const files = items.length - dirs;
|
|
2246
|
+
return tr("{count} voci ({dirs} cartelle, {files} file)", "{count} entries ({dirs} folders, {files} files)", { count: items.length, dirs, files });
|
|
2247
|
+
}
|
|
2248
|
+
// glob ("N file:") e grep ("N righe:") hanno gia' una prima riga sintetica:
|
|
2249
|
+
// si lascia firstLine (return null).
|
|
2250
|
+
default:
|
|
2251
|
+
return null;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
// Registro degli output collassabili per-canale (un canale = un comando come
|
|
2255
|
+
// "tree", "sessions", ...). Tiene l'id del fold dell'ultimo output mostrato per
|
|
2256
|
+
// quel canale, cosi':
|
|
2257
|
+
// - una nuova invocazione RIMUOVE il vecchio output (niente duplicati in chat);
|
|
2258
|
+
// - "<cmd> close" lo rimuove DAVVERO (cleanup reale, non resta in chat);
|
|
2259
|
+
// - "<cmd> open"/"<cmd> toggle" lo ri-mostrano/espandono.
|
|
2260
|
+
// E' deliberatamente semplice: si appoggia a ctl.fold/expandFold/removeFold del
|
|
2261
|
+
// modello "fold" gia' presente in master (no API divergenti).
|
|
2262
|
+
const collapsibleChannels = new Map();
|
|
2263
|
+
// Mostra `body` sotto una riga `summary` e lo collassa in un fold navigabile.
|
|
2264
|
+
// Se per quel `channel` c'era gia' un output, lo rimuove prima (no accumulo).
|
|
2265
|
+
// Ritorna l'id del fold creato.
|
|
2266
|
+
function showCollapsible(ctl, channel, summary, body) {
|
|
2267
|
+
ctl.write(summary + "\n" + body + "\n");
|
|
2268
|
+
return -1;
|
|
2269
|
+
}
|
|
2270
|
+
// Apre/chiude/rimuove l'output collassabile di un canale. Ritorna true se c'era
|
|
2271
|
+
// qualcosa su cui agire.
|
|
2272
|
+
function controlCollapsible(ctl, channel, action) {
|
|
2273
|
+
const cur = collapsibleChannels.get(channel);
|
|
2274
|
+
if (!cur)
|
|
2275
|
+
return false;
|
|
2276
|
+
if (action === "close") {
|
|
2277
|
+
// Chiusura = il blocco sparisce dalla chat (cleanup reale).
|
|
2278
|
+
ctl.removeFold(cur.foldId);
|
|
2279
|
+
collapsibleChannels.delete(channel);
|
|
2280
|
+
return true;
|
|
2281
|
+
}
|
|
2282
|
+
// open/toggle: espande il fold (mostra il corpo). toggle ri-collassa se aperto.
|
|
2283
|
+
if (action === "toggle")
|
|
2284
|
+
return ctl.expandFold(cur.foldId) || true;
|
|
2285
|
+
return ctl.expandFold(cur.foldId) || true;
|
|
2286
|
+
}
|
|
2287
|
+
// Ritorna: "quit" per uscire, input da inviare al modello, o null se il
|
|
2288
|
+
// comando e' stato gestito internamente.
|
|
2289
|
+
async function handleSlash(line, deps, ctl, theme, terminalColumns, themeCtl, templates, vascend, stats, signal, resetSessionUi) {
|
|
2290
|
+
return withCommandFeedback(ctl, commandCtl => handleSlashCommand(line, deps, commandCtl, theme, terminalColumns, themeCtl, templates, vascend, stats, signal, resetSessionUi), lines => showReadonlyOverlay({ title: line.split(/\s+/)[0], lines, ctl, theme, terminalColumns }));
|
|
2291
|
+
}
|
|
2292
|
+
async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, themeCtl, templates, vascend, stats, signal, resetSessionUi) {
|
|
2293
|
+
const [cmd, ...rest] = line.slice(1).split(/\s+/);
|
|
2294
|
+
const args = rest.join(" ");
|
|
2295
|
+
const { harness } = deps;
|
|
2296
|
+
switch (cmd) {
|
|
2297
|
+
case "review":
|
|
2298
|
+
case "revisione": {
|
|
2299
|
+
if (ctl.showReview)
|
|
2300
|
+
ctl.showReview(args);
|
|
2301
|
+
else if (ctl.review)
|
|
2302
|
+
ctl.write(reviewText(ctl.review, {
|
|
2303
|
+
cwd: harness.config.cwd, model: harness.config.model,
|
|
2304
|
+
permissions: harness.config.permMode, sessionId: harness.session.id,
|
|
2305
|
+
}, theme, terminalColumns(), args));
|
|
2306
|
+
return null;
|
|
2307
|
+
}
|
|
2308
|
+
case "copy": {
|
|
2309
|
+
const text = line.replace(/^\/copy(?:\s+|$)/, "") || lastAssistantText(harness.session.history());
|
|
2310
|
+
if (!text) {
|
|
2311
|
+
ctl.write(theme.dim("\n Nessuna risposta da copiare. Usa /copy una frase.\n"));
|
|
2312
|
+
return null;
|
|
2313
|
+
}
|
|
2314
|
+
const copied = await copyText(text);
|
|
2315
|
+
ctl.write(copied
|
|
2316
|
+
? theme.green("\n Testo copiato negli appunti.\n")
|
|
2317
|
+
: theme.yellow("\n Appunti non disponibili. Seleziona con Shift+trascina e copia dal terminale (Ctrl+Shift+C o Cmd+C).\n"));
|
|
2318
|
+
return null;
|
|
2319
|
+
}
|
|
2320
|
+
case "rename": {
|
|
2321
|
+
const title = args.trim();
|
|
2322
|
+
if (!title || title.length > 120) {
|
|
2323
|
+
ctl.write(theme.dim("\n Usa /rename Titolo della chat (da 1 a 120 caratteri).\n"));
|
|
2324
|
+
return null;
|
|
2325
|
+
}
|
|
2326
|
+
harness.session.setName(title);
|
|
2327
|
+
ctl.write(theme.green(`\n Nome della chat: ${title}\n`));
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
2330
|
+
case "help":
|
|
2331
|
+
case "commands":
|
|
2332
|
+
case "cmd": {
|
|
2333
|
+
// Palette di TUTTI i comandi (builtin + estensioni + template + skill),
|
|
2334
|
+
// NAVIGABILE e raggruppata per CATEGORIA: lista scorrevole con frecce;
|
|
2335
|
+
// Enter su un comando eseguibile lo lancia, Esc chiude. Raggiungibile
|
|
2336
|
+
// come /commands o digitando solo "/". Categoria e sintassi
|
|
2337
|
+
// compaiono soltanto per la voce a fuoco e restano ricercabili.
|
|
2338
|
+
const grouped = commandsByCategory();
|
|
2339
|
+
const builtinItems = grouped.flatMap((g) => g.commands.map((c) => ({
|
|
2340
|
+
key: c.name,
|
|
2341
|
+
label: `/${c.name}`,
|
|
2342
|
+
detail: c.desc,
|
|
2343
|
+
group: g.label,
|
|
2344
|
+
hint: c.hint,
|
|
2345
|
+
value: c.name,
|
|
2346
|
+
})));
|
|
2347
|
+
const extraItems = [
|
|
2348
|
+
...[...harness.host.commands.values()].map((c) => ({
|
|
2349
|
+
key: c.name,
|
|
2350
|
+
label: `/${c.name}`,
|
|
2351
|
+
detail: c.description,
|
|
2352
|
+
group: tr("estensione", "extension"),
|
|
2353
|
+
value: c.name,
|
|
2354
|
+
})),
|
|
2355
|
+
...templates.map((t) => ({
|
|
2356
|
+
key: t.name,
|
|
2357
|
+
label: `/${t.name}`,
|
|
2358
|
+
detail: t.description,
|
|
2359
|
+
group: "template",
|
|
2360
|
+
hint: t.argumentHint,
|
|
2361
|
+
value: t.name,
|
|
2362
|
+
})),
|
|
2363
|
+
...harness.skills.map((s) => ({
|
|
2364
|
+
key: `skill:${s.name}`,
|
|
2365
|
+
label: `/skill:${s.name}`,
|
|
2366
|
+
detail: s.description,
|
|
2367
|
+
group: "skill",
|
|
2368
|
+
value: `skill:${s.name}`,
|
|
2369
|
+
})),
|
|
2370
|
+
];
|
|
2371
|
+
const items = [...builtinItems, ...extraItems];
|
|
2372
|
+
// Fallback non interattivo: stampa la lista raggruppata per categoria.
|
|
2373
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
2374
|
+
ctl.write(theme.bold("\n comandi\n"));
|
|
2375
|
+
for (const g of grouped) {
|
|
2376
|
+
ctl.write(theme.accent(`\n ${g.label}\n`));
|
|
2377
|
+
for (const c of g.commands) {
|
|
2378
|
+
ctl.write(theme.dim(` /${c.name}${c.hint ? ` ${c.hint}` : ""}`) + theme.dark(` ${c.desc}`) + "\n");
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
if (extraItems.length) {
|
|
2382
|
+
ctl.write(theme.accent("\n estensioni, template e skill\n"));
|
|
2383
|
+
for (const it of extraItems)
|
|
2384
|
+
ctl.write(theme.dim(` ${it.label}${it.hint ? ` ${it.hint}` : ""}`) + theme.dark(` ${it.detail}`) + "\n");
|
|
2385
|
+
}
|
|
2386
|
+
ctl.write("\n");
|
|
2387
|
+
return null;
|
|
2388
|
+
}
|
|
2389
|
+
const selected = await selectOption({
|
|
2390
|
+
title: tr("Comandi", "Commands"),
|
|
2391
|
+
options: items,
|
|
2392
|
+
filterable: true,
|
|
2393
|
+
fullHeight: true,
|
|
2394
|
+
wide: true,
|
|
2395
|
+
selectedDetailsOnly: true,
|
|
2396
|
+
mouse: true,
|
|
2397
|
+
ctl,
|
|
2398
|
+
theme,
|
|
2399
|
+
terminalColumns,
|
|
2400
|
+
});
|
|
2401
|
+
if (!selected || ["help", "commands"].includes(selected.value))
|
|
2402
|
+
return null;
|
|
2403
|
+
// Esegue/riapre il comando scelto come se digitato.
|
|
2404
|
+
return handleSlash(`/${selected.value}`, deps, ctl, theme, terminalColumns, themeCtl, templates, vascend, stats, signal, resetSessionUi);
|
|
2405
|
+
}
|
|
2406
|
+
case "logout": {
|
|
2407
|
+
// Esce dall'account Vascend: revoca best-effort sul backend, poi rimuove
|
|
2408
|
+
// il token locale. Al prossimo avvio il cancello chiedera' l'accesso.
|
|
2409
|
+
const token = tokenVascend(harness.env);
|
|
2410
|
+
if (!token) {
|
|
2411
|
+
ctl.write(theme.dim("\n nessun accesso Vascend attivo\n"));
|
|
2412
|
+
return null;
|
|
2413
|
+
}
|
|
2414
|
+
await revocaSessione(harness.env, token);
|
|
2415
|
+
rimuoviTokenVascend(harness.env);
|
|
2416
|
+
// Via anche la prova offline: un logout che la lasciasse in giro
|
|
2417
|
+
// regalerebbe una finestra offline a chi e' appena uscito.
|
|
2418
|
+
rimuoviLicenzaLocale(harness.env);
|
|
2419
|
+
ctl.write(theme.green("\n uscito dall'account Vascend: al prossimo avvio verra' chiesto l'accesso\n"));
|
|
2420
|
+
return null;
|
|
2421
|
+
}
|
|
2422
|
+
case "quit":
|
|
2423
|
+
case "exit":
|
|
2424
|
+
return "quit";
|
|
2425
|
+
case "cwd":
|
|
2426
|
+
case "cd": {
|
|
2427
|
+
// Un cambio parziale di cwd contaminerebbe trust, system prompt, skill,
|
|
2428
|
+
// estensioni, template, watcher e sessionDir del progetto precedente.
|
|
2429
|
+
// Finche' non esiste un rebootstrap transazionale, il comando mostra il
|
|
2430
|
+
// riavvio sicuro invece di fingere un cambio live incompleto.
|
|
2431
|
+
const showState = async (verb) => {
|
|
2432
|
+
const root = await repoRoot(harness.config.cwd);
|
|
2433
|
+
const branch = await currentBranch(harness.config.cwd);
|
|
2434
|
+
const th = makeTheme(harness.config.ui, themeCtl.spec());
|
|
2435
|
+
ctl.write(th.green(`\n ${verb}: ${harness.config.cwd}\n`) +
|
|
2436
|
+
theme.dim(` repo: ${root ?? "(nessun repo git)"}${branch ? ` · ramo ${branch}` : ""}\n`));
|
|
2437
|
+
};
|
|
2438
|
+
if (!args.trim()) {
|
|
2439
|
+
await showState("progetto seguito");
|
|
2440
|
+
ctl.write(theme.dim(" usa /cwd <path> per seguire un altro progetto\n"));
|
|
2441
|
+
return null;
|
|
2442
|
+
}
|
|
2443
|
+
const target = isAbsolute(args) ? args : resolve(harness.config.cwd, args);
|
|
2444
|
+
if (!existsSync(target)) {
|
|
2445
|
+
ctl.write(theme.yellow(`\n path non valido: ${target}\n`));
|
|
2446
|
+
return null;
|
|
2447
|
+
}
|
|
2448
|
+
ctl.write(theme.yellow("\n cambio workspace live non applicato: richiede un rebootstrap sicuro.\n") +
|
|
2449
|
+
theme.dim(` riapri con: talos -C ${JSON.stringify(resolve(target))} --continue\n`) +
|
|
2450
|
+
theme.dim(" cosi' trust, AGENTS.md, skill, estensioni, sessioni e Context Fabric vengono ricaricati insieme.\n"));
|
|
2451
|
+
return null;
|
|
2452
|
+
}
|
|
2453
|
+
case "model": {
|
|
2454
|
+
if (!args) {
|
|
2455
|
+
await pickModel(harness, ctl, theme, terminalColumns);
|
|
2456
|
+
return null;
|
|
2457
|
+
}
|
|
2458
|
+
await applyModelChoice(args, harness, ctl, theme, terminalColumns);
|
|
2459
|
+
return null;
|
|
2460
|
+
}
|
|
2461
|
+
case "models": {
|
|
2462
|
+
await pickModel(harness, ctl, theme, terminalColumns);
|
|
2463
|
+
return null;
|
|
2464
|
+
}
|
|
2465
|
+
case "theme":
|
|
2466
|
+
case "themes": {
|
|
2467
|
+
if (args) {
|
|
2468
|
+
if (themeCtl.apply(args)) {
|
|
2469
|
+
ctl.write(makeTheme(deps.harness.config.ui, themeCtl.spec()).green(`\n tema impostato: ${args}\n`));
|
|
2470
|
+
}
|
|
2471
|
+
else {
|
|
2472
|
+
ctl.write(theme.yellow(`\n tema sconosciuto: ${args}`) +
|
|
2473
|
+
theme.dim(` disponibili: ${themeCtl.available.map((t) => t.name).join(", ")}\n`));
|
|
2474
|
+
}
|
|
2475
|
+
return null;
|
|
2476
|
+
}
|
|
2477
|
+
const options = themeCtl.available.map((t) => ({
|
|
2478
|
+
key: t.name,
|
|
2479
|
+
label: t.name,
|
|
2480
|
+
detail: t.mode === "light" ? "chiaro" : "scuro",
|
|
2481
|
+
value: t.name,
|
|
2482
|
+
}));
|
|
2483
|
+
const selected = await selectOption({
|
|
2484
|
+
title: "Temi",
|
|
2485
|
+
options,
|
|
2486
|
+
currentKey: themeCtl.activeName(),
|
|
2487
|
+
ctl,
|
|
2488
|
+
theme,
|
|
2489
|
+
terminalColumns,
|
|
2490
|
+
});
|
|
2491
|
+
if (selected && themeCtl.apply(selected.value)) {
|
|
2492
|
+
ctl.write(makeTheme(deps.harness.config.ui, themeCtl.spec()).green(`\n tema impostato: ${selected.value}\n`));
|
|
2493
|
+
}
|
|
2494
|
+
return null;
|
|
2495
|
+
}
|
|
2496
|
+
case "login": {
|
|
2497
|
+
stats.limits?.clear();
|
|
2498
|
+
await loginMenu(harness, ctl, theme, terminalColumns);
|
|
2499
|
+
return null;
|
|
2500
|
+
}
|
|
2501
|
+
case "usage":
|
|
2502
|
+
case "tokens": {
|
|
2503
|
+
const snapshot = stats.cost.snapshot(harness.config.model);
|
|
2504
|
+
const last = stats.last;
|
|
2505
|
+
const context = (stats.context ?? new ContextMeter()).read(harness);
|
|
2506
|
+
const lines = [theme.bold("\n Consumi della sessione") + theme.dim(` · ${harness.session.name || harness.session.id}`)];
|
|
2507
|
+
lines.push(theme.dim(` ${SESSION_USAGE_EXPLANATION}`));
|
|
2508
|
+
if (stats.incomplete)
|
|
2509
|
+
lines.push(theme.yellow(" Storico parziale: le chiamate precedenti alla registrazione non sono ricostruibili."));
|
|
2510
|
+
if (stats.missing)
|
|
2511
|
+
lines.push(theme.yellow(` ${stats.missing} chiamate senza conteggio token restituito dal provider.`));
|
|
2512
|
+
if (stats.unsaved)
|
|
2513
|
+
lines.push(theme.yellow(" Alcuni consumi non sono stati salvati su disco."));
|
|
2514
|
+
if (snapshot.totalTurns === 0) {
|
|
2515
|
+
lines.push(theme.dim(" Nessun conteggio token ricevuto dal provider per questa sessione."));
|
|
2516
|
+
}
|
|
2517
|
+
else {
|
|
2518
|
+
lines.push(theme.silver(` chiamate modello: ${formatTokenCount(snapshot.totalTurns)}`));
|
|
2519
|
+
lines.push(theme.silver(` input totale: ${formatTokenCount(snapshot.totalPromptTokens)}`));
|
|
2520
|
+
lines.push(theme.dim(` nuovo: ${formatTokenCount(snapshot.totalInputTokens)}`));
|
|
2521
|
+
lines.push(theme.dim(` cache letta: ${formatTokenCount(snapshot.totalCacheReadTokens)}`));
|
|
2522
|
+
lines.push(theme.dim(` cache scritta: ${formatTokenCount(snapshot.totalCacheWriteTokens)}`));
|
|
2523
|
+
lines.push(theme.silver(` output: ${formatTokenCount(snapshot.totalOutputTokens)}`));
|
|
2524
|
+
lines.push(theme.silver(` totale provider: ${formatTokenCount(snapshot.totalProviderTokens)}`));
|
|
2525
|
+
}
|
|
2526
|
+
if (last) {
|
|
2527
|
+
lines.push(theme.bold("\n ultima chiamata"));
|
|
2528
|
+
lines.push(theme.dim(` input ${formatTokenCount(promptTokens(last))} = nuovo ${formatTokenCount(last.inputTokens)} + cache letta ${formatTokenCount(last.cacheReadTokens ?? 0)} + cache scritta ${formatTokenCount(last.cacheWriteTokens ?? 0)}`));
|
|
2529
|
+
lines.push(theme.dim(` output ${formatTokenCount(last.outputTokens)}`));
|
|
2530
|
+
}
|
|
2531
|
+
if (snapshot.perModel.length) {
|
|
2532
|
+
lines.push(theme.bold("\n Per modello (collaboratori inclusi quando collegati alla chat)"));
|
|
2533
|
+
for (const row of snapshot.perModel)
|
|
2534
|
+
lines.push(theme.dim(` ${row.modelId}: ${formatTokenCount(promptTokens(row) + row.outputTokens)} token · ${row.turns} chiamate`));
|
|
2535
|
+
}
|
|
2536
|
+
const providerId = harness.providers.resolve(harness.config.model)?.provider.id ?? "";
|
|
2537
|
+
lines.push(...subscriptionDetails(providerId, stats.limits?.get(providerId), theme));
|
|
2538
|
+
lines.push(theme.bold("\n contesto vivo stimato") + theme.dim(" · non e' consumo API"));
|
|
2539
|
+
lines.push(...contextDetails(context).map(line => theme.dim(` ${line}`)));
|
|
2540
|
+
ctl.write(lines.join("\n") + "\n");
|
|
2541
|
+
return null;
|
|
2542
|
+
}
|
|
2543
|
+
case "provider":
|
|
2544
|
+
case "providers": {
|
|
2545
|
+
await providerMenu(harness, ctl, theme, terminalColumns);
|
|
2546
|
+
return null;
|
|
2547
|
+
}
|
|
2548
|
+
case "arch":
|
|
2549
|
+
case "architettura": {
|
|
2550
|
+
// Mappa architetturale del progetto seguito: stesso motore del tool
|
|
2551
|
+
// architecture_map (moduli, dipendenze interne, cicli, entry point,
|
|
2552
|
+
// hotspot). Output collassabile come /config.
|
|
2553
|
+
const sub = args.trim();
|
|
2554
|
+
if (sub === "close" || sub === "open" || sub === "toggle") {
|
|
2555
|
+
if (!controlCollapsible(ctl, "arch", sub) && sub === "close") {
|
|
2556
|
+
ctl.write(theme.dim("\n nessuna mappa da chiudere: usa /arch\n\n"));
|
|
2557
|
+
}
|
|
2558
|
+
else if (sub === "close") {
|
|
2559
|
+
ctl.write(theme.dim("\n mappa chiusa (output rimosso dalla chat)\n\n"));
|
|
2560
|
+
}
|
|
2561
|
+
return null;
|
|
2562
|
+
}
|
|
2563
|
+
const depthArg = Number(sub);
|
|
2564
|
+
const depth = Number.isFinite(depthArg) && depthArg >= 1 ? Math.floor(depthArg) : undefined;
|
|
2565
|
+
try {
|
|
2566
|
+
const map = scanArchitectureMap(harness.config.cwd, depth !== undefined ? { moduleDepth: depth } : {});
|
|
2567
|
+
if (!map.files) {
|
|
2568
|
+
ctl.write(theme.yellow("\n nessun file sorgente riconosciuto nel progetto seguito\n"));
|
|
2569
|
+
return null;
|
|
2570
|
+
}
|
|
2571
|
+
const body = renderArchitectureMap(map)
|
|
2572
|
+
.split("\n")
|
|
2573
|
+
.map((row) => (row.startsWith("#") ? theme.bold(` ${row.replace(/^#+\s*/, "")}`) : theme.dim(` ${row}`)))
|
|
2574
|
+
.join("\n");
|
|
2575
|
+
showCollapsible(ctl, "arch", theme.bold("\n architettura") + theme.dim(` · ${map.files} file, ${map.modules.length} moduli, ${map.cycles.length} cicli · /arch close per nascondere`), body);
|
|
2576
|
+
}
|
|
2577
|
+
catch (err) {
|
|
2578
|
+
ctl.write(theme.yellow(`\n errore analisi architettura: ${err.message}\n`));
|
|
2579
|
+
}
|
|
2580
|
+
return null;
|
|
2581
|
+
}
|
|
2582
|
+
case "config": {
|
|
2583
|
+
// In TTY /config e' un centro impostazioni a tutta altezza. La vista
|
|
2584
|
+
// diagnostica testuale resta disponibile con /config details.
|
|
2585
|
+
const csub = args.trim();
|
|
2586
|
+
if (csub === "close" || csub === "open" || csub === "toggle") {
|
|
2587
|
+
if (!controlCollapsible(ctl, "config", csub) && csub === "close") {
|
|
2588
|
+
ctl.write(theme.dim("\n nessuna config da chiudere: usa /config\n\n"));
|
|
2589
|
+
}
|
|
2590
|
+
else if (csub === "close") {
|
|
2591
|
+
ctl.write(theme.dim("\n config chiusa (output rimosso dalla chat)\n\n"));
|
|
2592
|
+
}
|
|
2593
|
+
return null;
|
|
2594
|
+
}
|
|
2595
|
+
if (!csub && stdin.isTTY && stdout.isTTY) {
|
|
2596
|
+
const cfg = harness.config;
|
|
2597
|
+
const resolved = harness.providers.resolve(cfg.model);
|
|
2598
|
+
const provider = resolved
|
|
2599
|
+
? (resolved.provider.label ?? resolved.provider.id)
|
|
2600
|
+
: "non configurato";
|
|
2601
|
+
const settings = [
|
|
2602
|
+
{ key: "model", label: "Modello", detail: cfg.model, value: "model" },
|
|
2603
|
+
{ key: "provider", label: "Provider e accessi", detail: provider, value: "provider" },
|
|
2604
|
+
{ key: "vibe", label: "Profilo operativo", detail: cfg.vibeMode, value: "vibe" },
|
|
2605
|
+
{ key: "theme", label: "Tema", detail: themeCtl.activeName(), value: "theme" },
|
|
2606
|
+
{ key: "cwd", label: "Workspace", detail: cfg.cwd, value: "cwd" },
|
|
2607
|
+
{ key: "sessions", label: "Sessioni", detail: harness.session.id, value: "sessions" },
|
|
2608
|
+
{ key: "extensions", label: "Estensioni", detail: `${harness.host.inventory().filter((item) => item.loaded).length} caricate`, value: "extensions" },
|
|
2609
|
+
{ key: "usage", label: "Utilizzo e contesto", detail: "token provider, cache e finestra viva", value: "usage" },
|
|
2610
|
+
];
|
|
2611
|
+
const selected = await selectOption({
|
|
2612
|
+
title: "Impostazioni",
|
|
2613
|
+
hint: "/config details mostra i file di configurazione",
|
|
2614
|
+
options: settings,
|
|
2615
|
+
fullHeight: true,
|
|
2616
|
+
wide: true,
|
|
2617
|
+
ctl,
|
|
2618
|
+
theme,
|
|
2619
|
+
terminalColumns,
|
|
2620
|
+
});
|
|
2621
|
+
if (!selected)
|
|
2622
|
+
return null;
|
|
2623
|
+
return handleSlash(`/${selected.value}`, deps, ctl, theme, terminalColumns, themeCtl, templates, vascend, stats, signal, resetSessionUi);
|
|
2624
|
+
}
|
|
2625
|
+
const cfg = harness.config;
|
|
2626
|
+
const files = cfg.loadedConfigFiles.length
|
|
2627
|
+
? cfg.loadedConfigFiles.map((file) => ` ${file}`).join("\n")
|
|
2628
|
+
: " nessun file caricato";
|
|
2629
|
+
const body = theme.dim(` modello: ${cfg.model}\n`) +
|
|
2630
|
+
theme.dim(` maxTokens: ${cfg.maxTokens > 0 ? cfg.maxTokens : "auto (max del modello)"}\n`) +
|
|
2631
|
+
theme.dim(` vibeMode: ${cfg.vibeMode}\n`) +
|
|
2632
|
+
theme.dim(` sessionDir: ${cfg.sessionDir}\n`) +
|
|
2633
|
+
theme.dim(` contextFiles: ${cfg.contextFiles.join(", ")}\n`) +
|
|
2634
|
+
theme.dim(` skill: ${harness.skills.length}${harness.skillCommandsEnabled ? "" : " (comandi off)"}${harness.skills.length ? ` (${harness.skills.map((s) => s.name).join(", ")})` : ""}\n`) +
|
|
2635
|
+
theme.dim(` progetto fidato: ${harness.projectTrusted ? "si" : "no (risorse locali non caricate)"}\n`) +
|
|
2636
|
+
theme.bold("\n tema\n") +
|
|
2637
|
+
theme.dim(` attivo: ${themeCtl.activeName()}${themeCtl.spec() ? ` (${themeCtl.spec()?.mode === "light" ? "chiaro" : "scuro"})` : ""}\n`) +
|
|
2638
|
+
theme.dim(` disponibili: ${themeCtl.available.map((t) => t.name).join(", ")}\n`) +
|
|
2639
|
+
theme.dim(` banner: ${cfg.ui.banner}\n`) +
|
|
2640
|
+
theme.dim(` prompt: ${JSON.stringify(cfg.ui.prompt)}\n`) +
|
|
2641
|
+
theme.bold("\n file\n") +
|
|
2642
|
+
theme.dim(`${files}`);
|
|
2643
|
+
showCollapsible(ctl, "config", theme.bold("\n configurazione") + theme.dim(" · /config close per nascondere"), body);
|
|
2644
|
+
return null;
|
|
2645
|
+
}
|
|
2646
|
+
case "vibe":
|
|
2647
|
+
case "profile": {
|
|
2648
|
+
const setMode = (value) => {
|
|
2649
|
+
const parsed = parseVibeMode(value);
|
|
2650
|
+
if (!parsed)
|
|
2651
|
+
return false;
|
|
2652
|
+
harness.config = { ...harness.config, vibeMode: parsed };
|
|
2653
|
+
saveGlobalSettings(harness.env, { vibeMode: parsed });
|
|
2654
|
+
return true;
|
|
2655
|
+
};
|
|
2656
|
+
const requested = args.trim().toLowerCase();
|
|
2657
|
+
if (requested) {
|
|
2658
|
+
if (!setMode(requested)) {
|
|
2659
|
+
ctl.write(theme.yellow(`\n modalita' sconosciuta: ${requested}\n`) +
|
|
2660
|
+
theme.dim(` disponibili: auto, ${listVibeModes().map((mode) => mode.id).join(", ")}\n`));
|
|
2661
|
+
return null;
|
|
2662
|
+
}
|
|
2663
|
+
const spec = requested === "auto" ? null : listVibeModes().find((mode) => mode.id === requested);
|
|
2664
|
+
ctl.write(theme.green(`\n vibe mode: ${requested}\n`) +
|
|
2665
|
+
theme.dim(` ${spec?.summary ?? "router automatico per ogni mission epoch"}\n`));
|
|
2666
|
+
ctl.refresh?.();
|
|
2667
|
+
return null;
|
|
2668
|
+
}
|
|
2669
|
+
const modes = listVibeModes();
|
|
2670
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
2671
|
+
ctl.write(theme.bold(`\n vibe mode: ${harness.config.vibeMode}\n`));
|
|
2672
|
+
for (const mode of modes)
|
|
2673
|
+
ctl.write(theme.dim(` ${mode.id.padEnd(12)} ${mode.summary}\n`));
|
|
2674
|
+
return null;
|
|
2675
|
+
}
|
|
2676
|
+
const selected = await selectOption({
|
|
2677
|
+
title: `vibecoding mode · attuale ${harness.config.vibeMode}`,
|
|
2678
|
+
hint: "La modalità cambia il modo di lavorare, i permessi richiesti e il contesto.",
|
|
2679
|
+
options: [
|
|
2680
|
+
{ key: "auto", label: "auto", detail: "router deterministico per missione/follow-up", value: "auto" },
|
|
2681
|
+
...modes.map((mode) => ({ key: mode.id, label: mode.label, detail: `${mode.id} · ${mode.summary}`, value: mode.id })),
|
|
2682
|
+
],
|
|
2683
|
+
currentKey: harness.config.vibeMode,
|
|
2684
|
+
ctl,
|
|
2685
|
+
theme,
|
|
2686
|
+
terminalColumns,
|
|
2687
|
+
filterable: true,
|
|
2688
|
+
});
|
|
2689
|
+
if (selected && setMode(selected.value)) {
|
|
2690
|
+
ctl.write(theme.green(`\n vibe mode: ${selected.value}\n`));
|
|
2691
|
+
ctl.refresh?.();
|
|
2692
|
+
}
|
|
2693
|
+
return null;
|
|
2694
|
+
}
|
|
2695
|
+
case "context":
|
|
2696
|
+
case "ctx": {
|
|
2697
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
2698
|
+
const sub = (parts[0] ?? "").toLowerCase();
|
|
2699
|
+
if (sub === "close" || sub === "open" || sub === "toggle") {
|
|
2700
|
+
if (!controlCollapsible(ctl, "context", sub) && sub === "close") {
|
|
2701
|
+
ctl.write(theme.dim("\n nessuna ricevuta context da chiudere: usa /context\n"));
|
|
2702
|
+
}
|
|
2703
|
+
return null;
|
|
2704
|
+
}
|
|
2705
|
+
if (sub === "reset") {
|
|
2706
|
+
resetContextFrame({
|
|
2707
|
+
sessionDir: harness.config.sessionDir,
|
|
2708
|
+
frameId: harness.session.id,
|
|
2709
|
+
cwd: harness.config.cwd,
|
|
2710
|
+
selection: harness.config.vibeMode,
|
|
2711
|
+
});
|
|
2712
|
+
ctl.write(theme.green("\n Context Fabric resettato: il prossimo turno apre un nuovo epoch.\n"));
|
|
2713
|
+
return null;
|
|
2714
|
+
}
|
|
2715
|
+
if (sub === "pin" || sub === "unpin") {
|
|
2716
|
+
const value = parts.slice(1).join(" ");
|
|
2717
|
+
if (!value) {
|
|
2718
|
+
ctl.write(theme.yellow(`\n uso: /context ${sub} <termine o path>\n`));
|
|
2719
|
+
return null;
|
|
2720
|
+
}
|
|
2721
|
+
const updated = updateContextPins({
|
|
2722
|
+
sessionDir: harness.config.sessionDir,
|
|
2723
|
+
frameId: harness.session.id,
|
|
2724
|
+
cwd: harness.config.cwd,
|
|
2725
|
+
...(sub === "pin" ? { add: [value] } : { remove: [value] }),
|
|
2726
|
+
});
|
|
2727
|
+
ctl.write(updated
|
|
2728
|
+
? theme.green(`\n context ${sub === "pin" ? "pin" : "unpin"}: ${value}\n`)
|
|
2729
|
+
: theme.yellow("\n nessun epoch attivo: invia prima una richiesta.\n"));
|
|
2730
|
+
return null;
|
|
2731
|
+
}
|
|
2732
|
+
if (sub && sub !== "explain" && sub !== "status") {
|
|
2733
|
+
ctl.write(theme.yellow("\n uso: /context [status|explain|pin <x>|unpin <x>|reset|close]\n"));
|
|
2734
|
+
return null;
|
|
2735
|
+
}
|
|
2736
|
+
const frame = loadContextFrame(harness.config.sessionDir, harness.session.id, contextProjectKey(harness.config.cwd), true);
|
|
2737
|
+
const body = [...contextDetails((stats.context ?? new ContextMeter()).read(harness)), "", ...renderContextFrame(frame).split("\n")]
|
|
2738
|
+
.map((line) => theme.dim(` ${line}`)).join("\n");
|
|
2739
|
+
showCollapsible(ctl, "context", theme.bold("\n Context Fabric") + theme.dim(" · /context close per nascondere"), body);
|
|
2740
|
+
return null;
|
|
2741
|
+
}
|
|
2742
|
+
case "vascend": {
|
|
2743
|
+
if (args.trim() === "off")
|
|
2744
|
+
vascend.resetPlan(harness.config.sessionDir, harness.session.id);
|
|
2745
|
+
const result = vascend.handleCommand(args);
|
|
2746
|
+
ctl.write(theme.green(`\n ${result.message}\n`));
|
|
2747
|
+
ctl.write(renderVascendStatus(vascend.state(), theme, terminalColumns()));
|
|
2748
|
+
return result.kind === "input" ? { input: result.input, vascend: true } : null;
|
|
2749
|
+
}
|
|
2750
|
+
case "queue":
|
|
2751
|
+
case "coda":
|
|
2752
|
+
if (ctl.showQueue)
|
|
2753
|
+
ctl.showQueue();
|
|
2754
|
+
else
|
|
2755
|
+
ctl.write(theme.dim("\n La gestione della coda richiede la TUI interattiva.\n"));
|
|
2756
|
+
return null;
|
|
2757
|
+
case "regno":
|
|
2758
|
+
case "castello":
|
|
2759
|
+
case "castelli":
|
|
2760
|
+
case "todo":
|
|
2761
|
+
case "tasks":
|
|
2762
|
+
case "attivita":
|
|
2763
|
+
case "piano": {
|
|
2764
|
+
// Rinfresca dal disco (l'agente potrebbe aver pianificato) e mostra il
|
|
2765
|
+
// lista: gruppi e attività navigabili. Gestione: svuota la lista.
|
|
2766
|
+
vascend.loadPlan(harness.config.sessionDir, harness.session.id);
|
|
2767
|
+
const castles = vascend.state().kingdom.castles;
|
|
2768
|
+
if (!castles.length) {
|
|
2769
|
+
ctl.write(theme.dim("\n La lista delle attività è vuota.\n") +
|
|
2770
|
+
theme.dim(" Talos creerà automaticamente le attività quando il compito lo richiede.\n\n"));
|
|
2771
|
+
return null;
|
|
2772
|
+
}
|
|
2773
|
+
const total = castles.flatMap((c) => c.tasks).length;
|
|
2774
|
+
const done = castles.flatMap((c) => c.tasks).filter((t) => t.status === "completed").length;
|
|
2775
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
2776
|
+
ctl.write(theme.bold(`\n Lista delle attività · ${castles.length} grupp${castles.length === 1 ? "o" : "i"} · ${done}/${total} completate\n`));
|
|
2777
|
+
for (const c of castles) {
|
|
2778
|
+
ctl.write(theme.silver(` ◆ ${c.title || c.slug} [${c.slug}]\n`));
|
|
2779
|
+
for (const t of c.tasks)
|
|
2780
|
+
ctl.write(theme.dim(` ${vascendRoomGlyph(t.status)} ${t.title} · ${TASK_STATUS_LABELS[t.status]}\n`));
|
|
2781
|
+
}
|
|
2782
|
+
ctl.write("\n");
|
|
2783
|
+
return null;
|
|
2784
|
+
}
|
|
2785
|
+
// Selettore a DUE livelli: scegli un gruppo, poi entri nelle sue attività.
|
|
2786
|
+
for (;;) {
|
|
2787
|
+
const fresh = vascend.state().kingdom.castles;
|
|
2788
|
+
if (!fresh.length)
|
|
2789
|
+
break;
|
|
2790
|
+
const tot = fresh.flatMap((c) => c.tasks).length;
|
|
2791
|
+
const dn = fresh.flatMap((c) => c.tasks).filter((t) => t.status === "completed").length;
|
|
2792
|
+
const castleOpts = fresh.map((c) => {
|
|
2793
|
+
const cd = c.tasks.filter((t) => t.status === "completed").length;
|
|
2794
|
+
const cf = c.tasks.filter((t) => t.status === "failed").length;
|
|
2795
|
+
return {
|
|
2796
|
+
key: `c:${c.slug}`,
|
|
2797
|
+
label: `◆ ${c.title || c.slug}`,
|
|
2798
|
+
detail: `${cd}/${c.tasks.length} completate${cf ? ` · ${cf} fallit${cf === 1 ? "a" : "e"}` : ""}`,
|
|
2799
|
+
value: `c:${c.slug}`,
|
|
2800
|
+
};
|
|
2801
|
+
});
|
|
2802
|
+
castleOpts.push({ key: "__clear__", label: "Svuota la lista", detail: "Rimuove tutti i gruppi e le attività", value: "__clear__" });
|
|
2803
|
+
const pick = await selectOption({
|
|
2804
|
+
title: `Lista delle attività — ${fresh.length} grupp${fresh.length === 1 ? "o" : "i"} · ${dn}/${tot} completate`,
|
|
2805
|
+
hint: "↑/↓ · Invio apre il gruppo · Esc chiudi",
|
|
2806
|
+
options: castleOpts,
|
|
2807
|
+
ctl, theme, terminalColumns,
|
|
2808
|
+
});
|
|
2809
|
+
if (!pick)
|
|
2810
|
+
break;
|
|
2811
|
+
if (pick.value === "__clear__") {
|
|
2812
|
+
vascend.resetPlan(harness.config.sessionDir, harness.session.id);
|
|
2813
|
+
ctl.write(theme.green("\n Lista delle attività svuotata.\n"));
|
|
2814
|
+
break;
|
|
2815
|
+
}
|
|
2816
|
+
const slug = pick.value.slice(2);
|
|
2817
|
+
const castle = fresh.find((c) => c.slug === slug);
|
|
2818
|
+
if (!castle)
|
|
2819
|
+
break;
|
|
2820
|
+
// Livello 2: attività del gruppo (sola lettura) + indietro.
|
|
2821
|
+
const roomOpts = castle.tasks.map((t) => {
|
|
2822
|
+
const extra = [
|
|
2823
|
+
t.tokens !== undefined ? `↓ ${fmtTok(t.tokens)}` : "",
|
|
2824
|
+
t.skills?.length ? `@skill:${t.skills.join(",")}` : "",
|
|
2825
|
+
t.notes?.length ? `${t.notes.length} note` : "",
|
|
2826
|
+
].filter(Boolean).join(" · ");
|
|
2827
|
+
return {
|
|
2828
|
+
key: t.id,
|
|
2829
|
+
label: `${vascendRoomGlyph(t.status)} ${t.title}`,
|
|
2830
|
+
detail: `${TASK_STATUS_LABELS[t.status]}${extra ? ` · ${extra}` : ""}`,
|
|
2831
|
+
value: t.id,
|
|
2832
|
+
};
|
|
2833
|
+
});
|
|
2834
|
+
roomOpts.push({ key: "__back__", label: "← indietro", detail: "Torna alla lista", value: "__back__" });
|
|
2835
|
+
await selectOption({
|
|
2836
|
+
title: `${castle.title || castle.slug} — ${castle.tasks.filter((t) => t.status === "completed").length}/${castle.tasks.length}`,
|
|
2837
|
+
hint: "↑/↓ attività · Esc/indietro torna alla lista",
|
|
2838
|
+
options: roomOpts,
|
|
2839
|
+
ctl, theme, terminalColumns,
|
|
2840
|
+
});
|
|
2841
|
+
// qualunque cosa scelga, si torna al livello gruppi (loop).
|
|
2842
|
+
}
|
|
2843
|
+
return null;
|
|
2844
|
+
}
|
|
2845
|
+
case "bg": {
|
|
2846
|
+
// /bg -> lista; /bg <id> -> output; /bg kill <id> -> termina.
|
|
2847
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
2848
|
+
if (!parts.length) {
|
|
2849
|
+
await showBackgroundPanel(ctl, theme, terminalColumns);
|
|
2850
|
+
return null;
|
|
2851
|
+
}
|
|
2852
|
+
if (parts[0] === "kill") {
|
|
2853
|
+
const id = parts[1] ?? "";
|
|
2854
|
+
ctl.write(backgroundManager.kill(id) ? theme.green(`\n terminato ${id}\n`) : theme.yellow(`\n ${id || "(id mancante)"} non in esecuzione\n`));
|
|
2855
|
+
return null;
|
|
2856
|
+
}
|
|
2857
|
+
const t = backgroundManager.get(parts[0] ?? "");
|
|
2858
|
+
if (!t) {
|
|
2859
|
+
ctl.write(theme.yellow(`\n task non trovato: ${parts[0]}\n`));
|
|
2860
|
+
return null;
|
|
2861
|
+
}
|
|
2862
|
+
const dur = t.endedAt ? `${Math.round((t.endedAt - t.startedAt) / 1000)}s` : `${Math.round((Date.now() - t.startedAt) / 1000)}s`;
|
|
2863
|
+
await showReadonlyOverlay({
|
|
2864
|
+
title: `${t.id} · ${t.status}${t.exitCode != null ? ` · uscita ${t.exitCode}` : ""} · ${dur}`,
|
|
2865
|
+
lines: [t.command, "", ...(t.output || "(nessun output)").replace(/\n+$/, "").split("\n")],
|
|
2866
|
+
ctl,
|
|
2867
|
+
theme,
|
|
2868
|
+
terminalColumns,
|
|
2869
|
+
});
|
|
2870
|
+
return null;
|
|
2871
|
+
}
|
|
2872
|
+
case "reload":
|
|
2873
|
+
case "reload-ext": {
|
|
2874
|
+
// Hot reload delle estensioni da .talos/extensions (parita' pi.dev):
|
|
2875
|
+
// scarica cio' che le estensioni avevano registrato e ricarica i file da
|
|
2876
|
+
// disco con cache-bust. Richiede project trust (come il caricamento iniziale).
|
|
2877
|
+
if (!harness.projectTrusted) {
|
|
2878
|
+
ctl.write(theme.yellow("\n reload estensioni: progetto non fidato (estensioni disabilitate)\n"));
|
|
2879
|
+
return null;
|
|
2880
|
+
}
|
|
2881
|
+
ctl.setStatus("ricarico estensioni...");
|
|
2882
|
+
try {
|
|
2883
|
+
const diff = await harness.host.reload();
|
|
2884
|
+
ctl.setStatus(null);
|
|
2885
|
+
const parts = [theme.green(`\n estensioni ricaricate · ${diff.after.length} tool`)];
|
|
2886
|
+
if (diff.added.length)
|
|
2887
|
+
parts.push(theme.dim(` +${diff.added.join(", ")}`));
|
|
2888
|
+
if (diff.removed.length)
|
|
2889
|
+
parts.push(theme.dim(` -${diff.removed.join(", ")}`));
|
|
2890
|
+
if (!diff.added.length && !diff.removed.length)
|
|
2891
|
+
parts.push(theme.dim(" (nessuna variazione di tool)"));
|
|
2892
|
+
ctl.write(parts.join("") + "\n");
|
|
2893
|
+
}
|
|
2894
|
+
catch (err) {
|
|
2895
|
+
ctl.setStatus(null);
|
|
2896
|
+
ctl.write(theme.red(`\n reload fallito: ${err.message}\n`));
|
|
2897
|
+
}
|
|
2898
|
+
return null;
|
|
2899
|
+
}
|
|
2900
|
+
case "extensions":
|
|
2901
|
+
case "ext": {
|
|
2902
|
+
// Lista e gestione estensioni: senza argomenti elenca; con "on/off <id>"
|
|
2903
|
+
// abilita/disabilita; con "set <id> <chiave> <valore>" imposta un'opzione.
|
|
2904
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
2905
|
+
const sub = (parts[0] ?? "").toLowerCase();
|
|
2906
|
+
if (sub === "on" || sub === "off" || sub === "enable" || sub === "disable") {
|
|
2907
|
+
const idArg = parts[1];
|
|
2908
|
+
if (!idArg) {
|
|
2909
|
+
ctl.write(theme.yellow("\n uso: /extensions on|off <id>\n"));
|
|
2910
|
+
return null;
|
|
2911
|
+
}
|
|
2912
|
+
const id = harness.host.resolveId(idArg);
|
|
2913
|
+
if (!id) {
|
|
2914
|
+
ctl.write(theme.red(`\n estensione '${idArg}' non trovata\n`));
|
|
2915
|
+
return null;
|
|
2916
|
+
}
|
|
2917
|
+
const enable = sub === "on" || sub === "enable";
|
|
2918
|
+
ctl.setStatus("aggiorno estensioni...");
|
|
2919
|
+
const r = await harness.host.setEnabled(id, enable);
|
|
2920
|
+
ctl.setStatus(null);
|
|
2921
|
+
ctl.write(r.ok
|
|
2922
|
+
? theme.green(`\n ${id} ${enable ? "abilitata" : "disabilitata"}\n`)
|
|
2923
|
+
: theme.red(`\n ${r.reason ?? "operazione fallita"}\n`));
|
|
2924
|
+
return null;
|
|
2925
|
+
}
|
|
2926
|
+
if (sub === "set") {
|
|
2927
|
+
const [, idArg, key, ...rest] = parts;
|
|
2928
|
+
if (!idArg || !key || !rest.length) {
|
|
2929
|
+
ctl.write(theme.yellow("\n uso: /extensions set <id> <chiave> <valore>\n"));
|
|
2930
|
+
return null;
|
|
2931
|
+
}
|
|
2932
|
+
const id = harness.host.resolveId(idArg);
|
|
2933
|
+
if (!id) {
|
|
2934
|
+
ctl.write(theme.red(`\n estensione '${idArg}' non trovata\n`));
|
|
2935
|
+
return null;
|
|
2936
|
+
}
|
|
2937
|
+
const rawVal = rest.join(" ");
|
|
2938
|
+
let value = rawVal;
|
|
2939
|
+
try {
|
|
2940
|
+
value = JSON.parse(rawVal);
|
|
2941
|
+
}
|
|
2942
|
+
catch {
|
|
2943
|
+
// non-JSON: tieni la stringa cosi' com'e'.
|
|
2944
|
+
}
|
|
2945
|
+
ctl.setStatus("aggiorno estensioni...");
|
|
2946
|
+
const r = await harness.host.setOption(id, key, value);
|
|
2947
|
+
ctl.setStatus(null);
|
|
2948
|
+
ctl.write(r.ok
|
|
2949
|
+
? theme.green(`\n ${id}: ${key} = ${JSON.stringify(value)}\n`)
|
|
2950
|
+
: theme.red(`\n ${r.reason ?? "operazione fallita"}\n`));
|
|
2951
|
+
return null;
|
|
2952
|
+
}
|
|
2953
|
+
// Default: elenca le estensioni.
|
|
2954
|
+
const inv = harness.host.inventory();
|
|
2955
|
+
if (!inv.length) {
|
|
2956
|
+
ctl.write(theme.dim("\n nessuna estensione in .talos/extensions\n"));
|
|
2957
|
+
return null;
|
|
2958
|
+
}
|
|
2959
|
+
const out = [theme.bold("\n Estensioni (.talos/extensions)")];
|
|
2960
|
+
for (const e of inv) {
|
|
2961
|
+
const badge = e.enabled
|
|
2962
|
+
? e.loaded
|
|
2963
|
+
? theme.green("on")
|
|
2964
|
+
: theme.yellow("on*")
|
|
2965
|
+
: theme.red("off");
|
|
2966
|
+
out.push(`\n ${badge} ${theme.bold(e.id)}`);
|
|
2967
|
+
const reg = [];
|
|
2968
|
+
if (e.tools.length)
|
|
2969
|
+
reg.push(`tool ${e.tools.join(",")}`);
|
|
2970
|
+
if (e.commands.length)
|
|
2971
|
+
reg.push(`cmd /${e.commands.join(" /")}`);
|
|
2972
|
+
if (e.shortcuts.length)
|
|
2973
|
+
reg.push(`key ${e.shortcuts.join(",")}`);
|
|
2974
|
+
if (e.events.length)
|
|
2975
|
+
reg.push(`ev ${e.events.join(",")}`);
|
|
2976
|
+
if (e.systemHooks)
|
|
2977
|
+
reg.push(`hook ${e.systemHooks}`);
|
|
2978
|
+
if (reg.length)
|
|
2979
|
+
out.push(theme.dim(` ${reg.join(" · ")}`));
|
|
2980
|
+
if (Object.keys(e.options).length)
|
|
2981
|
+
out.push(theme.dim(` set ${JSON.stringify(e.options)}`));
|
|
2982
|
+
}
|
|
2983
|
+
out.push(theme.dim("\n gestisci: /extensions on|off <id> · /extensions set <id> <chiave> <valore>\n"));
|
|
2984
|
+
ctl.write(out.join("\n") + "\n");
|
|
2985
|
+
return null;
|
|
2986
|
+
}
|
|
2987
|
+
case "expand": {
|
|
2988
|
+
if (args.trim().toLowerCase() === "all") {
|
|
2989
|
+
const n = ctl.expandAllFolds();
|
|
2990
|
+
ctl.write(n ? theme.dim(`\n espanse ${n} ondate di tool\n`) : theme.dim("\n niente da espandere\n"));
|
|
2991
|
+
}
|
|
2992
|
+
else {
|
|
2993
|
+
const ok = ctl.expandFold();
|
|
2994
|
+
ctl.write(ok ? theme.dim("\n ondata espansa\n") : theme.dim("\n niente da espandere\n"));
|
|
2995
|
+
}
|
|
2996
|
+
return null;
|
|
2997
|
+
}
|
|
2998
|
+
case "vascend-clear": {
|
|
2999
|
+
vascend.resetPlan(harness.config.sessionDir, harness.session.id);
|
|
3000
|
+
const result = vascend.handleCommand("clear");
|
|
3001
|
+
ctl.write(theme.green(`\n ${result.message}\n`));
|
|
3002
|
+
return null;
|
|
3003
|
+
}
|
|
3004
|
+
// Compact UNIFICATO: una sola operazione che (1) compatta il contesto vivo
|
|
3005
|
+
// con riassunto LLM (stile Pi) e (2) salva il checkpoint Vascend/Danilov in
|
|
3006
|
+
// .vascend-compact.md per la ripresa a freddo. /vascend-compact e' un alias.
|
|
3007
|
+
case "vascend-compact":
|
|
3008
|
+
case "compact": {
|
|
3009
|
+
const hasModel = Boolean(harness.providers.resolve(harness.config.model));
|
|
3010
|
+
// 1) Compattazione LLM del contesto della sessione.
|
|
3011
|
+
if (hasModel) {
|
|
3012
|
+
const meter = stats.context ?? new ContextMeter();
|
|
3013
|
+
const before = meter.read(harness).tokens;
|
|
3014
|
+
ctl.write(theme.dim(`\n compatto ~${formatTokenCount(before)} token attivi; preservo richiesta, attivita aperte e lavoro recente...\n`));
|
|
3015
|
+
const controller = new AbortController();
|
|
3016
|
+
const onSigint = () => controller.abort();
|
|
3017
|
+
process.once("SIGINT", onSigint);
|
|
3018
|
+
try {
|
|
3019
|
+
const done = await compactNow(harness.deps(async () => false), controller.signal, (e) => {
|
|
3020
|
+
if (e.type === "usage")
|
|
3021
|
+
stats.observe?.(e);
|
|
3022
|
+
}, args || undefined);
|
|
3023
|
+
if (done) {
|
|
3024
|
+
const after = meter.read(harness);
|
|
3025
|
+
const archived = after.archived.reduce((sum, block) => sum + block.tokens, 0);
|
|
3026
|
+
ctl.write(theme.green(` attivo ~${formatTokenCount(before)} → ~${formatTokenCount(after.tokens)} token · archivio originale ~${formatTokenCount(archived)}\n`));
|
|
3027
|
+
}
|
|
3028
|
+
if (!done)
|
|
3029
|
+
ctl.write(theme.dim(controller.signal.aborted
|
|
3030
|
+
? " compattazione interrotta; contesto conservato.\n"
|
|
3031
|
+
: " nessuna compattazione applicata; contesto conservato.\n"));
|
|
3032
|
+
}
|
|
3033
|
+
catch (err) {
|
|
3034
|
+
ctl.write(theme.red(` errore compattazione: ${err.message}\n`));
|
|
3035
|
+
}
|
|
3036
|
+
finally {
|
|
3037
|
+
process.removeListener("SIGINT", onSigint);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
else {
|
|
3041
|
+
ctl.write(theme.dim("\n nessun modello configurato: salvo solo il checkpoint Vascend.\n"));
|
|
3042
|
+
}
|
|
3043
|
+
// 2) Checkpoint Vascend/Danilov sullo stato (gia' compattato) della sessione.
|
|
3044
|
+
vascend.loadPlan(harness.config.sessionDir, harness.config.planStoreId ?? harness.config.roomStoreId ?? harness.session.id);
|
|
3045
|
+
const checkpoint = vascend.compact(harness.session.history());
|
|
3046
|
+
ctl.write(theme.green(" ✓ checkpoint Vascend salvato in .vascend-compact.md\n"));
|
|
3047
|
+
ctl.write(theme.dim(` piano e contesto pronti per la ripresa (${checkpoint.text.split("\n").length} righe)\n`));
|
|
3048
|
+
return null;
|
|
3049
|
+
}
|
|
3050
|
+
case "tree": {
|
|
3051
|
+
const sub = args.trim();
|
|
3052
|
+
// /tree close|open|toggle: controlla l'output collassabile dell'albero.
|
|
3053
|
+
// Alla chiusura il blocco SPARISCE dalla chat (cleanup reale), non resta
|
|
3054
|
+
// un segnaposto: e' cio' che l'utente si aspetta quando lo richiude.
|
|
3055
|
+
if (sub === "close" || sub === "open" || sub === "toggle") {
|
|
3056
|
+
if (!controlCollapsible(ctl, "tree", sub)) {
|
|
3057
|
+
ctl.write(theme.dim("\n nessun albero da " + (sub === "close" ? "chiudere" : "aprire") + ": usa /tree\n\n"));
|
|
3058
|
+
}
|
|
3059
|
+
else if (sub === "close") {
|
|
3060
|
+
ctl.write(theme.dim("\n albero chiuso (output rimosso dalla chat)\n\n"));
|
|
3061
|
+
}
|
|
3062
|
+
return null;
|
|
3063
|
+
}
|
|
3064
|
+
// /tree <id>: naviga al nodo, riassumendo il ramo abbandonato (branch
|
|
3065
|
+
// summarization, parita' Pi).
|
|
3066
|
+
if (sub) {
|
|
3067
|
+
const targetId = sub;
|
|
3068
|
+
const previousHead = harness.session.head;
|
|
3069
|
+
if (!harness.session.checkout(targetId)) {
|
|
3070
|
+
ctl.write(theme.yellow(`\n nodo non trovato: ${targetId}\n`));
|
|
3071
|
+
return null;
|
|
3072
|
+
}
|
|
3073
|
+
if (previousHead && previousHead !== targetId && harness.providers.resolve(harness.config.model)) {
|
|
3074
|
+
const ancestor = harness.session.commonAncestor(previousHead, targetId);
|
|
3075
|
+
const abandoned = harness.session.branchMessages(previousHead, ancestor);
|
|
3076
|
+
if (abandoned.length) {
|
|
3077
|
+
ctl.write(theme.dim(` navigato a ${targetId}; il ramo precedente resta nell'albero.\n`));
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
ctl.write(theme.green(`\n testa spostata su ${targetId}\n`));
|
|
3081
|
+
resetSessionUi();
|
|
3082
|
+
replaySession(harness.session, ctl, theme, terminalColumns());
|
|
3083
|
+
return null;
|
|
3084
|
+
}
|
|
3085
|
+
// /tree senza argomenti: mostra l'albero come OUTPUT COLLASSABILE e
|
|
3086
|
+
// navigabile. Se ce n'era gia' uno, viene rimosso e ricreato aggiornato
|
|
3087
|
+
// (la chat non accumula duplicati dell'albero).
|
|
3088
|
+
const nodes = harness.session.tree();
|
|
3089
|
+
const lines = [];
|
|
3090
|
+
for (const n of nodes) {
|
|
3091
|
+
const head = n.id === harness.session.head ? theme.green(" ←") : "";
|
|
3092
|
+
const preview = n.message.content
|
|
3093
|
+
.map((p) => (p.type === "text" ? p.text : `[${p.type}]`))
|
|
3094
|
+
.join(" ")
|
|
3095
|
+
.slice(0, 50);
|
|
3096
|
+
lines.push(theme.dim(` ${n.id} ${n.message.role}: `) + preview + head);
|
|
3097
|
+
}
|
|
3098
|
+
lines.push(theme.dim(" /tree <id> sposta la testa · /tree close nasconde questo output"));
|
|
3099
|
+
showCollapsible(ctl, "tree", theme.bold(`\n sessione (${nodes.length} nodi)`) + theme.dim(" · /tree close per nascondere"), lines.join("\n"));
|
|
3100
|
+
return null;
|
|
3101
|
+
}
|
|
3102
|
+
case "sessions": {
|
|
3103
|
+
const ssub = args.trim();
|
|
3104
|
+
if (ssub === "close" || ssub === "open" || ssub === "toggle") {
|
|
3105
|
+
if (!controlCollapsible(ctl, "sessions", ssub) && ssub === "close") {
|
|
3106
|
+
ctl.write(theme.dim("\n nessun elenco da chiudere: usa /sessions\n\n"));
|
|
3107
|
+
}
|
|
3108
|
+
else if (ssub === "close") {
|
|
3109
|
+
ctl.write(theme.dim("\n elenco chiuso (output rimosso dalla chat)\n\n"));
|
|
3110
|
+
}
|
|
3111
|
+
return null;
|
|
3112
|
+
}
|
|
3113
|
+
const list = listSessions(harness.config.sessionDir);
|
|
3114
|
+
if (!list.length) {
|
|
3115
|
+
ctl.write(theme.dim("\n nessuna sessione salvata\n\n"));
|
|
3116
|
+
return null;
|
|
3117
|
+
}
|
|
3118
|
+
const lines = [];
|
|
3119
|
+
for (const s of list) {
|
|
3120
|
+
const cur = s.id === harness.session.id ? theme.green(" ← corrente") : "";
|
|
3121
|
+
lines.push(theme.dim(` ${s.id} `) + (s.name ? theme.silver(`${s.name} `) : "") + theme.dark(`${fmtAge(s.updatedAt)} · ${s.messages} msg`) + cur);
|
|
3122
|
+
if (s.preview)
|
|
3123
|
+
lines.push(theme.dark(` ${truncate(s.preview, terminalColumns() - 8)}`));
|
|
3124
|
+
}
|
|
3125
|
+
lines.push(theme.dim(" /resume <id> per riprenderne una · /sessions close per nascondere"));
|
|
3126
|
+
showCollapsible(ctl, "sessions", theme.bold(`\n sessioni (${list.length})`) + theme.dim(" · /sessions close per nascondere"), lines.join("\n"));
|
|
3127
|
+
return null;
|
|
3128
|
+
}
|
|
3129
|
+
case "language":
|
|
3130
|
+
case "lang":
|
|
3131
|
+
case "lingua":
|
|
3132
|
+
case "idioma":
|
|
3133
|
+
case "sprache": {
|
|
3134
|
+
const choices = [
|
|
3135
|
+
{ key: "system", label: tr("Automatico (sistema)", "Automatic (system)"), detail: "", value: "system" },
|
|
3136
|
+
...LANGUAGES.map(language => ({ key: language.id, label: language.nome, detail: "", value: language.id })),
|
|
3137
|
+
];
|
|
3138
|
+
let preference = normalizePreference(args.trim().toLowerCase());
|
|
3139
|
+
if (!args.trim()) {
|
|
3140
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
3141
|
+
ctl.write(tr("Uso: /language system|it|en|es|de", "Usage: /language system|it|en|es|de") + "\n");
|
|
3142
|
+
return null;
|
|
3143
|
+
}
|
|
3144
|
+
const selected = await selectOption({ title: tr("Lingua", "Language"), options: choices,
|
|
3145
|
+
currentKey: languagePreference(leggiOnboarding(harness.env), harness.env), ctl, theme, terminalColumns });
|
|
3146
|
+
if (!selected)
|
|
3147
|
+
return null;
|
|
3148
|
+
preference = selected.value;
|
|
3149
|
+
}
|
|
3150
|
+
if (!preference) {
|
|
3151
|
+
notifyStatus(ctl, tr("Uso: /language system|it|en|es|de", "Usage: /language system|it|en|es|de"), true);
|
|
3152
|
+
}
|
|
3153
|
+
else if (!saveLanguagePreference(harness.env, preference)) {
|
|
3154
|
+
notifyStatus(ctl, tr("Impossibile salvare la preferenza della lingua", "Unable to save the language preference"), true);
|
|
3155
|
+
}
|
|
3156
|
+
else {
|
|
3157
|
+
notifyStatus(ctl, tr("Lingua salvata", "Language saved"));
|
|
3158
|
+
ctl.refresh?.();
|
|
3159
|
+
}
|
|
3160
|
+
return null;
|
|
3161
|
+
}
|
|
3162
|
+
case "new":
|
|
3163
|
+
case "clear": {
|
|
3164
|
+
// Sessione NUOVA e vuota, come /clear di Claude Code: azzera cronologia e
|
|
3165
|
+
// contesto vivo senza riavviare il processo. Il piano del regno riparte
|
|
3166
|
+
// pulito (resetPlan + svuotamento stato Vascend) e si riaggancia alla
|
|
3167
|
+
// nuova sessione (loadPlan, che non trova nulla -> nessun custom plan).
|
|
3168
|
+
const previousId = harness.session.id;
|
|
3169
|
+
const fresh = harness.newSession();
|
|
3170
|
+
resetContextFrame({
|
|
3171
|
+
sessionDir: harness.config.sessionDir,
|
|
3172
|
+
frameId: fresh.id,
|
|
3173
|
+
cwd: harness.config.cwd,
|
|
3174
|
+
selection: harness.config.vibeMode,
|
|
3175
|
+
preservePins: false,
|
|
3176
|
+
});
|
|
3177
|
+
vascend.resetPlan(harness.config.sessionDir, fresh.id);
|
|
3178
|
+
vascend.handleCommand("clear");
|
|
3179
|
+
vascend.loadPlan(harness.config.sessionDir, fresh.id);
|
|
3180
|
+
resetSessionUi();
|
|
3181
|
+
await harness.host.emit("session_start", { reason: "new" });
|
|
3182
|
+
ctl.write(tr("nuova sessione {id} · chat precedente salvata: /resume {previous}", "New session {id} · previous chat saved: /resume {previous}", { id: fresh.id, previous: previousId }) + "\n");
|
|
3183
|
+
return null;
|
|
3184
|
+
}
|
|
3185
|
+
case "chats":
|
|
3186
|
+
case "resume": {
|
|
3187
|
+
const list = listSessions(harness.config.sessionDir);
|
|
3188
|
+
if (!list.length) {
|
|
3189
|
+
ctl.write(theme.dim("\n " + tr("nessuna sessione da riprendere", "No saved sessions") + "\n\n"));
|
|
3190
|
+
return null;
|
|
3191
|
+
}
|
|
3192
|
+
if (args.trim()) {
|
|
3193
|
+
if (resumeInto(harness, vascend, args.trim(), ctl, theme, terminalColumns, resetSessionUi)) {
|
|
3194
|
+
await harness.host.emit("session_start", { reason: "resume" });
|
|
3195
|
+
}
|
|
3196
|
+
return null;
|
|
3197
|
+
}
|
|
3198
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
3199
|
+
ctl.write(theme.dim("\n indica l'id: /resume <id> (/sessions per la lista)\n\n"));
|
|
3200
|
+
return null;
|
|
3201
|
+
}
|
|
3202
|
+
const selected = await selectOption({
|
|
3203
|
+
title: tr("Chat salvate", "Saved chats"),
|
|
3204
|
+
hint: tr("Scrivi per cercare · Invio apre · Esc torna alla chat corrente", "Type to search · Enter open · Esc return to the current chat"),
|
|
3205
|
+
filterable: true,
|
|
3206
|
+
options: sessionOptions(list).map((option) => ({ ...option,
|
|
3207
|
+
detail: option.detail + (option.value === harness.session.id ? " · " + tr("corrente", "current") : ""),
|
|
3208
|
+
})),
|
|
3209
|
+
ctl,
|
|
3210
|
+
theme,
|
|
3211
|
+
terminalColumns,
|
|
3212
|
+
});
|
|
3213
|
+
if (!selected)
|
|
3214
|
+
return null;
|
|
3215
|
+
if (resumeInto(harness, vascend, selected.value, ctl, theme, terminalColumns, resetSessionUi)) {
|
|
3216
|
+
await harness.host.emit("session_start", { reason: "resume" });
|
|
3217
|
+
}
|
|
3218
|
+
return null;
|
|
3219
|
+
}
|
|
3220
|
+
case "fast": {
|
|
3221
|
+
const arg = args.trim().toLowerCase();
|
|
3222
|
+
const supported = harness.providers.resolve(harness.config.model)?.provider.supportsFastMode === true;
|
|
3223
|
+
if (!["", "status", "on", "off"].includes(arg)) {
|
|
3224
|
+
ctl.write(theme.dim("\n Uso: /fast on, /fast off oppure /fast status.\n"));
|
|
3225
|
+
return null;
|
|
3226
|
+
}
|
|
3227
|
+
if (arg === "on" && !supported) {
|
|
3228
|
+
ctl.write(theme.yellow("\n Fast non è disponibile con il provider selezionato.\n"));
|
|
3229
|
+
return null;
|
|
3230
|
+
}
|
|
3231
|
+
if (arg === "on" || arg === "off") {
|
|
3232
|
+
const fast = arg === "on";
|
|
3233
|
+
harness.config = { ...harness.config, fast };
|
|
3234
|
+
saveGlobalSettings(harness.env, { fast });
|
|
3235
|
+
}
|
|
3236
|
+
if (!arg || arg === "status")
|
|
3237
|
+
notifyStatus(ctl, `Fast ${harness.config.fast && supported ? "attivo" : "disattivato"}`);
|
|
3238
|
+
ctl.refresh?.();
|
|
3239
|
+
return null;
|
|
3240
|
+
}
|
|
3241
|
+
case "effort": {
|
|
3242
|
+
const resolved = harness.providers.resolve(harness.config.model);
|
|
3243
|
+
const fullModel = resolved ? `${resolved.provider.id}/${resolved.modelId}` : harness.config.model;
|
|
3244
|
+
await loadModelsDev(harness.env);
|
|
3245
|
+
const levels = modelEfforts(fullModel, cachedModelCatalog(harness.env).get(fullModel));
|
|
3246
|
+
const arg = args.trim().toLowerCase();
|
|
3247
|
+
const effort = arg ? (["default", "auto"].includes(arg) ? "" : arg) : await pickEffort(fullModel, harness, ctl, theme, terminalColumns);
|
|
3248
|
+
if (effort === null)
|
|
3249
|
+
return null;
|
|
3250
|
+
if (!levels.includes(effort)) {
|
|
3251
|
+
notifyStatus(ctl, `Effort non disponibile per questo modello: ${arg}. Opzioni: default${levels.filter(Boolean).length ? ", " + levels.filter(Boolean).join(", ") : ""}`, true);
|
|
3252
|
+
return null;
|
|
3253
|
+
}
|
|
3254
|
+
harness.config = { ...harness.config, effort: effort };
|
|
3255
|
+
saveGlobalSettings(harness.env, { effort });
|
|
3256
|
+
ctl.refresh?.();
|
|
3257
|
+
return null;
|
|
3258
|
+
}
|
|
3259
|
+
case "collaboratori":
|
|
3260
|
+
case "kingdoms":
|
|
3261
|
+
case "warm": {
|
|
3262
|
+
const parsed = parseCastles(args);
|
|
3263
|
+
let castles = parsed.castles;
|
|
3264
|
+
if (!castles.length) {
|
|
3265
|
+
ctl.write(theme.dim("\n uso: /collaboratori <attività1; attività2; ...>\n") +
|
|
3266
|
+
theme.dim(" ogni gruppo: \"slug: obiettivo\" oppure solo l'obiettivo.\n") +
|
|
3267
|
+
theme.dim(" esempio: /collaboratori auth: login OAuth; ui: pagina profilo; api: endpoint REST\n") +
|
|
3268
|
+
theme.dim(" un solo obiettivo viene suddiviso automaticamente in gruppi.\n") +
|
|
3269
|
+
theme.dim(" più gruppi possono usare collaboratori paralleli con memoria condivisa.\n\n"));
|
|
3270
|
+
return null;
|
|
3271
|
+
}
|
|
3272
|
+
// Feedback sui warning di parsing (es. slug duplicati rinominati).
|
|
3273
|
+
for (const w of parsed.warnings)
|
|
3274
|
+
ctl.write(theme.yellow(`\n attenzione: ${w}\n`));
|
|
3275
|
+
// Un solo obiettivo senza ';': viene scomposto automaticamente in gruppi.
|
|
3276
|
+
if (castles.length === 1 && !args.includes(";")) {
|
|
3277
|
+
ctl.write(theme.dim("\n suddivisione dell'obiettivo in gruppi di lavoro...\n"));
|
|
3278
|
+
const decomposed = await decomposeKingdom({ harness, goal: args.trim(), signal });
|
|
3279
|
+
if (decomposed.length > 1) {
|
|
3280
|
+
castles = decomposed;
|
|
3281
|
+
for (const c of castles)
|
|
3282
|
+
ctl.write(theme.silver(` · ${c.slug}`) + theme.dim(` ${truncate(c.goal, 70)}`) + "\n");
|
|
3283
|
+
}
|
|
3284
|
+
else {
|
|
3285
|
+
ctl.write(theme.dim(" obiettivo unico: eseguo un solo gruppo.\n"));
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
const threshold = kingdomThreshold(harness.env);
|
|
3289
|
+
const conc = warmConcurrency(harness.env);
|
|
3290
|
+
let parallel = castles.length >= threshold;
|
|
3291
|
+
if (parallel) {
|
|
3292
|
+
const choice = await selectOption({
|
|
3293
|
+
title: `esecuzione parallela — ${castles.length} gruppi`,
|
|
3294
|
+
hint: `fino a ${conc} collaboratori simultanei con memoria condivisa`,
|
|
3295
|
+
options: [
|
|
3296
|
+
{ key: "yes", label: "Sì, in parallelo", detail: `${Math.min(conc, castles.length)} collaboratori simultanei`, value: true },
|
|
3297
|
+
{ key: "no", label: "No, in sequenza", detail: "un gruppo alla volta", value: false },
|
|
3298
|
+
],
|
|
3299
|
+
currentKey: "yes",
|
|
3300
|
+
ctl,
|
|
3301
|
+
theme,
|
|
3302
|
+
terminalColumns,
|
|
3303
|
+
});
|
|
3304
|
+
parallel = choice?.value === true;
|
|
3305
|
+
}
|
|
3306
|
+
const kingdomId = `kw-${harness.session.id}`;
|
|
3307
|
+
const concurrency = parallel ? conc : 1;
|
|
3308
|
+
ctl.write(theme.accent(`\n Attività in parallelo`) +
|
|
3309
|
+
theme.dim(` · ${castles.length} gruppi · ${parallel ? `${Math.min(conc, castles.length)} collaboratori paralleli` : "sequenziale"} · memoria condivisa\n`));
|
|
3310
|
+
const live = createKingdomLive(ctl, theme, castles);
|
|
3311
|
+
const reports = await runKingdomWarm({
|
|
3312
|
+
harness,
|
|
3313
|
+
kingdomId,
|
|
3314
|
+
castles,
|
|
3315
|
+
signal,
|
|
3316
|
+
confirm: async () => true, // consenso dato in blocco: i vassalli eseguono i tool
|
|
3317
|
+
concurrency,
|
|
3318
|
+
onEvent: live.onEvent,
|
|
3319
|
+
});
|
|
3320
|
+
live.finish();
|
|
3321
|
+
ctl.write(renderKingdomReport(reports, theme, terminalColumns()));
|
|
3322
|
+
// GATE KPI di regno (opt-in): misura esecuzione + allineamento all'obiettivo.
|
|
3323
|
+
if (selfImproveEnabled(harness.env) && reports.length && !signal.aborted) {
|
|
3324
|
+
try {
|
|
3325
|
+
let roomIndex = "";
|
|
3326
|
+
try {
|
|
3327
|
+
const block = RoomMemory.open(harness.config.sessionDir, kingdomId).indexBlock();
|
|
3328
|
+
if (block.trim())
|
|
3329
|
+
roomIndex = block;
|
|
3330
|
+
}
|
|
3331
|
+
catch { /* nessuna memoria */ }
|
|
3332
|
+
const verdict = await runKingdomGate({
|
|
3333
|
+
harness,
|
|
3334
|
+
reports,
|
|
3335
|
+
goal: args.trim(),
|
|
3336
|
+
...(roomIndex ? { roomIndex } : {}),
|
|
3337
|
+
signal,
|
|
3338
|
+
});
|
|
3339
|
+
ctl.write("\n " + formatKingdomGate(verdict).split("\n").join("\n ") + "\n");
|
|
3340
|
+
}
|
|
3341
|
+
catch { /* fail-open */ }
|
|
3342
|
+
}
|
|
3343
|
+
return null;
|
|
3344
|
+
}
|
|
3345
|
+
default: {
|
|
3346
|
+
// Skill: /skill:nome [args] carica SKILL.md come input per il modello.
|
|
3347
|
+
if (harness.skillCommandsEnabled && (cmd === "skill" || (cmd ?? "").startsWith("skill:"))) {
|
|
3348
|
+
const skillName = cmd === "skill" ? (rest[0] ?? "") : (cmd ?? "").slice("skill:".length);
|
|
3349
|
+
const skillArgs = cmd === "skill" ? rest.slice(1).join(" ") : args;
|
|
3350
|
+
if (!skillName) {
|
|
3351
|
+
const names = harness.skills.map((s) => s.name).join(", ") || "nessuna";
|
|
3352
|
+
ctl.write(theme.dim(`\n skill disponibili: ${names}\n`));
|
|
3353
|
+
return null;
|
|
3354
|
+
}
|
|
3355
|
+
const skill = findSkill(harness.skills, skillName);
|
|
3356
|
+
if (!skill) {
|
|
3357
|
+
ctl.write(theme.yellow(`\n skill sconosciuta: ${skillName}\n`));
|
|
3358
|
+
return null;
|
|
3359
|
+
}
|
|
3360
|
+
ctl.write("\n" + skillPreview(skill.name, theme, terminalColumns(), "ok").join("\n") + "\n");
|
|
3361
|
+
return { input: renderSkillInvocation(skill, skillArgs) };
|
|
3362
|
+
}
|
|
3363
|
+
// Prompt template: espande il corpo e lo invia come input al modello.
|
|
3364
|
+
const tpl = findTemplate(templates, cmd ?? "");
|
|
3365
|
+
if (tpl) {
|
|
3366
|
+
return { input: renderTemplate(tpl, args) };
|
|
3367
|
+
}
|
|
3368
|
+
const ext = harness.host.commands.get(cmd ?? "");
|
|
3369
|
+
if (ext) {
|
|
3370
|
+
const output = await ext.run(args, harness.host.publicApi());
|
|
3371
|
+
return output ? { input: output } : null;
|
|
3372
|
+
}
|
|
3373
|
+
// Comando sconosciuto: suggerisci il piu' vicino fra TUTTI i comandi noti
|
|
3374
|
+
// (builtin + alias + estensioni + template + skill) con edit distance.
|
|
3375
|
+
const known = [
|
|
3376
|
+
...visibleBuiltinNames(),
|
|
3377
|
+
...[...harness.host.commands.keys()],
|
|
3378
|
+
...templates.map((t) => t.name),
|
|
3379
|
+
...harness.skills.map((s) => `skill:${s.name}`),
|
|
3380
|
+
];
|
|
3381
|
+
const guesses = suggestCommands(cmd ?? "", known);
|
|
3382
|
+
ctl.write(theme.red(`\n comando sconosciuto: /${cmd}`));
|
|
3383
|
+
if (guesses.length) {
|
|
3384
|
+
ctl.write(theme.dim(` forse intendevi: ${guesses.map((g) => `/${g}`).join(", ")}`));
|
|
3385
|
+
}
|
|
3386
|
+
else {
|
|
3387
|
+
ctl.write(theme.dim(" usa /help per la lista dei comandi"));
|
|
3388
|
+
}
|
|
3389
|
+
ctl.write("\n");
|
|
3390
|
+
return null;
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
// === Accesso provider (/login) ==============================================
|
|
3395
|
+
// Un solo flusso mostra lo stato reale, avvia i login dei CLI ufficiali e
|
|
3396
|
+
// ricarica subito il ProviderRegistry. Le API key e OpenRouter riusano il menu
|
|
3397
|
+
// provider, evitando due implementazioni divergenti delle credenziali.
|
|
3398
|
+
async function loginMenu(harness, ctl, theme, terminalColumns) {
|
|
3399
|
+
for (;;) {
|
|
3400
|
+
const rows = loginRows(harness.env, harness.providers);
|
|
3401
|
+
const picked = await selectOption({
|
|
3402
|
+
title: tr("accesso ai modelli", "model sign-in"),
|
|
3403
|
+
hint: "Enter gestisci · digita per cercare · Esc chiudi",
|
|
3404
|
+
options: rows.map((row) => ({
|
|
3405
|
+
key: row.id,
|
|
3406
|
+
label: row.label,
|
|
3407
|
+
detail: `${loginStatusLabel(row)} · ${row.detail}`,
|
|
3408
|
+
value: row,
|
|
3409
|
+
})),
|
|
3410
|
+
ctl,
|
|
3411
|
+
theme,
|
|
3412
|
+
terminalColumns,
|
|
3413
|
+
filterable: true,
|
|
3414
|
+
});
|
|
3415
|
+
if (!picked)
|
|
3416
|
+
return;
|
|
3417
|
+
const row = picked.value;
|
|
3418
|
+
if (row.id === "api-keys") {
|
|
3419
|
+
await providerMenu(harness, ctl, theme, terminalColumns);
|
|
3420
|
+
continue;
|
|
3421
|
+
}
|
|
3422
|
+
if (row.id === "openrouter") {
|
|
3423
|
+
const provider = providerRows(harness.env, harness.providers, harness.config.model)
|
|
3424
|
+
.find((item) => item.id === "openrouter");
|
|
3425
|
+
if (provider)
|
|
3426
|
+
await providerActions(provider, harness, ctl, theme, terminalColumns);
|
|
3427
|
+
continue;
|
|
3428
|
+
}
|
|
3429
|
+
const action = await selectOption({
|
|
3430
|
+
title: `${row.label} · ${loginStatusLabel(row)}`,
|
|
3431
|
+
hint: row.command ? `comando: ${row.command.join(" ")}` : "",
|
|
3432
|
+
options: [
|
|
3433
|
+
{
|
|
3434
|
+
key: "login",
|
|
3435
|
+
label: row.configured && !row.needsRefresh ? tr("accedi di nuovo", "sign in again") : tr("accedi ora", "sign in"),
|
|
3436
|
+
detail: tr("apre la procedura di accesso nel terminale corrente", "opens the sign-in flow in the current terminal"),
|
|
3437
|
+
value: "login",
|
|
3438
|
+
},
|
|
3439
|
+
...(row.configured && !row.needsRefresh
|
|
3440
|
+
? [{
|
|
3441
|
+
key: "model",
|
|
3442
|
+
label: tr("scegli un modello", "choose a model"),
|
|
3443
|
+
detail: tr("usa questo abbonamento nel prossimo turno", "use this subscription on the next turn"),
|
|
3444
|
+
value: "model",
|
|
3445
|
+
}]
|
|
3446
|
+
: []),
|
|
3447
|
+
],
|
|
3448
|
+
ctl,
|
|
3449
|
+
theme,
|
|
3450
|
+
terminalColumns,
|
|
3451
|
+
});
|
|
3452
|
+
if (!action)
|
|
3453
|
+
continue;
|
|
3454
|
+
if (action.value === "model") {
|
|
3455
|
+
if (await pickModel(harness, ctl, theme, terminalColumns))
|
|
3456
|
+
return;
|
|
3457
|
+
continue;
|
|
3458
|
+
}
|
|
3459
|
+
if (!row.command)
|
|
3460
|
+
continue;
|
|
3461
|
+
const completed = await runInteractiveLogin(row.command, harness, ctl, theme);
|
|
3462
|
+
harness.reloadProviders();
|
|
3463
|
+
const refreshed = loginRows(harness.env, harness.providers).find((item) => item.id === row.id);
|
|
3464
|
+
if (completed && refreshed?.configured && !refreshed.needsRefresh) {
|
|
3465
|
+
ctl.write(theme.green(`\n ${row.label}: accesso rilevato e modelli aggiornati.\n`));
|
|
3466
|
+
}
|
|
3467
|
+
else if (completed) {
|
|
3468
|
+
ctl.write(theme.yellow(`\n ${row.label}: il comando e' terminato, ma non trovo ancora una sessione valida.\n`));
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
async function runInteractiveLogin(command, harness, ctl, theme) {
|
|
3473
|
+
const [executable, ...args] = command;
|
|
3474
|
+
if (!executable)
|
|
3475
|
+
return false;
|
|
3476
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
3477
|
+
ctl.write(theme.dim(`\n esegui in un terminale: ${command.join(" ")}\n`));
|
|
3478
|
+
return false;
|
|
3479
|
+
}
|
|
3480
|
+
ctl.write(theme.bold(`\n avvio ${command.join(" ")}\n`));
|
|
3481
|
+
ctl.write(theme.dim(" completa l'accesso; al termine tornerai automaticamente a Talos.\n\n"));
|
|
3482
|
+
ctl.pause();
|
|
3483
|
+
ctl.setOverlay(null);
|
|
3484
|
+
ctl.setStatus(null);
|
|
3485
|
+
const wasRaw = stdin.isRaw;
|
|
3486
|
+
if (typeof stdin.setRawMode === "function")
|
|
3487
|
+
stdin.setRawMode(false);
|
|
3488
|
+
stdin.resume();
|
|
3489
|
+
try {
|
|
3490
|
+
const code = await new Promise((resolve) => {
|
|
3491
|
+
const child = spawn(executable, args, {
|
|
3492
|
+
env: harness.env,
|
|
3493
|
+
stdio: "inherit",
|
|
3494
|
+
shell: process.platform === "win32",
|
|
3495
|
+
});
|
|
3496
|
+
child.once("error", () => resolve(-1));
|
|
3497
|
+
child.once("exit", (value) => resolve(value ?? -1));
|
|
3498
|
+
});
|
|
3499
|
+
if (code !== 0) {
|
|
3500
|
+
ctl.write(theme.yellow(`\n login non completato (uscita ${code}). Verifica che \`${executable}\` sia installato.\n`));
|
|
3501
|
+
return false;
|
|
3502
|
+
}
|
|
3503
|
+
return true;
|
|
3504
|
+
}
|
|
3505
|
+
finally {
|
|
3506
|
+
if (typeof stdin.setRawMode === "function")
|
|
3507
|
+
stdin.setRawMode(Boolean(wasRaw));
|
|
3508
|
+
ctl.resume();
|
|
3509
|
+
ctl.refresh?.();
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
// === Menu provider navigabile (/provider) ===
|
|
3513
|
+
// Schermata interattiva: elenco provider con stato, scelta di uno, azioni
|
|
3514
|
+
// guidate (imposta chiave/endpoint, OAuth, scegli modello, rimuovi). Dopo ogni
|
|
3515
|
+
// modifica ricostruisce il registro (harness.reloadProviders) cosi' il provider
|
|
3516
|
+
// diventa attivo senza riavvio.
|
|
3517
|
+
async function providerMenu(harness, ctl, theme, terminalColumns) {
|
|
3518
|
+
// Loop sull'elenco: si esce con Esc.
|
|
3519
|
+
for (;;) {
|
|
3520
|
+
const rows = providerRows(harness.env, harness.providers, harness.config.model);
|
|
3521
|
+
const options = rows.map((row) => ({
|
|
3522
|
+
key: row.id,
|
|
3523
|
+
label: `${row.label}${row.active ? " ←" : ""}`,
|
|
3524
|
+
detail: `${rowStatusLabel(row)}${row.modelCount ? ` · ${row.modelCount} modelli` : ""}`,
|
|
3525
|
+
value: row,
|
|
3526
|
+
}));
|
|
3527
|
+
const configured = rows.filter((r) => r.configured).length;
|
|
3528
|
+
const picked = await selectOption({
|
|
3529
|
+
title: `provider — ${configured} configurati su ${rows.length}`,
|
|
3530
|
+
hint: "↑/↓ scegli · Enter apri · digita per cercare · Esc chiudi",
|
|
3531
|
+
options,
|
|
3532
|
+
ctl,
|
|
3533
|
+
theme,
|
|
3534
|
+
terminalColumns,
|
|
3535
|
+
filterable: true,
|
|
3536
|
+
});
|
|
3537
|
+
if (!picked || !picked.value)
|
|
3538
|
+
return; // Esc: chiudi il menu
|
|
3539
|
+
const done = await providerActions(picked.value, harness, ctl, theme, terminalColumns);
|
|
3540
|
+
if (done === "model-set")
|
|
3541
|
+
return; // ha scelto un modello: torna alla chat
|
|
3542
|
+
// altrimenti rientra nel loop con lo stato aggiornato
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
// Azioni su un singolo provider. Ritorna "model-set" se l'utente ha scelto un
|
|
3546
|
+
// modello (chiude il menu), altrimenti "back" per tornare all'elenco.
|
|
3547
|
+
async function providerActions(row, harness, ctl, theme, terminalColumns) {
|
|
3548
|
+
for (;;) {
|
|
3549
|
+
// Rilegge lo stato corrente del provider (puo' essere cambiato).
|
|
3550
|
+
const fresh = providerRows(harness.env, harness.providers, harness.config.model).find((r) => r.id === row.id) ?? row;
|
|
3551
|
+
const actions = actionsFor(harness.env, fresh);
|
|
3552
|
+
const choice = await selectOption({
|
|
3553
|
+
title: `${fresh.label} · ${rowStatusLabel(fresh)}`,
|
|
3554
|
+
hint: fresh.baseUrl ? `endpoint: ${fresh.baseUrl}` : "",
|
|
3555
|
+
options: actions.map((a) => ({ key: a.action, label: a.label, detail: a.detail, value: a.action })),
|
|
3556
|
+
ctl,
|
|
3557
|
+
theme,
|
|
3558
|
+
terminalColumns,
|
|
3559
|
+
});
|
|
3560
|
+
if (!choice || choice.value === "back")
|
|
3561
|
+
return "back";
|
|
3562
|
+
switch (choice.value) {
|
|
3563
|
+
case "set-key": {
|
|
3564
|
+
const ok = await setKeyFlow(fresh, harness, ctl, theme);
|
|
3565
|
+
if (ok)
|
|
3566
|
+
ctl.write(theme.green(`\n ${fresh.label}: chiave salvata e provider attivato.\n`));
|
|
3567
|
+
break;
|
|
3568
|
+
}
|
|
3569
|
+
case "set-endpoint": {
|
|
3570
|
+
await setEndpointFlow(fresh, harness, ctl, theme);
|
|
3571
|
+
break;
|
|
3572
|
+
}
|
|
3573
|
+
case "oauth": {
|
|
3574
|
+
await oauthFlow(fresh, harness, ctl, theme, terminalColumns);
|
|
3575
|
+
break;
|
|
3576
|
+
}
|
|
3577
|
+
case "use-model": {
|
|
3578
|
+
const set = await pickModel(harness, ctl, theme, terminalColumns);
|
|
3579
|
+
if (set)
|
|
3580
|
+
return "model-set";
|
|
3581
|
+
break;
|
|
3582
|
+
}
|
|
3583
|
+
case "remove": {
|
|
3584
|
+
const confirmed = await selectOption({
|
|
3585
|
+
title: `rimuovere la credenziale di ${fresh.label}?`,
|
|
3586
|
+
hint: "la chiave salvata verra' eliminata dal config",
|
|
3587
|
+
options: [
|
|
3588
|
+
{ key: "no", label: "No, mantieni", detail: "", value: false },
|
|
3589
|
+
{ key: "yes", label: "Sì, rimuovi", detail: "", value: true },
|
|
3590
|
+
],
|
|
3591
|
+
currentKey: "no",
|
|
3592
|
+
ctl,
|
|
3593
|
+
theme,
|
|
3594
|
+
terminalColumns,
|
|
3595
|
+
});
|
|
3596
|
+
if (confirmed?.value) {
|
|
3597
|
+
const res = removeProvider(harness.env, fresh.id);
|
|
3598
|
+
if (res.ok) {
|
|
3599
|
+
harness.reloadProviders();
|
|
3600
|
+
ctl.write(theme.green(`\n ${fresh.label}: credenziale rimossa.\n`));
|
|
3601
|
+
}
|
|
3602
|
+
else {
|
|
3603
|
+
ctl.write(theme.yellow(`\n ${res.error ?? "rimozione non riuscita"}\n`));
|
|
3604
|
+
}
|
|
3605
|
+
return "back";
|
|
3606
|
+
}
|
|
3607
|
+
break;
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
// Flusso "imposta API key": chiede la chiave, la valida (per OpenRouter via
|
|
3613
|
+
// rete), la salva e ricostruisce il registro. Ritorna true se salvata.
|
|
3614
|
+
async function setKeyFlow(row, harness, ctl, theme) {
|
|
3615
|
+
ctl.write(theme.dim(`\n ${providerKeyHint(row.spec)}\n`));
|
|
3616
|
+
const key = await ctl.promptInput(`API key per ${row.label} (Invio conferma · Esc annulla)`);
|
|
3617
|
+
if (!key)
|
|
3618
|
+
return false;
|
|
3619
|
+
const trimmed = key.trim();
|
|
3620
|
+
// Validazione di forma per OpenRouter (sk-or-...). Avviso non bloccante.
|
|
3621
|
+
if (row.id === "openrouter" && !looksLikeApiKey(trimmed)) {
|
|
3622
|
+
ctl.write(theme.yellow("\n attenzione: la chiave non sembra una OpenRouter key (sk-or-...). Provo comunque.\n"));
|
|
3623
|
+
}
|
|
3624
|
+
// Validazione di rete (oggi solo OpenRouter espone /key). Best effort.
|
|
3625
|
+
if (row.id === "openrouter") {
|
|
3626
|
+
ctl.setStatus(theme.dim("verifico la chiave su OpenRouter…"));
|
|
3627
|
+
try {
|
|
3628
|
+
const v = await validateApiKey(trimmed);
|
|
3629
|
+
ctl.setStatus(null);
|
|
3630
|
+
if (!v.ok) {
|
|
3631
|
+
ctl.write(theme.yellow(`\n chiave non valida: ${v.error ?? "verifica fallita"}. Non salvata.\n`));
|
|
3632
|
+
return false;
|
|
3633
|
+
}
|
|
3634
|
+
const credit = v.limitRemaining === null || v.limitRemaining === undefined
|
|
3635
|
+
? "credito illimitato"
|
|
3636
|
+
: `credito residuo ${v.limitRemaining.toFixed(2)}`;
|
|
3637
|
+
ctl.write(theme.green(`\n chiave valida${v.label ? ` (${v.label})` : ""} · ${credit}\n`));
|
|
3638
|
+
}
|
|
3639
|
+
catch {
|
|
3640
|
+
ctl.setStatus(null);
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
const res = saveProviderKey(harness.env, row.id, trimmed);
|
|
3644
|
+
if (!res.ok) {
|
|
3645
|
+
ctl.write(theme.yellow(`\n ${res.error ?? "salvataggio non riuscito"}\n`));
|
|
3646
|
+
return false;
|
|
3647
|
+
}
|
|
3648
|
+
// La env scoped serve a reloadProviders (che la rilegge dal disco), ma la
|
|
3649
|
+
// impostiamo anche nel processo corrente per coerenza immediata.
|
|
3650
|
+
const norm = row.id.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
3651
|
+
harness.env[`TALOS_${norm}_API_KEY`] = trimmed;
|
|
3652
|
+
harness.reloadProviders();
|
|
3653
|
+
return true;
|
|
3654
|
+
}
|
|
3655
|
+
// Flusso "imposta endpoint": chiede una baseUrl (vuota = ripristina default).
|
|
3656
|
+
async function setEndpointFlow(row, harness, ctl, theme) {
|
|
3657
|
+
ctl.write(theme.dim(`\n endpoint attuale: ${row.baseUrl ?? "(default del provider)"} · vuoto = ripristina default\n`));
|
|
3658
|
+
const url = await ctl.promptInput(`baseUrl per ${row.label} (Invio conferma · Esc annulla)`);
|
|
3659
|
+
if (url === null)
|
|
3660
|
+
return; // Esc
|
|
3661
|
+
const res = saveProviderBaseUrl(harness.env, row.id, url);
|
|
3662
|
+
if (!res.ok) {
|
|
3663
|
+
ctl.write(theme.yellow(`\n ${res.error ?? "salvataggio non riuscito"}\n`));
|
|
3664
|
+
return;
|
|
3665
|
+
}
|
|
3666
|
+
const norm = row.id.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
3667
|
+
if (url.trim())
|
|
3668
|
+
harness.env[`TALOS_${norm}_BASE_URL`] = url.trim();
|
|
3669
|
+
else
|
|
3670
|
+
delete harness.env[`TALOS_${norm}_BASE_URL`];
|
|
3671
|
+
harness.reloadProviders();
|
|
3672
|
+
ctl.write(theme.green(`\n ${row.label}: endpoint ${url.trim() ? "aggiornato" : "ripristinato al default"}.\n`));
|
|
3673
|
+
}
|
|
3674
|
+
// Flusso OAuth (oggi: OpenRouter PKCE). Mostra l'URL da aprire nel browser e
|
|
3675
|
+
// raccoglie il code dal callback, poi scambia e salva la chiave.
|
|
3676
|
+
async function oauthFlow(row, harness, ctl, theme, terminalColumns) {
|
|
3677
|
+
if (row.id !== "openrouter") {
|
|
3678
|
+
transientNotice(ctl, "OAuth non disponibile per questo provider.", "failed", theme);
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3681
|
+
// Il login e' una vista operativa temporanea: non deve diventare parte della
|
|
3682
|
+
// conversazione. L'overlay viene aggiornato in place e rimosso al termine.
|
|
3683
|
+
let authUrl = "";
|
|
3684
|
+
let authState = "avvio il server locale…";
|
|
3685
|
+
const drawOAuth = () => {
|
|
3686
|
+
const lines = [authState];
|
|
3687
|
+
if (authUrl)
|
|
3688
|
+
lines.push("", "Se il browser non si apre, copia questo link:", authUrl);
|
|
3689
|
+
lines.push("", "Questa schermata si chiudera' automaticamente.");
|
|
3690
|
+
ctl.setOverlay([
|
|
3691
|
+
theme.bold(" OpenRouter · accesso via browser"),
|
|
3692
|
+
theme.dark(` ${"─".repeat(Math.max(12, terminalColumns() - 4))}`),
|
|
3693
|
+
...lines.flatMap((line) => wrapPlain(line || " ", Math.max(20, terminalColumns() - 4)))
|
|
3694
|
+
.map((line) => ` ${theme.silver(line)}`),
|
|
3695
|
+
]);
|
|
3696
|
+
};
|
|
3697
|
+
drawOAuth();
|
|
3698
|
+
const onEvent = (ev) => {
|
|
3699
|
+
switch (ev.phase) {
|
|
3700
|
+
case "url_built":
|
|
3701
|
+
authUrl = ev.detail ?? authUrl;
|
|
3702
|
+
authState = "apro il browser per autorizzare…";
|
|
3703
|
+
break;
|
|
3704
|
+
case "browser_opened":
|
|
3705
|
+
authState = "browser aperto · in attesa dell'autorizzazione…";
|
|
3706
|
+
break;
|
|
3707
|
+
case "browser_failed":
|
|
3708
|
+
authState = "apri il link manualmente · in attesa dell'autorizzazione…";
|
|
3709
|
+
break;
|
|
3710
|
+
case "awaiting_code":
|
|
3711
|
+
authState = "in attesa dell'autorizzazione nel browser…";
|
|
3712
|
+
break;
|
|
3713
|
+
case "exchanging":
|
|
3714
|
+
authState = "autorizzazione ricevuta · attivo la API key…";
|
|
3715
|
+
break;
|
|
3716
|
+
default:
|
|
3717
|
+
break;
|
|
3718
|
+
}
|
|
3719
|
+
drawOAuth();
|
|
3720
|
+
};
|
|
3721
|
+
try {
|
|
3722
|
+
// Flusso automatico: server locale su loopback + apertura browser. Il
|
|
3723
|
+
// redirect_uri e' http://127.0.0.1:PORT/callback (niente dominio pubblico).
|
|
3724
|
+
const result = await orchestrateOAuth(harness.env, {
|
|
3725
|
+
startServer: startCallbackServer,
|
|
3726
|
+
buildAuthUrl: (redirectUri, challenge) => {
|
|
3727
|
+
const u = new URL(redirectUri);
|
|
3728
|
+
return buildAuthUrlForPort(Number(u.port), challenge, u.hostname);
|
|
3729
|
+
},
|
|
3730
|
+
openBrowser: async (url) => {
|
|
3731
|
+
// print no-op: l'URL lo mostriamo gia' noi tramite onEvent(url_built).
|
|
3732
|
+
const res = await openUrl(url, { print: () => { } });
|
|
3733
|
+
return { opened: res.opened, error: res.error };
|
|
3734
|
+
},
|
|
3735
|
+
onEvent,
|
|
3736
|
+
});
|
|
3737
|
+
ctl.setStatus(null);
|
|
3738
|
+
ctl.setOverlay(null);
|
|
3739
|
+
if (result.ok && result.apiKey) {
|
|
3740
|
+
// orchestrateOAuth ha gia' persistito le credenziali OpenRouter; allineiamo
|
|
3741
|
+
// anche il config globale generico e la env scoped per reloadProviders.
|
|
3742
|
+
saveProviderKey(harness.env, row.id, result.apiKey);
|
|
3743
|
+
harness.env["TALOS_OPENROUTER_API_KEY"] = result.apiKey;
|
|
3744
|
+
harness.reloadProviders();
|
|
3745
|
+
transientNotice(ctl, "OpenRouter collegato e attivato", "done", theme);
|
|
3746
|
+
return;
|
|
3747
|
+
}
|
|
3748
|
+
// Fallimento del flusso automatico (browser/timeout/callback/server):
|
|
3749
|
+
// ricadiamo sul percorso manuale con un flow nuovo e coerente (PKCE
|
|
3750
|
+
// proprio + authUrl mostrato all'utente, che incolla il code).
|
|
3751
|
+
const reason = result.error ?? "callback automatico non riuscito";
|
|
3752
|
+
const manualFlow = startOAuthFlow(harness.env, callbackUrlForPort());
|
|
3753
|
+
const manualLines = [];
|
|
3754
|
+
const manual = await runManualCodeFallback({
|
|
3755
|
+
print: (line) => { manualLines.push(line); },
|
|
3756
|
+
prompt: async (label) => {
|
|
3757
|
+
await showReadonlyOverlay({
|
|
3758
|
+
title: "OpenRouter · autorizzazione manuale",
|
|
3759
|
+
lines: manualLines,
|
|
3760
|
+
ctl,
|
|
3761
|
+
theme,
|
|
3762
|
+
terminalColumns,
|
|
3763
|
+
});
|
|
3764
|
+
return ctl.promptInput(label);
|
|
3765
|
+
},
|
|
3766
|
+
}, manualFlow, { reason });
|
|
3767
|
+
if (!manual.ok || !manual.apiKey) {
|
|
3768
|
+
if (manual.error === "cancelled") {
|
|
3769
|
+
transientNotice(ctl, "OAuth annullato", "done", theme);
|
|
3770
|
+
}
|
|
3771
|
+
else {
|
|
3772
|
+
transientNotice(ctl, `OAuth non riuscito: ${manual.error ?? "scambio fallito"}`, "failed", theme);
|
|
3773
|
+
}
|
|
3774
|
+
return;
|
|
3775
|
+
}
|
|
3776
|
+
saveProviderKey(harness.env, row.id, manual.apiKey);
|
|
3777
|
+
harness.env["TALOS_OPENROUTER_API_KEY"] = manual.apiKey;
|
|
3778
|
+
harness.reloadProviders();
|
|
3779
|
+
transientNotice(ctl, "OpenRouter collegato e attivato", "done", theme);
|
|
3780
|
+
}
|
|
3781
|
+
catch (err) {
|
|
3782
|
+
ctl.setStatus(null);
|
|
3783
|
+
ctl.setOverlay(null);
|
|
3784
|
+
transientNotice(ctl, `errore OAuth: ${err.message}`, "failed", theme);
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
function transientNotice(ctl, label, state, theme) {
|
|
3788
|
+
notifyStatus(ctl, label, state === "failed");
|
|
3789
|
+
}
|
|
3790
|
+
async function pickModel(harness, ctl, theme, terminalColumns) {
|
|
3791
|
+
const providers = harness.providers.list();
|
|
3792
|
+
const dynamic = await loadDynamicModels(providers, ctl, theme, harness.env);
|
|
3793
|
+
const accessOptions = loginRows(harness.env, harness.providers)
|
|
3794
|
+
.filter((row) => !row.configured || row.needsRefresh)
|
|
3795
|
+
.map((row) => ({
|
|
3796
|
+
key: `access:${row.id}`,
|
|
3797
|
+
label: `${row.needsRefresh ? "Rinnova" : "Collega"} ${row.label}`,
|
|
3798
|
+
detail: `${loginStatusLabel(row)} · ${row.detail}`,
|
|
3799
|
+
value: { providerId: `access:${row.id}`, modelId: "", fullModel: `access:${row.id}` },
|
|
3800
|
+
}));
|
|
3801
|
+
const options = [...accessOptions, ...modelOptions(providers, dynamic)];
|
|
3802
|
+
if (!options.length) {
|
|
3803
|
+
ctl.write(theme.yellow("\n nessun provider configurato. Usa /login per collegare un abbonamento o una API key.\n"));
|
|
3804
|
+
return false;
|
|
3805
|
+
}
|
|
3806
|
+
const current = canonicalModel(harness.config.model);
|
|
3807
|
+
const selected = await selectOption({
|
|
3808
|
+
title: "Modelli",
|
|
3809
|
+
options,
|
|
3810
|
+
currentKey: current?.fullModel,
|
|
3811
|
+
ctl,
|
|
3812
|
+
theme,
|
|
3813
|
+
terminalColumns,
|
|
3814
|
+
filterable: true,
|
|
3815
|
+
});
|
|
3816
|
+
if (!selected)
|
|
3817
|
+
return false;
|
|
3818
|
+
if (selected.value.providerId.startsWith("access:")) {
|
|
3819
|
+
await loginMenu(harness, ctl, theme, terminalColumns);
|
|
3820
|
+
return false;
|
|
3821
|
+
}
|
|
3822
|
+
const auth = await pickAuthMethod(selected.value, harness, ctl, theme, terminalColumns);
|
|
3823
|
+
if (!auth)
|
|
3824
|
+
return false;
|
|
3825
|
+
return applyModelChoice(auth.value, harness, ctl, theme, terminalColumns);
|
|
3826
|
+
}
|
|
3827
|
+
async function pickAuthMethod(choice, harness, ctl, theme, terminalColumns) {
|
|
3828
|
+
const options = authOptions(choice, harness.providers.list());
|
|
3829
|
+
if (!options.length) {
|
|
3830
|
+
ctl.write(theme.yellow(`\n nessun metodo di accesso configurato per ${choice.fullModel}.\n`));
|
|
3831
|
+
ctl.write(theme.dim(authHint(choice.providerId) + "\n"));
|
|
3832
|
+
return null;
|
|
3833
|
+
}
|
|
3834
|
+
if (!requiresAuthChoice(choice.providerId))
|
|
3835
|
+
return options[0] ?? null;
|
|
3836
|
+
return selectOption({
|
|
3837
|
+
title: "metodo accesso",
|
|
3838
|
+
hint: "↑/↓ muovi Enter conferma Esc/q annulla",
|
|
3839
|
+
options,
|
|
3840
|
+
currentKey: harness.config.model,
|
|
3841
|
+
ctl,
|
|
3842
|
+
theme,
|
|
3843
|
+
terminalColumns,
|
|
3844
|
+
});
|
|
3845
|
+
}
|
|
3846
|
+
function modelOptions(providers, dynamic) {
|
|
3847
|
+
const byId = new Map(providers.map((provider) => [provider.id, provider]));
|
|
3848
|
+
const out = [];
|
|
3849
|
+
for (const spec of PROVIDER_CATALOG) {
|
|
3850
|
+
const authProviders = authProviderIds(spec.id).map((id) => byId.get(id)).filter((p) => Boolean(p));
|
|
3851
|
+
if (!authProviders.length)
|
|
3852
|
+
continue;
|
|
3853
|
+
const dyn = dynamic?.get(spec.id) ?? [];
|
|
3854
|
+
const dynById = new Map(dyn.map((d) => [d.id, d]));
|
|
3855
|
+
const models = unique([
|
|
3856
|
+
...spec.models,
|
|
3857
|
+
...authProviders.flatMap((provider) => provider.models),
|
|
3858
|
+
...dyn.map((d) => d.id),
|
|
3859
|
+
]);
|
|
3860
|
+
for (const model of models) {
|
|
3861
|
+
const full = `${spec.id}/${model}`;
|
|
3862
|
+
const detail = dynById.get(model)?.detail;
|
|
3863
|
+
out.push({
|
|
3864
|
+
key: full,
|
|
3865
|
+
label: `${spec.label} ${model}`,
|
|
3866
|
+
detail: `${authSummary(spec.id, authProviders)}${detail ? ` · ${detail}` : ""}`,
|
|
3867
|
+
value: { providerId: spec.id, modelId: model, fullModel: full },
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
return out;
|
|
3872
|
+
}
|
|
3873
|
+
// Carica i cataloghi dinamici dei provider che li espongono (oggi: OpenRouter).
|
|
3874
|
+
// Fallisce in modo morbido: in caso di rete assente, restituisce mappa vuota e
|
|
3875
|
+
// il picker resta coi suggerimenti statici.
|
|
3876
|
+
async function loadDynamicModels(providers, ctl, theme, env) {
|
|
3877
|
+
const map = new Map();
|
|
3878
|
+
const catalog = await loadModelsDev(env);
|
|
3879
|
+
for (const spec of PROVIDER_CATALOG) {
|
|
3880
|
+
if (!providers.some(p => authProviderIds(spec.id).includes(p.id)))
|
|
3881
|
+
continue;
|
|
3882
|
+
map.set(spec.id, catalog.list(spec.id)
|
|
3883
|
+
.filter(m => m.status !== "deprecated" && m.outputModalities.includes("text") && m.toolCall !== false)
|
|
3884
|
+
.map(m => ({ id: m.id, detail: modelDetail(m) })));
|
|
3885
|
+
}
|
|
3886
|
+
if (providers.some((p) => p.id === "openrouter")) {
|
|
3887
|
+
ctl.setStatus(theme.dim("carico catalogo OpenRouter…"));
|
|
3888
|
+
try {
|
|
3889
|
+
const models = await fetchOpenRouterModels(env);
|
|
3890
|
+
if (models.length) {
|
|
3891
|
+
const existing = new Map((map.get("openrouter") ?? []).map(m => [m.id, m]));
|
|
3892
|
+
for (const m of models)
|
|
3893
|
+
if (!existing.has(m.id))
|
|
3894
|
+
existing.set(m.id, { id: m.id, detail: openRouterDetail(m) });
|
|
3895
|
+
map.set("openrouter", [...existing.values()]);
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
finally {
|
|
3899
|
+
ctl.setStatus(null);
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
return map;
|
|
3903
|
+
}
|
|
3904
|
+
function authOptions(choice, providers) {
|
|
3905
|
+
const byId = new Map(providers.map((provider) => [provider.id, provider]));
|
|
3906
|
+
return authProviderIds(choice.providerId)
|
|
3907
|
+
.map((id) => byId.get(id))
|
|
3908
|
+
.filter(isProvider)
|
|
3909
|
+
.filter((provider) => supportsModel(provider, choice.modelId))
|
|
3910
|
+
.map((provider) => ({
|
|
3911
|
+
key: `${provider.id}/${choice.modelId}`,
|
|
3912
|
+
label: authLabel(choice.providerId, provider.id),
|
|
3913
|
+
detail: provider.label ?? provider.id,
|
|
3914
|
+
value: `${provider.id}/${choice.modelId}`,
|
|
3915
|
+
}));
|
|
3916
|
+
}
|
|
3917
|
+
function isProvider(provider) {
|
|
3918
|
+
return provider !== undefined;
|
|
3919
|
+
}
|
|
3920
|
+
function authProviderIds(providerId) {
|
|
3921
|
+
if (providerId === "anthropic")
|
|
3922
|
+
return ["claude-code", "anthropic"];
|
|
3923
|
+
if (providerId === "openai")
|
|
3924
|
+
return ["chatgpt", "openai"];
|
|
3925
|
+
return [providerId];
|
|
3926
|
+
}
|
|
3927
|
+
function authLabel(canonicalProviderId, actualProviderId) {
|
|
3928
|
+
if (canonicalProviderId === "anthropic" && actualProviderId === "claude-code")
|
|
3929
|
+
return "OAuth abbonamento";
|
|
3930
|
+
if (canonicalProviderId === "anthropic" && actualProviderId === "anthropic")
|
|
3931
|
+
return "API key";
|
|
3932
|
+
if (canonicalProviderId === "openai" && actualProviderId === "chatgpt")
|
|
3933
|
+
return "OAuth abbonamento";
|
|
3934
|
+
if (canonicalProviderId === "openai" && actualProviderId === "openai")
|
|
3935
|
+
return "API key";
|
|
3936
|
+
return actualProviderId === "ollama" ? "locale" : "API key";
|
|
3937
|
+
}
|
|
3938
|
+
function authSummary(providerId, providers) {
|
|
3939
|
+
return providers.map((provider) => authLabel(providerId, provider.id)).join(" / ");
|
|
3940
|
+
}
|
|
3941
|
+
function requiresAuthChoice(providerId) {
|
|
3942
|
+
return providerId === "anthropic" || providerId === "openai";
|
|
3943
|
+
}
|
|
3944
|
+
function supportsModel(provider, modelId) {
|
|
3945
|
+
// OpenRouter aggrega centinaia di modelli (catalogo dinamico): accetta
|
|
3946
|
+
// qualsiasi modelId, non solo quelli elencati staticamente nel catalogo.
|
|
3947
|
+
if (provider.id !== "chatgpt" && provider.id !== "claude-code")
|
|
3948
|
+
return true;
|
|
3949
|
+
return provider.models.length === 0 || provider.models.includes(modelId);
|
|
3950
|
+
}
|
|
3951
|
+
function canonicalModel(model) {
|
|
3952
|
+
const slash = model.indexOf("/");
|
|
3953
|
+
if (slash <= 0)
|
|
3954
|
+
return undefined;
|
|
3955
|
+
const providerId = canonicalProviderId(model.slice(0, slash));
|
|
3956
|
+
const modelId = model.slice(slash + 1);
|
|
3957
|
+
return { providerId, modelId, fullModel: `${providerId}/${modelId}` };
|
|
3958
|
+
}
|
|
3959
|
+
function canonicalProviderId(providerId) {
|
|
3960
|
+
if (providerId === "claude-code")
|
|
3961
|
+
return "anthropic";
|
|
3962
|
+
if (providerId === "chatgpt")
|
|
3963
|
+
return "openai";
|
|
3964
|
+
return providerId;
|
|
3965
|
+
}
|
|
3966
|
+
function authHint(providerId) {
|
|
3967
|
+
if (providerId === "anthropic")
|
|
3968
|
+
return " Configura CLAUDE_CODE OAuth con `claude` oppure ANTHROPIC_API_KEY.";
|
|
3969
|
+
if (providerId === "openai")
|
|
3970
|
+
return " Configura ChatGPT OAuth con `codex` oppure OPENAI_API_KEY.";
|
|
3971
|
+
const spec = PROVIDER_CATALOG.find((item) => item.id === providerId);
|
|
3972
|
+
return spec?.envKey ? ` Configura ${spec.envKey}.` : " Verifica la configurazione del provider.";
|
|
3973
|
+
}
|
|
3974
|
+
function unique(values) {
|
|
3975
|
+
return [...new Set(values)];
|
|
3976
|
+
}
|
|
3977
|
+
// Pannello informativo effimero. A differenza di ctl.write non modifica il log
|
|
3978
|
+
// della conversazione: e' pensato per viste operative come /bg, diagnostica e
|
|
3979
|
+
// riepiloghi che servono soltanto mentre l'utente li consulta.
|
|
3980
|
+
async function showReadonlyOverlay(opts) {
|
|
3981
|
+
if (!stdin.isTTY || !stdout.isTTY || typeof stdin.setRawMode !== "function") {
|
|
3982
|
+
opts.ctl.write(`\n ${opts.title}\n${opts.lines.map((line) => ` ${line}`).join("\n")}\n`);
|
|
3983
|
+
return;
|
|
3984
|
+
}
|
|
3985
|
+
readline.emitKeypressEvents(stdin);
|
|
3986
|
+
const wasRaw = stdin.isRaw;
|
|
3987
|
+
const wasPaused = stdin.isPaused();
|
|
3988
|
+
let offset = 0;
|
|
3989
|
+
const pageSize = () => Math.max(3, (stdout.rows || 24) - 7);
|
|
3990
|
+
const renderedLines = () => opts.lines.flatMap((line) => wrapPlain(line || " ", Math.max(12, opts.terminalColumns() - 4)));
|
|
3991
|
+
const maxOffset = () => Math.max(0, renderedLines().length - pageSize());
|
|
3992
|
+
const draw = () => {
|
|
3993
|
+
const width = Math.max(32, opts.terminalColumns());
|
|
3994
|
+
const allLines = renderedLines();
|
|
3995
|
+
const body = allLines.slice(offset, offset + pageSize());
|
|
3996
|
+
const rows = [
|
|
3997
|
+
opts.theme.bold(` ${opts.title}`),
|
|
3998
|
+
opts.theme.dark(` ${"─".repeat(Math.max(8, width - 4))}`),
|
|
3999
|
+
...body.map((line) => ` ${opts.theme.silver(truncate(line, Math.max(12, width - 4)))}`),
|
|
4000
|
+
opts.theme.dark(` ${allLines.length > pageSize() ? `↑/↓ scorri · ${offset + 1}-${Math.min(offset + pageSize(), allLines.length)}/${allLines.length} · ` : ""}Esc/Invio/q chiude`),
|
|
4001
|
+
];
|
|
4002
|
+
opts.ctl.setOverlay(rows);
|
|
4003
|
+
};
|
|
4004
|
+
await new Promise((resolve) => {
|
|
4005
|
+
let settled = false;
|
|
4006
|
+
const cleanup = () => {
|
|
4007
|
+
if (settled)
|
|
4008
|
+
return;
|
|
4009
|
+
settled = true;
|
|
4010
|
+
stdin.off("keypress", onKeypress);
|
|
4011
|
+
stdin.setRawMode(Boolean(wasRaw));
|
|
4012
|
+
if (wasPaused)
|
|
4013
|
+
stdin.pause();
|
|
4014
|
+
opts.ctl.setOverlay(null);
|
|
4015
|
+
opts.ctl.resume();
|
|
4016
|
+
resolve();
|
|
4017
|
+
};
|
|
4018
|
+
const onKeypress = (_chunk, key) => {
|
|
4019
|
+
if (key.name === "down" || (key.ctrl && key.name === "n")) {
|
|
4020
|
+
offset = Math.min(maxOffset(), offset + 1);
|
|
4021
|
+
draw();
|
|
4022
|
+
return;
|
|
4023
|
+
}
|
|
4024
|
+
if (key.name === "up" || (key.ctrl && key.name === "p")) {
|
|
4025
|
+
offset = Math.max(0, offset - 1);
|
|
4026
|
+
draw();
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
if (key.name === "pagedown") {
|
|
4030
|
+
offset = Math.min(maxOffset(), offset + pageSize());
|
|
4031
|
+
draw();
|
|
4032
|
+
return;
|
|
4033
|
+
}
|
|
4034
|
+
if (key.name === "pageup") {
|
|
4035
|
+
offset = Math.max(0, offset - pageSize());
|
|
4036
|
+
draw();
|
|
4037
|
+
return;
|
|
4038
|
+
}
|
|
4039
|
+
if (key.name === "escape" || key.name === "return" || key.name === "q" || (key.ctrl && key.name === "c"))
|
|
4040
|
+
cleanup();
|
|
4041
|
+
};
|
|
4042
|
+
opts.ctl.pause();
|
|
4043
|
+
stdin.setRawMode(true);
|
|
4044
|
+
stdin.resume();
|
|
4045
|
+
stdin.on("keypress", onKeypress);
|
|
4046
|
+
draw();
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
// Parsea i castelli da una stringa: separati da ";" (o newline). Ogni voce e'
|
|
4050
|
+
// "slug: obiettivo" oppure solo l'obiettivo (slug auto c1, c2, ...). Garantisce
|
|
4051
|
+
// slug UNICI (i duplicati ricevono un suffisso -2, -3, ...) e raccoglie warning
|
|
4052
|
+
// per dare feedback chiaro all'utente senza bloccare l'esecuzione.
|
|
4053
|
+
function parseCastles(args) {
|
|
4054
|
+
const pieces = args.split(/[;\n]+/).map((s) => s.trim()).filter(Boolean);
|
|
4055
|
+
const warnings = [];
|
|
4056
|
+
const used = new Set();
|
|
4057
|
+
const castles = pieces.map((piece, i) => {
|
|
4058
|
+
const m = /^([A-Za-z][\w-]{0,23}):\s*(.+)$/.exec(piece);
|
|
4059
|
+
let slug = m && m[2] ? m[1] : `c${i + 1}`;
|
|
4060
|
+
const goal = m && m[2] ? m[2] : piece;
|
|
4061
|
+
// Slug unico: se gia' usato, aggiungi un suffisso progressivo.
|
|
4062
|
+
if (used.has(slug)) {
|
|
4063
|
+
const base = slug;
|
|
4064
|
+
let n = 2;
|
|
4065
|
+
while (used.has(`${base}-${n}`))
|
|
4066
|
+
n++;
|
|
4067
|
+
slug = `${base}-${n}`;
|
|
4068
|
+
warnings.push(`slug "${base}" duplicato, rinominato in "${slug}"`);
|
|
4069
|
+
}
|
|
4070
|
+
used.add(slug);
|
|
4071
|
+
return { slug, goal };
|
|
4072
|
+
});
|
|
4073
|
+
return { castles, warnings };
|
|
4074
|
+
}
|
|
4075
|
+
function kingdomThreshold(env) {
|
|
4076
|
+
const n = Number(env["TALOS_KINGDOM_THRESHOLD"]);
|
|
4077
|
+
return Number.isInteger(n) && n > 0 ? n : 3;
|
|
4078
|
+
}
|
|
4079
|
+
function renderKingdomReport(reports, theme, columns) {
|
|
4080
|
+
const width = Math.max(40, Math.min(96, columns - 4));
|
|
4081
|
+
const ok = reports.filter((r) => r.ok).length;
|
|
4082
|
+
const out = [
|
|
4083
|
+
"",
|
|
4084
|
+
theme.accent(" report al Imperatore") + theme.dim(` ${ok}/${reports.length} gruppi ok`),
|
|
4085
|
+
];
|
|
4086
|
+
for (const r of reports) {
|
|
4087
|
+
const mark = r.ok ? theme.green(" ✓ ") : theme.red(" ✗ ");
|
|
4088
|
+
const head = `${r.slug}` + (r.rooms ? ` · ${r.rooms} attivita` : "") + (r.error ? ` · ${r.error}` : "");
|
|
4089
|
+
out.push(mark + theme.silver(head));
|
|
4090
|
+
const line = (r.summary.split("\n").find((l) => l.trim()) ?? "").slice(0, width - 6);
|
|
4091
|
+
if (line)
|
|
4092
|
+
out.push(theme.dim(` ${line}`));
|
|
4093
|
+
}
|
|
4094
|
+
// Stash da risolvere: rami di vassallo non merge-ati (conflitti) lasciati per
|
|
4095
|
+
// la risoluzione manuale. Elencali in chiaro con i comandi per chiuderli.
|
|
4096
|
+
const conflicts = collectConflicts(reports);
|
|
4097
|
+
if (conflicts.length) {
|
|
4098
|
+
out.push("");
|
|
4099
|
+
out.push(theme.yellow(` ⚠ ${conflicts.length} ramo/i da risolvere a mano (merge non riuscito)`));
|
|
4100
|
+
for (const c of conflicts) {
|
|
4101
|
+
out.push(theme.red(" • ") + theme.silver(c.slug) + theme.dim(` · ${c.branch} · ${c.conflict ? "conflitto" : "errore"}`));
|
|
4102
|
+
out.push(theme.dim(` git merge --no-ff ${c.branch} # risolvi, poi git add -A && git commit`));
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
out.push("");
|
|
4106
|
+
return out.join("\n") + "\n";
|
|
4107
|
+
}
|
|
4108
|
+
function vascendRoomGlyph(status) {
|
|
4109
|
+
if (status === "completed")
|
|
4110
|
+
return "✦";
|
|
4111
|
+
if (status === "running")
|
|
4112
|
+
return "▸";
|
|
4113
|
+
if (status === "failed")
|
|
4114
|
+
return "✗";
|
|
4115
|
+
return "·";
|
|
4116
|
+
}
|
|
4117
|
+
function truncate(value, max) {
|
|
4118
|
+
if (value.length <= max)
|
|
4119
|
+
return value;
|
|
4120
|
+
if (max <= 1)
|
|
4121
|
+
return value.slice(0, max);
|
|
4122
|
+
return `${value.slice(0, max - 1)}…`;
|
|
4123
|
+
}
|
|
4124
|
+
function padRight(value, width) {
|
|
4125
|
+
const pad = width - visibleLength(value);
|
|
4126
|
+
return pad > 0 ? `${value}${" ".repeat(pad)}` : value;
|
|
4127
|
+
}
|
|
4128
|
+
function visibleLength(value) {
|
|
4129
|
+
return value.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
4130
|
+
}
|