@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
|
@@ -0,0 +1,1958 @@
|
|
|
1
|
+
import { tr } from "./i18n.js";
|
|
2
|
+
import { stdin, stdout } from "node:process";
|
|
3
|
+
import { pulseGlyph, pulseToolName } from "./tool-style.js";
|
|
4
|
+
import { fitLine, plainText, terminalWidth, wrapText } from "./terminal-text.js";
|
|
5
|
+
import { PromptQueue } from "./prompt-queue.js";
|
|
6
|
+
import { QueueScreen, queuePreview } from "./queue-screen.js";
|
|
7
|
+
import { createAttachmentHighlighter } from "./attachment-highlight.js";
|
|
8
|
+
import { NativeScrollback } from "./native-scrollback.js";
|
|
9
|
+
import { QueuedQuestions } from "./queued-questions.js";
|
|
10
|
+
const SYNC_ON = "\x1b[?2026h";
|
|
11
|
+
const SYNC_OFF = "\x1b[?2026l";
|
|
12
|
+
const ALT_ON = "\x1b[?1049h";
|
|
13
|
+
const ALT_OFF = "\x1b[?1049l";
|
|
14
|
+
// Lascia click e trascinamento al terminale per la selezione nativa.
|
|
15
|
+
// Alternate scroll converte la rotella in frecce, gestite da onArrow.
|
|
16
|
+
// Disabilita anche eventuali modalita' ereditate da altre applicazioni.
|
|
17
|
+
export const NATIVE_SELECTION_MODE = "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1007h";
|
|
18
|
+
export const NATIVE_SCROLL_MODE = "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1007l";
|
|
19
|
+
// Bracketed paste (DEC 2004): il terminale racchiude il testo incollato tra
|
|
20
|
+
// ESC[200~ ed ESC[201~. Cosi' distinguiamo un INCOLLO da una digitazione: un
|
|
21
|
+
// path/data-URI incollato puo' diventare un allegato "img #N" invece di finire
|
|
22
|
+
// crudo nel buffer (parita' Claude Code).
|
|
23
|
+
const PASTE_ON = "\x1b[?2004h";
|
|
24
|
+
const PASTE_OFF = "\x1b[?2004l";
|
|
25
|
+
const SPINNER = ["✶", "✸", "✺", "✹", "✷", "✦"];
|
|
26
|
+
const MAX_LOG = 5000;
|
|
27
|
+
const SCROLL_FRAME_MS = 16;
|
|
28
|
+
const RESIZE_SETTLE_MS = 80;
|
|
29
|
+
const WHEEL_ARROW_LINES = 2;
|
|
30
|
+
const WHEEL_MOUSE_LINES = 4;
|
|
31
|
+
// Chiusura riga: reset SGR (colori/bold/underline) + chiusura hyperlink OSC 8.
|
|
32
|
+
// Va appesa a OGNI riga renderizzata: se una riga apre un link (fileLink) e
|
|
33
|
+
// viene troncata prima del terminatore, senza questo il sottolineato del link
|
|
34
|
+
// "cola" su tutte le righe successive (bug delle linee orizzontali).
|
|
35
|
+
const LINE_END = "\x1b[0m\x1b]8;;\x1b\\";
|
|
36
|
+
// Helper puro: mantiene il confronto dei frame separato dall'I/O del terminale
|
|
37
|
+
// e rende verificabile che uno spinner non ridisegni l'intera chat.
|
|
38
|
+
export function changedFrameRows(previous, next, force = false) {
|
|
39
|
+
const changed = [];
|
|
40
|
+
for (let i = 0; i < next.length; i += 1) {
|
|
41
|
+
if (force || previous[i] !== next[i])
|
|
42
|
+
changed.push(i);
|
|
43
|
+
}
|
|
44
|
+
return changed;
|
|
45
|
+
}
|
|
46
|
+
export class BottomInput {
|
|
47
|
+
h;
|
|
48
|
+
buffer = "";
|
|
49
|
+
cursor = 0;
|
|
50
|
+
history = [];
|
|
51
|
+
histIdx = -1;
|
|
52
|
+
draft = "";
|
|
53
|
+
active = false;
|
|
54
|
+
paused = false;
|
|
55
|
+
ctrlCConfirmation = null;
|
|
56
|
+
ctrlCTimer;
|
|
57
|
+
running = false;
|
|
58
|
+
questions = new QueuedQuestions(() => this.refresh());
|
|
59
|
+
runAbort;
|
|
60
|
+
queue;
|
|
61
|
+
queueEditor = null;
|
|
62
|
+
// Log della chat (righe gia' formattate) + scroll del viewport (0 = in fondo).
|
|
63
|
+
log = [];
|
|
64
|
+
logIds = [];
|
|
65
|
+
logSeq = 0;
|
|
66
|
+
nativeOutput = new NativeScrollback();
|
|
67
|
+
conversationRenderer;
|
|
68
|
+
conversationColumns = 0;
|
|
69
|
+
nativeScroll;
|
|
70
|
+
alternateScreen = false;
|
|
71
|
+
renderedEntries = [];
|
|
72
|
+
scrollOffset = 0;
|
|
73
|
+
logRevision = 0;
|
|
74
|
+
renderedLogRevision = -1;
|
|
75
|
+
renderedLogCache = [];
|
|
76
|
+
// Fold: intervalli del log collassati in una riga di sintesi (ondate di tool,
|
|
77
|
+
// stile Claude Code "ran N shell commands"). Espandibili con /expand.
|
|
78
|
+
folds = [];
|
|
79
|
+
foldSeq = 0;
|
|
80
|
+
// Viewer dei vassalli (Kingdoms Mode): stream live per-castello, navigabile con
|
|
81
|
+
// Ctrl+G (entra/cicla) ed Esc (esci). vassals=null quando non c'e' un regno.
|
|
82
|
+
vassals = null;
|
|
83
|
+
vassalIdx = 0;
|
|
84
|
+
vassalOpen = false;
|
|
85
|
+
// Board del regno (Kingdoms Mode): righe gia' stilate dal repl, mostrate nella
|
|
86
|
+
// regione viva (sopra l'input) e aggiornate in place — niente spam nella chat.
|
|
87
|
+
kingdomBoard = null;
|
|
88
|
+
overlay = null;
|
|
89
|
+
screen = null;
|
|
90
|
+
// Domanda a risposta libera (tool ask_user): quando attiva, l'editing va alla
|
|
91
|
+
// risposta e Invio/Esc risolvono la promise invece di inviare un prompt.
|
|
92
|
+
modal = null;
|
|
93
|
+
statusLabel = null;
|
|
94
|
+
// Nome GREZZO del tool attualmente in corso (es. "read_file"): pilota colore e
|
|
95
|
+
// glyph animati nella status-line (pulse abbinato alla categoria). null quando
|
|
96
|
+
// l'attivita' non e' un singolo tool (es. "elaboro", ondata mista).
|
|
97
|
+
statusTool = null;
|
|
98
|
+
// Stato VIVO della stanza Danilov corrente (metodo Vascend): invece di
|
|
99
|
+
// sputare ogni mark/avvio in chat (rumoroso), la status-line mostra la stanza
|
|
100
|
+
// attiva e il suo esito, animata. running = punto che pulsa; l'esito
|
|
101
|
+
// (illuminata/fallita) resta per qualche secondo poi si autospegne.
|
|
102
|
+
roomStatus = null;
|
|
103
|
+
roomTimer;
|
|
104
|
+
startedAt = 0;
|
|
105
|
+
tokensNew = 0;
|
|
106
|
+
tokensCache = 0;
|
|
107
|
+
tokensOut = 0;
|
|
108
|
+
frame = 0;
|
|
109
|
+
spinnerTimer;
|
|
110
|
+
lastLayout;
|
|
111
|
+
liveStartRow = 1;
|
|
112
|
+
paintedRows = [];
|
|
113
|
+
paintedColumns = 0;
|
|
114
|
+
paintedHeight = 0;
|
|
115
|
+
paintedCursor = "";
|
|
116
|
+
lastPaintAt = 0;
|
|
117
|
+
scrollRenderTimer;
|
|
118
|
+
quitResolve;
|
|
119
|
+
dataListener;
|
|
120
|
+
resizeListener;
|
|
121
|
+
resizeTimer;
|
|
122
|
+
// Disambiguazione frecce vs scroll (rotella/touchpad): le frecce ravvicinate
|
|
123
|
+
// sono uno scroll, una singola e' una pressione vera (history/cursore).
|
|
124
|
+
pendingArrows = [];
|
|
125
|
+
arrowTimer;
|
|
126
|
+
// Dopo aver riconosciuto una raffica come wheel, le eventuali sequenze
|
|
127
|
+
// residue appartengono allo stesso gesto e non devono raggiungere la history.
|
|
128
|
+
arrowScrollUntil = 0;
|
|
129
|
+
// Accumulatore di un incollo (bracketed paste) arrivato su piu' frame: si
|
|
130
|
+
// riempie finche' non si incontra il terminatore ESC[201~.
|
|
131
|
+
pasteBuffer = null;
|
|
132
|
+
capturingImage = false;
|
|
133
|
+
deferredImageSubmit = null;
|
|
134
|
+
inputGeneration = 0;
|
|
135
|
+
escapePending = "";
|
|
136
|
+
escapeTimer;
|
|
137
|
+
constructor(h) {
|
|
138
|
+
this.h = h;
|
|
139
|
+
this.nativeScroll = h.nativeScroll !== false;
|
|
140
|
+
this.queue = this.createQueue();
|
|
141
|
+
}
|
|
142
|
+
createQueue() {
|
|
143
|
+
return new PromptQueue(this.h.prepareQueuedInput, () => this.render(), this.h.queuePersistence?.(), this.h.restoreQueuedInput);
|
|
144
|
+
}
|
|
145
|
+
restoreQueue() {
|
|
146
|
+
this.inputGeneration++;
|
|
147
|
+
this.queue = this.createQueue();
|
|
148
|
+
this.queueEditor = null;
|
|
149
|
+
if (this.screen instanceof QueueScreen)
|
|
150
|
+
this.screen = null;
|
|
151
|
+
this.render();
|
|
152
|
+
}
|
|
153
|
+
get review() { return this.h.review; }
|
|
154
|
+
get userInbox() { return this.queue.inbox; }
|
|
155
|
+
acceptQueuedMessage(id) {
|
|
156
|
+
const text = this.queue.acknowledge(id);
|
|
157
|
+
if (text)
|
|
158
|
+
this.history.push(text);
|
|
159
|
+
return text;
|
|
160
|
+
}
|
|
161
|
+
pauseQueuedMessages() { if (this.queue.length) {
|
|
162
|
+
this.queue.paused = true;
|
|
163
|
+
this.render();
|
|
164
|
+
} }
|
|
165
|
+
showQueue() {
|
|
166
|
+
if (this.modal || this.overlay || this.queueEditor)
|
|
167
|
+
return;
|
|
168
|
+
this.inputGeneration++;
|
|
169
|
+
if (this.arrowTimer) {
|
|
170
|
+
clearTimeout(this.arrowTimer);
|
|
171
|
+
this.arrowTimer = undefined;
|
|
172
|
+
}
|
|
173
|
+
this.pendingArrows = [];
|
|
174
|
+
this.queue.held = true;
|
|
175
|
+
this.screen = new QueueScreen(this.queue, id => this.editQueuedMessage(id), this.h.queueStyle);
|
|
176
|
+
this.render();
|
|
177
|
+
}
|
|
178
|
+
closeScreen() {
|
|
179
|
+
if (this.screen instanceof QueueScreen)
|
|
180
|
+
this.queue.held = false;
|
|
181
|
+
this.screen = null;
|
|
182
|
+
this.startNextQueued();
|
|
183
|
+
this.render();
|
|
184
|
+
}
|
|
185
|
+
editQueuedMessage(id) {
|
|
186
|
+
const entry = this.queue.get(id);
|
|
187
|
+
if (!entry || !(this.screen instanceof QueueScreen))
|
|
188
|
+
return;
|
|
189
|
+
this.inputGeneration++;
|
|
190
|
+
this.queueEditor = { id, screen: this.screen, draft: { buffer: this.buffer, cursor: this.cursor, histIdx: this.histIdx } };
|
|
191
|
+
this.screen = null;
|
|
192
|
+
this.buffer = entry.text;
|
|
193
|
+
this.cursor = this.buffer.length;
|
|
194
|
+
this.histIdx = -1;
|
|
195
|
+
this.render();
|
|
196
|
+
}
|
|
197
|
+
finishQueueEdit(save, immediate = false) {
|
|
198
|
+
const editor = this.queueEditor;
|
|
199
|
+
if (!editor)
|
|
200
|
+
return;
|
|
201
|
+
if (save) {
|
|
202
|
+
try {
|
|
203
|
+
if (!this.queue.update(editor.id, this.buffer))
|
|
204
|
+
throw new Error("Il messaggio non è più in attesa.");
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
this.reportFailure(error instanceof Error ? error.message : String(error));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
this.buffer = editor.draft.buffer;
|
|
212
|
+
this.cursor = editor.draft.cursor;
|
|
213
|
+
this.histIdx = editor.draft.histIdx;
|
|
214
|
+
this.queueEditor = null;
|
|
215
|
+
this.screen = editor.screen;
|
|
216
|
+
if (immediate && this.queue.sendNow(editor.id)) {
|
|
217
|
+
this.closeScreen();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.render();
|
|
221
|
+
}
|
|
222
|
+
showReview(section) {
|
|
223
|
+
if (!this.h.createReviewScreen || this.modal || this.overlay || this.queueEditor || this.queue.held)
|
|
224
|
+
return;
|
|
225
|
+
this.inputGeneration++;
|
|
226
|
+
if (this.arrowTimer) {
|
|
227
|
+
clearTimeout(this.arrowTimer);
|
|
228
|
+
this.arrowTimer = undefined;
|
|
229
|
+
}
|
|
230
|
+
this.pendingArrows = [];
|
|
231
|
+
this.screen = this.h.createReviewScreen(section);
|
|
232
|
+
this.render();
|
|
233
|
+
}
|
|
234
|
+
get screenVisible() { return this.screen !== null && !this.modal && !this.overlay; }
|
|
235
|
+
static supported() {
|
|
236
|
+
return Boolean(stdout.isTTY && stdin.isTTY && typeof stdin.setRawMode === "function");
|
|
237
|
+
}
|
|
238
|
+
start() {
|
|
239
|
+
if (!BottomInput.supported())
|
|
240
|
+
return Promise.resolve();
|
|
241
|
+
this.active = true;
|
|
242
|
+
this.alternateScreen = !this.nativeScroll;
|
|
243
|
+
stdout.write(this.nativeScroll ? "\x1b[2J\x1b[H" + NATIVE_SCROLL_MODE + PASTE_ON
|
|
244
|
+
: ALT_ON + "\x1b[2J\x1b[H" + NATIVE_SELECTION_MODE + PASTE_ON);
|
|
245
|
+
stdin.setRawMode(true);
|
|
246
|
+
stdin.resume();
|
|
247
|
+
// Parser RAW dello stdin: gestiamo noi mouse e tasti (readline spezzava le
|
|
248
|
+
// sequenze del mouse, facendone digitare i frammenti nell'input).
|
|
249
|
+
this.dataListener = (chunk) => this.feed(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
250
|
+
stdin.on("data", this.dataListener);
|
|
251
|
+
this.resizeListener = () => this.queueResizeRender();
|
|
252
|
+
stdout.on("resize", this.resizeListener);
|
|
253
|
+
this.h.onReady?.();
|
|
254
|
+
this.render(true);
|
|
255
|
+
return new Promise((resolve) => { this.quitResolve = resolve; });
|
|
256
|
+
}
|
|
257
|
+
// Aggiunge contenuto alla CHAT (log) e mantiene il viewport in fondo.
|
|
258
|
+
write(text) {
|
|
259
|
+
this.conversationRenderer = undefined;
|
|
260
|
+
if (!this.active) {
|
|
261
|
+
stdout.write(text);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const lines = text.replace(/\n+$/, "").split("\n");
|
|
265
|
+
for (const l of lines) {
|
|
266
|
+
this.log.push(l);
|
|
267
|
+
this.logIds.push(++this.logSeq);
|
|
268
|
+
}
|
|
269
|
+
this.touchLog();
|
|
270
|
+
// Se stai leggendo piu' in alto (scrollato), MANTIENI la posizione invece di
|
|
271
|
+
// strapparti in fondo; se eri in fondo, resti in fondo.
|
|
272
|
+
if (this.scrollOffset > 0)
|
|
273
|
+
this.scrollOffset += lines.length;
|
|
274
|
+
this.render();
|
|
275
|
+
this.trimLog();
|
|
276
|
+
}
|
|
277
|
+
// Forza un re-render del footer/status-line senza scrivere nel log.
|
|
278
|
+
// Usato per le viste LIVE (es. task in background che cambiano stato).
|
|
279
|
+
refresh() { if (this.active)
|
|
280
|
+
this.render(); }
|
|
281
|
+
// Rebuild a restored conversation from its source on terminal resize. A
|
|
282
|
+
// subsequent live write takes ownership of the transcript again.
|
|
283
|
+
replayConversation(renderer) {
|
|
284
|
+
const columns = this.cols();
|
|
285
|
+
this.write(renderer(columns));
|
|
286
|
+
this.conversationRenderer = renderer;
|
|
287
|
+
this.conversationColumns = columns;
|
|
288
|
+
}
|
|
289
|
+
resetConversationLog() {
|
|
290
|
+
this.log.length = 0;
|
|
291
|
+
this.logIds.length = 0;
|
|
292
|
+
this.logSeq = 0;
|
|
293
|
+
this.folds.length = 0;
|
|
294
|
+
this.renderedEntries = [];
|
|
295
|
+
this.renderedLogCache = [];
|
|
296
|
+
this.touchLog();
|
|
297
|
+
this.paintedRows = [];
|
|
298
|
+
this.paintedCursor = "";
|
|
299
|
+
return this.nativeOutput.reset();
|
|
300
|
+
}
|
|
301
|
+
// Forget the previous session's display and editor state before any replay.
|
|
302
|
+
clearConversation() {
|
|
303
|
+
this.questions.clear();
|
|
304
|
+
this.inputGeneration++;
|
|
305
|
+
this.buffer = "";
|
|
306
|
+
this.cursor = 0;
|
|
307
|
+
this.history.length = 0;
|
|
308
|
+
this.histIdx = -1;
|
|
309
|
+
this.draft = "";
|
|
310
|
+
this.conversationRenderer = undefined;
|
|
311
|
+
this.scrollOffset = 0;
|
|
312
|
+
this.lastLayout = undefined;
|
|
313
|
+
this.screen = null;
|
|
314
|
+
this.overlay = null;
|
|
315
|
+
this.vassals = null;
|
|
316
|
+
this.vassalIdx = 0;
|
|
317
|
+
this.vassalOpen = false;
|
|
318
|
+
this.kingdomBoard = null;
|
|
319
|
+
this.statusLabel = null;
|
|
320
|
+
this.statusTool = null;
|
|
321
|
+
this.roomStatus = null;
|
|
322
|
+
this.tokensNew = this.tokensCache = this.tokensOut = 0;
|
|
323
|
+
if (this.spinnerTimer) {
|
|
324
|
+
clearInterval(this.spinnerTimer);
|
|
325
|
+
this.spinnerTimer = undefined;
|
|
326
|
+
}
|
|
327
|
+
if (this.roomTimer) {
|
|
328
|
+
clearTimeout(this.roomTimer);
|
|
329
|
+
this.roomTimer = undefined;
|
|
330
|
+
}
|
|
331
|
+
if (this.arrowTimer) {
|
|
332
|
+
clearTimeout(this.arrowTimer);
|
|
333
|
+
this.arrowTimer = undefined;
|
|
334
|
+
}
|
|
335
|
+
if (this.scrollRenderTimer) {
|
|
336
|
+
clearTimeout(this.scrollRenderTimer);
|
|
337
|
+
this.scrollRenderTimer = undefined;
|
|
338
|
+
}
|
|
339
|
+
this.pendingArrows = [];
|
|
340
|
+
this.arrowScrollUntil = 0;
|
|
341
|
+
const reset = this.resetConversationLog();
|
|
342
|
+
if (this.active) {
|
|
343
|
+
if (this.nativeScroll && this.alternateScreen) {
|
|
344
|
+
stdout.write(ALT_OFF + NATIVE_SCROLL_MODE);
|
|
345
|
+
this.alternateScreen = false;
|
|
346
|
+
}
|
|
347
|
+
stdout.write(reset);
|
|
348
|
+
this.render(true);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Sostituisce le ULTIME `count` righe del log con `lines`, poi re-renderizza
|
|
352
|
+
// in place. Pensato per le animazioni dello scrollback (es. intro): si scrive
|
|
353
|
+
// una volta il blocco con write(), poi si aggiorna lo stesso blocco frame per
|
|
354
|
+
// frame senza accodare nuove righe. Se non e' attivo, scrive su stdout grezzo.
|
|
355
|
+
replaceTail(lines, count) {
|
|
356
|
+
if (!this.active) {
|
|
357
|
+
stdout.write(lines.join("\n") + "\n");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const n = Math.min(count, this.log.length);
|
|
361
|
+
const ids = this.logIds.splice(this.log.length - n, n);
|
|
362
|
+
if (n > 0)
|
|
363
|
+
this.log.splice(this.log.length - n, n);
|
|
364
|
+
for (const [i, line] of lines.entries()) {
|
|
365
|
+
this.log.push(line);
|
|
366
|
+
this.logIds.push(ids[i] ?? ++this.logSeq);
|
|
367
|
+
}
|
|
368
|
+
this.touchLog();
|
|
369
|
+
this.render();
|
|
370
|
+
this.trimLog();
|
|
371
|
+
}
|
|
372
|
+
// Riscrive in place una SINGOLA riga del log (per indice assoluto), poi
|
|
373
|
+
// re-renderizza. Pensato per l'animazione del bullet ● del tool in corso nello
|
|
374
|
+
// scrollback: si conosce l'indice della riga "● Nome(arg)" e si aggiorna solo
|
|
375
|
+
// quella frame per frame, senza accodare nulla. No-op se l'indice e' fuori range.
|
|
376
|
+
setLogLine(index, text) {
|
|
377
|
+
if (index < 0 || index >= this.log.length)
|
|
378
|
+
return;
|
|
379
|
+
if (this.log[index] === text)
|
|
380
|
+
return;
|
|
381
|
+
this.log[index] = text;
|
|
382
|
+
this.touchLog();
|
|
383
|
+
if (this.active)
|
|
384
|
+
this.render();
|
|
385
|
+
}
|
|
386
|
+
setTokens(newInput, cache, outTok) {
|
|
387
|
+
this.tokensNew = newInput;
|
|
388
|
+
this.tokensCache = cache;
|
|
389
|
+
this.tokensOut = outTok;
|
|
390
|
+
if (this.active && this.statusLabel !== null)
|
|
391
|
+
this.render();
|
|
392
|
+
}
|
|
393
|
+
setStatus(label) {
|
|
394
|
+
this.statusLabel = label;
|
|
395
|
+
if (label !== null) {
|
|
396
|
+
if (!this.spinnerTimer)
|
|
397
|
+
this.spinnerTimer = setInterval(() => { this.frame += 1; this.render(); }, 140);
|
|
398
|
+
}
|
|
399
|
+
else if (this.spinnerTimer) {
|
|
400
|
+
clearInterval(this.spinnerTimer);
|
|
401
|
+
this.spinnerTimer = undefined;
|
|
402
|
+
}
|
|
403
|
+
if (this.active)
|
|
404
|
+
this.render();
|
|
405
|
+
}
|
|
406
|
+
getLogLine(index) { return this.log[index]; }
|
|
407
|
+
// Stato leggibile dall'host per evitare una seconda telemetria statica mentre
|
|
408
|
+
// la riga animata del run sta gia' mostrando token, tempo e coda.
|
|
409
|
+
isBusy() {
|
|
410
|
+
return this.running || this.statusLabel !== null;
|
|
411
|
+
}
|
|
412
|
+
// Nome del tool in corso per l'animazione della status-line. null = attivita'
|
|
413
|
+
// generica (spinner neutro). Non forza un timer: lo gestisce gia' setStatus.
|
|
414
|
+
setStatusTool(name) {
|
|
415
|
+
this.statusTool = name;
|
|
416
|
+
if (this.active && this.statusLabel !== null)
|
|
417
|
+
this.render();
|
|
418
|
+
}
|
|
419
|
+
// Stato della stanza Danilov (Vascend) per la status-line: niente piu' righe
|
|
420
|
+
// in chat per avvio/illuminazione. `running` resta finche' la stanza lavora;
|
|
421
|
+
// `done` si spegne dopo 3s; `failed` resta fino al prossimo invio o avviso.
|
|
422
|
+
// null lo rimuove subito.
|
|
423
|
+
setRoomStatus(label, state = "running") {
|
|
424
|
+
if (this.roomTimer) {
|
|
425
|
+
clearTimeout(this.roomTimer);
|
|
426
|
+
this.roomTimer = undefined;
|
|
427
|
+
}
|
|
428
|
+
if (!label) {
|
|
429
|
+
this.roomStatus = null;
|
|
430
|
+
}
|
|
431
|
+
else if (state === "running" || state === "failed") {
|
|
432
|
+
this.roomStatus = { label, state };
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
this.roomStatus = { label, state, until: Date.now() + 3000 };
|
|
436
|
+
this.roomTimer = setTimeout(() => {
|
|
437
|
+
// Spegni solo se non e' arrivata nel frattempo una nuova stanza running.
|
|
438
|
+
if (this.roomStatus?.state === "done") {
|
|
439
|
+
this.roomStatus = null;
|
|
440
|
+
if (this.active)
|
|
441
|
+
this.render();
|
|
442
|
+
}
|
|
443
|
+
this.roomTimer = undefined;
|
|
444
|
+
}, 3000);
|
|
445
|
+
}
|
|
446
|
+
// La status-line anima solo col timer dello spinner: se non c'e' un run in
|
|
447
|
+
// corso (statusLabel null) accendiamo un refresh leggero per il pulse della
|
|
448
|
+
// stanza running.
|
|
449
|
+
if (this.active)
|
|
450
|
+
this.render();
|
|
451
|
+
}
|
|
452
|
+
reportFailure(message) {
|
|
453
|
+
const detail = plainText(message).trim();
|
|
454
|
+
this.h.review?.record({ type: "error", message: detail });
|
|
455
|
+
this.setRoomStatus(detail, "failed");
|
|
456
|
+
}
|
|
457
|
+
setOverlay(lines) {
|
|
458
|
+
this.overlay = lines && lines.length
|
|
459
|
+
? { lines, cursorRow: lines.length - 1, cursorCol: 1 }
|
|
460
|
+
: null;
|
|
461
|
+
if (!this.overlay)
|
|
462
|
+
this.startNextQueued();
|
|
463
|
+
if (this.active)
|
|
464
|
+
this.render();
|
|
465
|
+
}
|
|
466
|
+
// Lunghezza attuale del log (indice di inserimento): per delimitare un'ondata.
|
|
467
|
+
logLength() { return this.log.length; }
|
|
468
|
+
// Collassa [start, end) del log in una riga di sintesi. Ritorna l'id del fold.
|
|
469
|
+
fold(start, end, summary) {
|
|
470
|
+
const id = (this.foldSeq += 1);
|
|
471
|
+
this.folds.push({ id, start, end, summary, expanded: false, open: false });
|
|
472
|
+
this.touchLog();
|
|
473
|
+
if (this.active)
|
|
474
|
+
this.render();
|
|
475
|
+
return id;
|
|
476
|
+
}
|
|
477
|
+
// Fold APERTO: copre da `start` fino alla fine corrente del log e si estende da
|
|
478
|
+
// solo man mano che arrivano nuove righe (cosi' durante un'ondata di tool la
|
|
479
|
+
// chat mostra UNA riga di sintesi che si aggiorna, niente duplicati).
|
|
480
|
+
openFold(start, summary) {
|
|
481
|
+
const id = (this.foldSeq += 1);
|
|
482
|
+
this.folds.push({ id, start, end: start, summary, expanded: false, open: true });
|
|
483
|
+
this.touchLog();
|
|
484
|
+
if (this.active)
|
|
485
|
+
this.render();
|
|
486
|
+
return id;
|
|
487
|
+
}
|
|
488
|
+
updateFoldSummary(id, summary) {
|
|
489
|
+
const f = this.folds.find((x) => x.id === id);
|
|
490
|
+
if (!f)
|
|
491
|
+
return;
|
|
492
|
+
if (f.summary === summary)
|
|
493
|
+
return;
|
|
494
|
+
f.summary = summary;
|
|
495
|
+
this.touchLog();
|
|
496
|
+
if (this.active)
|
|
497
|
+
this.render();
|
|
498
|
+
}
|
|
499
|
+
// Aggiorna fine + sintesi di un fold (chiuso). Usato per collassare i tool
|
|
500
|
+
// GIA' finiti di un'ondata lasciando visibile quello in corso.
|
|
501
|
+
updateFold(id, end, summary) {
|
|
502
|
+
const f = this.folds.find((x) => x.id === id);
|
|
503
|
+
if (!f)
|
|
504
|
+
return;
|
|
505
|
+
f.end = end;
|
|
506
|
+
f.summary = summary;
|
|
507
|
+
f.open = false;
|
|
508
|
+
this.touchLog();
|
|
509
|
+
if (this.active)
|
|
510
|
+
this.render();
|
|
511
|
+
}
|
|
512
|
+
// Apre/chiude (toggle) un fold: per id, o il piu' recente. Ritorna true se
|
|
513
|
+
// c'era un fold su cui agire.
|
|
514
|
+
toggleFold(id) {
|
|
515
|
+
const target = id !== undefined ? this.folds.find((f) => f.id === id) : this.folds[this.folds.length - 1];
|
|
516
|
+
if (!target)
|
|
517
|
+
return false;
|
|
518
|
+
if (!target.expanded && this.reprintArchivedFold(target))
|
|
519
|
+
return true;
|
|
520
|
+
target.expanded = !target.expanded;
|
|
521
|
+
this.touchLog();
|
|
522
|
+
if (this.active)
|
|
523
|
+
this.render();
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
// Chiude un fold aperto: ne fissa la fine (cosi' il contenuto successivo, es.
|
|
527
|
+
// il testo dell'agente, non viene piu' nascosto).
|
|
528
|
+
closeFold(id, summary) {
|
|
529
|
+
const f = this.folds.find((x) => x.id === id);
|
|
530
|
+
if (!f)
|
|
531
|
+
return;
|
|
532
|
+
f.open = false;
|
|
533
|
+
f.end = this.log.length;
|
|
534
|
+
if (summary !== undefined)
|
|
535
|
+
f.summary = summary;
|
|
536
|
+
this.touchLog();
|
|
537
|
+
if (this.active)
|
|
538
|
+
this.render();
|
|
539
|
+
}
|
|
540
|
+
// Espande un fold (per id, o il piu' recente non espanso). Ritorna true se uno
|
|
541
|
+
// e' stato espanso.
|
|
542
|
+
expandFold(id) {
|
|
543
|
+
const target = id !== undefined
|
|
544
|
+
? this.folds.find((f) => f.id === id && !f.expanded)
|
|
545
|
+
: [...this.folds].reverse().find((f) => !f.expanded);
|
|
546
|
+
if (!target)
|
|
547
|
+
return false;
|
|
548
|
+
if (this.reprintArchivedFold(target))
|
|
549
|
+
return true;
|
|
550
|
+
target.expanded = true;
|
|
551
|
+
this.touchLog();
|
|
552
|
+
if (this.active)
|
|
553
|
+
this.render();
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
// Rimuove DAVVERO un fold e le righe di log che copriva: il blocco sparisce
|
|
557
|
+
// dalla chat (niente segnaposto), come si aspetta l'utente quando chiude un
|
|
558
|
+
// output che non vuole piu' vedere. Gli start/end dei fold successivi vengono
|
|
559
|
+
// ricompattati. Ritorna true se il fold esisteva.
|
|
560
|
+
removeFold(id) {
|
|
561
|
+
const idx = this.folds.findIndex((f) => f.id === id);
|
|
562
|
+
if (idx < 0)
|
|
563
|
+
return false;
|
|
564
|
+
const f = this.folds[idx];
|
|
565
|
+
const start = f.start;
|
|
566
|
+
const end = f.open ? this.log.length : f.end;
|
|
567
|
+
const removed = Math.max(0, end - start);
|
|
568
|
+
this.folds.splice(idx, 1);
|
|
569
|
+
if (removed > 0) {
|
|
570
|
+
this.log.splice(start, removed);
|
|
571
|
+
this.logIds.splice(start, removed);
|
|
572
|
+
// Ricompatta gli intervalli dei fold rimasti che stavano dopo la regione.
|
|
573
|
+
for (const o of this.folds) {
|
|
574
|
+
if (o.start >= end)
|
|
575
|
+
o.start -= removed;
|
|
576
|
+
if (o.end >= end)
|
|
577
|
+
o.end -= removed;
|
|
578
|
+
else if (o.end > start)
|
|
579
|
+
o.end = start;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
this.touchLog();
|
|
583
|
+
if (this.active)
|
|
584
|
+
this.render();
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
// Espande tutti i fold. Ritorna quanti ne ha espansi.
|
|
588
|
+
expandAllFolds() {
|
|
589
|
+
let n = 0;
|
|
590
|
+
for (const f of [...this.folds])
|
|
591
|
+
if (!f.expanded) {
|
|
592
|
+
if (!this.reprintArchivedFold(f))
|
|
593
|
+
f.expanded = true;
|
|
594
|
+
n += 1;
|
|
595
|
+
}
|
|
596
|
+
if (n)
|
|
597
|
+
this.touchLog();
|
|
598
|
+
if (n && this.active)
|
|
599
|
+
this.render();
|
|
600
|
+
return n;
|
|
601
|
+
}
|
|
602
|
+
reprintArchivedFold(fold) {
|
|
603
|
+
if (!this.nativeScroll || fold.open || !this.nativeOutput.isCommitted(this.logIds[fold.start] ?? Infinity))
|
|
604
|
+
return false;
|
|
605
|
+
const details = this.log.slice(fold.start, fold.end);
|
|
606
|
+
fold.expanded = true;
|
|
607
|
+
fold.start = this.log.length;
|
|
608
|
+
fold.end = fold.start + 1 + details.length;
|
|
609
|
+
this.write([fold.summary, ...details].join("\n"));
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
trimLog() {
|
|
613
|
+
// Native scrollback owns committed rows. The alternate viewport needs the
|
|
614
|
+
// complete session in memory so a resumed chat remains scrollable in full.
|
|
615
|
+
if (!this.nativeScroll)
|
|
616
|
+
return;
|
|
617
|
+
const count = this.log.length - MAX_LOG;
|
|
618
|
+
if (count <= 0 || (this.nativeScroll && (this.alternateScreen || this.paused)))
|
|
619
|
+
return;
|
|
620
|
+
this.log.splice(0, count);
|
|
621
|
+
this.logIds.splice(0, count);
|
|
622
|
+
for (let i = this.folds.length - 1; i >= 0; i--) {
|
|
623
|
+
const fold = this.folds[i];
|
|
624
|
+
fold.start = Math.max(0, fold.start - count);
|
|
625
|
+
fold.end = Math.max(0, fold.end - count);
|
|
626
|
+
if (!fold.open && fold.end <= fold.start)
|
|
627
|
+
this.folds.splice(i, 1);
|
|
628
|
+
}
|
|
629
|
+
this.touchLog();
|
|
630
|
+
}
|
|
631
|
+
// Log con i fold applicati: sostituisce gli intervalli collassati con la sintesi.
|
|
632
|
+
renderedLog() {
|
|
633
|
+
if (this.renderedLogRevision === this.logRevision)
|
|
634
|
+
return this.renderedLogCache;
|
|
635
|
+
const active = this.folds.filter((f) => !f.expanded).sort((a, b) => a.start - b.start);
|
|
636
|
+
if (!active.length) {
|
|
637
|
+
this.renderedEntries = this.log.map((text, index) => ({ text, id: this.logIds[index] }));
|
|
638
|
+
this.renderedLogRevision = this.logRevision;
|
|
639
|
+
this.renderedLogCache = this.log;
|
|
640
|
+
return this.renderedLogCache;
|
|
641
|
+
}
|
|
642
|
+
const out = [];
|
|
643
|
+
const entries = [];
|
|
644
|
+
let i = 0;
|
|
645
|
+
let fi = 0;
|
|
646
|
+
while (i < this.log.length) {
|
|
647
|
+
let f = active[fi];
|
|
648
|
+
while (f && f.start < i) {
|
|
649
|
+
fi += 1;
|
|
650
|
+
f = active[fi];
|
|
651
|
+
}
|
|
652
|
+
if (f && i === f.start) {
|
|
653
|
+
const end = f.open ? this.log.length : f.end; // fold aperto: fino alla fine
|
|
654
|
+
if (end > f.start) {
|
|
655
|
+
out.push(f.summary);
|
|
656
|
+
entries.push({ text: f.summary, id: this.logIds[i], through: this.logIds[Math.min(end, this.log.length) - 1] });
|
|
657
|
+
i = end;
|
|
658
|
+
fi += 1;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
out.push(this.log[i] ?? "");
|
|
663
|
+
entries.push({ text: this.log[i] ?? "", id: this.logIds[i] });
|
|
664
|
+
i += 1;
|
|
665
|
+
}
|
|
666
|
+
this.renderedLogRevision = this.logRevision;
|
|
667
|
+
this.renderedLogCache = out;
|
|
668
|
+
this.renderedEntries = entries;
|
|
669
|
+
return this.renderedLogCache;
|
|
670
|
+
}
|
|
671
|
+
touchLog() {
|
|
672
|
+
this.logRevision += 1;
|
|
673
|
+
}
|
|
674
|
+
pause() {
|
|
675
|
+
if (this.paused)
|
|
676
|
+
return;
|
|
677
|
+
this.paused = true;
|
|
678
|
+
this.inputGeneration++;
|
|
679
|
+
this.clearPendingEscape();
|
|
680
|
+
if (this.nativeScroll) {
|
|
681
|
+
if (this.alternateScreen) {
|
|
682
|
+
stdout.write(ALT_OFF);
|
|
683
|
+
this.alternateScreen = false;
|
|
684
|
+
}
|
|
685
|
+
stdout.write(this.nativeOutput.release());
|
|
686
|
+
}
|
|
687
|
+
this.cancelCtrlC();
|
|
688
|
+
if (this.dataListener)
|
|
689
|
+
stdin.off("data", this.dataListener);
|
|
690
|
+
if (this.spinnerTimer) {
|
|
691
|
+
clearInterval(this.spinnerTimer);
|
|
692
|
+
this.spinnerTimer = undefined;
|
|
693
|
+
}
|
|
694
|
+
if (this.scrollRenderTimer) {
|
|
695
|
+
clearTimeout(this.scrollRenderTimer);
|
|
696
|
+
this.scrollRenderTimer = undefined;
|
|
697
|
+
}
|
|
698
|
+
this.paintedRows = [];
|
|
699
|
+
this.paintedCursor = "";
|
|
700
|
+
}
|
|
701
|
+
resume() {
|
|
702
|
+
if (!this.active)
|
|
703
|
+
return;
|
|
704
|
+
this.paused = false;
|
|
705
|
+
stdout.write(this.nativeScroll ? NATIVE_SCROLL_MODE : NATIVE_SELECTION_MODE);
|
|
706
|
+
if (this.dataListener)
|
|
707
|
+
stdin.on("data", this.dataListener);
|
|
708
|
+
if (typeof stdin.setRawMode === "function")
|
|
709
|
+
stdin.setRawMode(true);
|
|
710
|
+
stdin.resume();
|
|
711
|
+
if (this.statusLabel !== null && !this.spinnerTimer) {
|
|
712
|
+
this.spinnerTimer = setInterval(() => { this.frame += 1; this.render(); }, 140);
|
|
713
|
+
}
|
|
714
|
+
this.render(true);
|
|
715
|
+
}
|
|
716
|
+
// Dati dei vassalli per il viewer (Kingdoms Mode). null = nessun regno: chiude.
|
|
717
|
+
setKingdomVassals(data) {
|
|
718
|
+
if (!data || !data.length) {
|
|
719
|
+
this.vassals = null;
|
|
720
|
+
this.vassalOpen = false;
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
this.vassals = data.map((d) => ({ slug: d.slug, lines: d.lines }));
|
|
724
|
+
if (this.vassalIdx >= this.vassals.length)
|
|
725
|
+
this.vassalIdx = 0;
|
|
726
|
+
}
|
|
727
|
+
if (this.active && (this.vassalOpen || !data))
|
|
728
|
+
this.render();
|
|
729
|
+
}
|
|
730
|
+
// Board live del regno: panoramica di tutti i vassalli, aggiornata in place
|
|
731
|
+
// nella regione viva. null = nessun regno (la rimuove). Le righe arrivano gia'
|
|
732
|
+
// stilate dal repl. Non si mostra mentre si e' dentro il viewer di un vassallo.
|
|
733
|
+
setKingdomBoard(lines) {
|
|
734
|
+
this.kingdomBoard = lines && lines.length ? lines.slice() : null;
|
|
735
|
+
if (this.active && !this.vassalOpen)
|
|
736
|
+
this.render();
|
|
737
|
+
}
|
|
738
|
+
// true se la board live del regno e' pinned ora: il chiamante puo' evitare di
|
|
739
|
+
// scrivere ANCHE il riquadro inline del piano, che duplicherebbe la vista.
|
|
740
|
+
hasKingdomBoard() {
|
|
741
|
+
return this.kingdomBoard !== null;
|
|
742
|
+
}
|
|
743
|
+
requestQuit() { this.quit(); }
|
|
744
|
+
cols() { return stdout.columns || 80; }
|
|
745
|
+
rows() { return stdout.rows || 24; }
|
|
746
|
+
queueResizeRender() {
|
|
747
|
+
this.nativeOutput.invalidate();
|
|
748
|
+
if (this.resizeTimer)
|
|
749
|
+
clearTimeout(this.resizeTimer);
|
|
750
|
+
this.resizeTimer = setTimeout(() => {
|
|
751
|
+
this.resizeTimer = undefined;
|
|
752
|
+
this.render(true);
|
|
753
|
+
}, RESIZE_SETTLE_MS);
|
|
754
|
+
}
|
|
755
|
+
// ── rendering full-frame ────────────────────────────────────────────────────
|
|
756
|
+
liveContent() {
|
|
757
|
+
if (this.overlay) {
|
|
758
|
+
return {
|
|
759
|
+
lines: this.overlay.lines, cursorRow: this.overlay.cursorRow, cursorCol: this.overlay.cursorCol,
|
|
760
|
+
inputStart: 0, inputRows: 0, textWidth: 0, prefixCols: 0, rowStarts: [], rowLengths: [],
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
// Viewer vassallo: occupa la regione viva con il tail dello stream del castello.
|
|
764
|
+
if (this.vassalOpen && this.vassals && this.vassals.length) {
|
|
765
|
+
const v = this.vassals[this.vassalIdx] ?? this.vassals[0];
|
|
766
|
+
const style = this.h.statusStyle ?? ((s) => s);
|
|
767
|
+
const maxLines = Math.max(3, this.rows() - 3);
|
|
768
|
+
const tail = v.lines.slice(-maxLines);
|
|
769
|
+
const header = ` ◆ collaboratore ${this.vassalIdx + 1}/${this.vassals.length} · ${v.slug} · Ctrl+G prossimo · Esc esci`;
|
|
770
|
+
const lines = [style(header), ...(tail.length ? tail : [style(" (in attesa di attività…)")])];
|
|
771
|
+
return { lines, cursorRow: 0, cursorCol: 1, inputStart: 0, inputRows: 0, textWidth: 0, prefixCols: 0, rowStarts: [], rowLengths: [] };
|
|
772
|
+
}
|
|
773
|
+
return this.layout();
|
|
774
|
+
}
|
|
775
|
+
render(force = false) {
|
|
776
|
+
if (!this.active || this.resizeTimer || (this.paused && !this.overlay))
|
|
777
|
+
return;
|
|
778
|
+
if (this.scrollRenderTimer) {
|
|
779
|
+
clearTimeout(this.scrollRenderTimer);
|
|
780
|
+
this.scrollRenderTimer = undefined;
|
|
781
|
+
}
|
|
782
|
+
const H = this.rows();
|
|
783
|
+
const W = this.cols();
|
|
784
|
+
const alternate = !this.nativeScroll || this.screenVisible || this.overlay !== null || this.vassalOpen || this.scrollOffset > 0;
|
|
785
|
+
if (alternate !== this.alternateScreen) {
|
|
786
|
+
stdout.write(alternate ? ALT_ON + "\x1b[2J\x1b[H" + NATIVE_SELECTION_MODE : ALT_OFF + NATIVE_SCROLL_MODE);
|
|
787
|
+
this.alternateScreen = alternate;
|
|
788
|
+
this.paintedRows = [];
|
|
789
|
+
this.paintedCursor = "";
|
|
790
|
+
force = true;
|
|
791
|
+
}
|
|
792
|
+
if (this.screenVisible) {
|
|
793
|
+
const content = this.screen.render(W, H);
|
|
794
|
+
const rows = Array.from({ length: H }, (_, i) => fitLine(content[i] ?? "", Math.max(1, W - 1)));
|
|
795
|
+
this.paint(rows, W, H, 1, 1, force, true);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (this.conversationRenderer && this.conversationColumns !== W && (!this.nativeScroll || !alternate)) {
|
|
799
|
+
const text = this.conversationRenderer(W);
|
|
800
|
+
const reset = this.resetConversationLog();
|
|
801
|
+
for (const line of text.replace(/\n+$/, "").split("\n")) {
|
|
802
|
+
this.log.push(line);
|
|
803
|
+
this.logIds.push(++this.logSeq);
|
|
804
|
+
}
|
|
805
|
+
this.conversationColumns = W;
|
|
806
|
+
if (this.nativeScroll)
|
|
807
|
+
stdout.write(reset);
|
|
808
|
+
force = true;
|
|
809
|
+
}
|
|
810
|
+
const lay = this.liveContent();
|
|
811
|
+
this.lastLayout = lay;
|
|
812
|
+
if (this.nativeScroll && !alternate) {
|
|
813
|
+
this.renderedLog();
|
|
814
|
+
const output = this.nativeOutput.render(this.renderedEntries, lay, W, H, false, this.h.startupLines?.() ?? this.h.headerLines?.() ?? []);
|
|
815
|
+
if (output)
|
|
816
|
+
stdout.write(output);
|
|
817
|
+
this.lastPaintAt = Date.now();
|
|
818
|
+
this.trimLog();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const liveH = Math.min(lay.lines.length, H - 1);
|
|
822
|
+
const requestedHeader = this.h.headerLines?.() ?? [];
|
|
823
|
+
const headerH = Math.min(requestedHeader.length, Math.max(0, H - liveH - 1));
|
|
824
|
+
const header = requestedHeader.slice(0, headerH);
|
|
825
|
+
const chatRows = Math.max(1, H - liveH - headerH);
|
|
826
|
+
const rendered = this.renderedLog();
|
|
827
|
+
const total = rendered.length;
|
|
828
|
+
const maxOffset = Math.max(0, total - chatRows);
|
|
829
|
+
if (this.scrollOffset > maxOffset)
|
|
830
|
+
this.scrollOffset = maxOffset;
|
|
831
|
+
const startIdx = Math.max(0, total - chatRows - this.scrollOffset);
|
|
832
|
+
const visible = rendered.slice(startIdx, startIdx + chatRows);
|
|
833
|
+
const frameRows = Array.from({ length: H }, () => "");
|
|
834
|
+
for (let i = 0; i < headerH; i += 1)
|
|
835
|
+
frameRows[i] = clipVisible(header[i] ?? "", W);
|
|
836
|
+
for (let i = 0; i < chatRows; i += 1) {
|
|
837
|
+
let line = visible[i] ?? "";
|
|
838
|
+
// Indicatore "altro sopra/sotto" sulla prima/ultima riga del viewport.
|
|
839
|
+
if (i === 0 && this.scrollOffset < maxOffset)
|
|
840
|
+
line = scrollHint(line, "↑ altro sopra", W);
|
|
841
|
+
frameRows[headerH + i] = clipVisible(line, W);
|
|
842
|
+
}
|
|
843
|
+
this.liveStartRow = headerH + chatRows + 1;
|
|
844
|
+
for (let i = 0; i < liveH; i += 1) {
|
|
845
|
+
frameRows[headerH + chatRows + i] = clipVisible(lay.lines[i] ?? "", W);
|
|
846
|
+
}
|
|
847
|
+
const cursorRow = Math.max(1, Math.min(H, this.liveStartRow + Math.min(lay.cursorRow, Math.max(0, liveH - 1))));
|
|
848
|
+
const cursorCol = Math.max(1, Math.min(W, lay.cursorCol));
|
|
849
|
+
this.paint(frameRows, W, H, cursorRow, cursorCol, force, this.overlay !== null);
|
|
850
|
+
}
|
|
851
|
+
paint(frameRows, W, H, cursorRow, cursorCol, force, hideCursor = false) {
|
|
852
|
+
const dimensionsChanged = W !== this.paintedColumns || H !== this.paintedHeight;
|
|
853
|
+
const full = force || dimensionsChanged || this.paintedRows.length !== frameRows.length;
|
|
854
|
+
const changed = changedFrameRows(this.paintedRows, frameRows, full);
|
|
855
|
+
const cursor = `${cursorRow}:${cursorCol}:${hideCursor}`;
|
|
856
|
+
if (!changed.length && cursor === this.paintedCursor)
|
|
857
|
+
return;
|
|
858
|
+
let out = SYNC_ON;
|
|
859
|
+
for (const index of changed) {
|
|
860
|
+
out += `\x1b[${index + 1};1H\x1b[2K` + (frameRows[index] ?? "") + LINE_END;
|
|
861
|
+
}
|
|
862
|
+
out += `\x1b[${cursorRow};${cursorCol}H` + (hideCursor ? "\x1b[?25l" : "\x1b[?25h");
|
|
863
|
+
out += SYNC_OFF;
|
|
864
|
+
stdout.write(out);
|
|
865
|
+
this.paintedRows = frameRows;
|
|
866
|
+
this.paintedColumns = W;
|
|
867
|
+
this.paintedHeight = H;
|
|
868
|
+
this.paintedCursor = cursor;
|
|
869
|
+
this.lastPaintAt = Date.now();
|
|
870
|
+
}
|
|
871
|
+
scrollBy(delta) {
|
|
872
|
+
const next = Math.max(0, this.scrollOffset + delta);
|
|
873
|
+
if (next === this.scrollOffset)
|
|
874
|
+
return;
|
|
875
|
+
this.scrollOffset = next;
|
|
876
|
+
this.queueScrollRender();
|
|
877
|
+
}
|
|
878
|
+
// Touchpad e rotelle ad alta risoluzione possono inviare decine di eventi in
|
|
879
|
+
// pochi millisecondi. Lo stato avanza subito, ma il terminale riceve al massimo
|
|
880
|
+
// un frame per intervallo visivo: niente coda ANSI che continua a smaltirsi
|
|
881
|
+
// quando il gesto e' gia' terminato.
|
|
882
|
+
queueScrollRender() {
|
|
883
|
+
if (!this.active || this.scrollRenderTimer)
|
|
884
|
+
return;
|
|
885
|
+
const elapsed = Date.now() - this.lastPaintAt;
|
|
886
|
+
const delay = Math.max(0, SCROLL_FRAME_MS - elapsed);
|
|
887
|
+
this.scrollRenderTimer = setTimeout(() => {
|
|
888
|
+
this.scrollRenderTimer = undefined;
|
|
889
|
+
this.render();
|
|
890
|
+
}, delay);
|
|
891
|
+
}
|
|
892
|
+
layout() {
|
|
893
|
+
const cols = this.cols();
|
|
894
|
+
const lines = [];
|
|
895
|
+
if (this.statusLabel !== null)
|
|
896
|
+
lines.push(this.statusLine());
|
|
897
|
+
if (this.statusLabel === null && this.roomStatus?.state === "failed") {
|
|
898
|
+
const notice = this.roomSegment().replace(/\s+/g, " ").trim();
|
|
899
|
+
lines.push(...wrapText(notice, Math.max(8, cols - 3)).slice(0, 3).map(line => ` ${line}`));
|
|
900
|
+
lines.push(fitLine(tr(" Ctrl+R dettagli", " Ctrl+R details"), cols - 1));
|
|
901
|
+
}
|
|
902
|
+
// La riga live descrive gia' il team; la board completa si consulta a parte.
|
|
903
|
+
if (this.kingdomBoard && this.statusLabel === null)
|
|
904
|
+
lines.push(fitLine(this.kingdomBoard[0] ?? "", cols - 1));
|
|
905
|
+
for (const f of this.h.footerLines())
|
|
906
|
+
lines.push(f);
|
|
907
|
+
if (!this.modal && !this.queueEditor)
|
|
908
|
+
lines.push(...queuePreview(this.queue, cols, this.h.queueStyle, !this.running));
|
|
909
|
+
const questionStart = lines.length;
|
|
910
|
+
const questions = !this.modal && !this.queueEditor
|
|
911
|
+
? this.questions.render(cols, Math.max(5, this.rows() - lines.length - 6), this.h.queueStyle)
|
|
912
|
+
: { lines: [], cursorRow: 0, cursorCol: 1 };
|
|
913
|
+
lines.push(...questions.lines);
|
|
914
|
+
const box = this.h.boxStyle ?? ((s) => s);
|
|
915
|
+
// Etichetta della domanda a risposta libera (modal) sopra il box.
|
|
916
|
+
if (this.modal) {
|
|
917
|
+
const style = this.h.statusStyle ?? ((s) => s);
|
|
918
|
+
lines.push(...wrapText(this.modal.label, Math.max(2, cols - 3), true).map(line => style(` ${line}`)));
|
|
919
|
+
}
|
|
920
|
+
else if (this.queueEditor) {
|
|
921
|
+
const label = tr(" Modifica in coda · Invio salva · Alt+S invia subito · Esc annulla", " Edit queued message · Enter save · Alt+S send now · Esc cancel");
|
|
922
|
+
lines.push(fitLine(this.h.queueStyle?.heading(label) ?? label, cols - 1));
|
|
923
|
+
}
|
|
924
|
+
const prompt = this.h.promptText();
|
|
925
|
+
const pcols = visibleLength(prompt);
|
|
926
|
+
const cell = Math.max(8, cols - 3);
|
|
927
|
+
const badge = !this.modal ? (this.h.composerBadge?.() ?? "") : "";
|
|
928
|
+
const badgeSuffix = badge ? box(" │ ") + badge : "";
|
|
929
|
+
const badgeCols = visibleLength(badgeSuffix);
|
|
930
|
+
const tw = Math.max(4, cell - pcols - badgeCols);
|
|
931
|
+
// Badge artifact degli allegati immagine citati nel buffer: appaiono DENTRO
|
|
932
|
+
// la regione input (sopra il box), cosi' l'immagine incollata si vede come
|
|
933
|
+
// oggetto del prompt e NON come messaggio in chat.
|
|
934
|
+
if (!this.modal) {
|
|
935
|
+
const badges = this.h.imageBadges?.(this.buffer) ?? [];
|
|
936
|
+
if (badges.length)
|
|
937
|
+
lines.push(fitLine(badges.join(" ").replace(/\n/g, " "), cols - 1));
|
|
938
|
+
}
|
|
939
|
+
const { rows, crow, ccol, starts } = wrapInput(this.buffer, this.cursor, tw);
|
|
940
|
+
const inputCap = Math.max(1, Math.min(3, Math.floor(this.rows() / 4)));
|
|
941
|
+
const rowOffset = Math.max(0, Math.min(crow - Math.floor(inputCap / 2), rows.length - inputCap));
|
|
942
|
+
const visibleRows = rows.slice(rowOffset, rowOffset + inputCap);
|
|
943
|
+
const padding = !this.modal && !this.queueEditor && this.rows() >= 16 && this.h.composerPadding?.();
|
|
944
|
+
const blank = " " + (this.h.composerStyle?.(" ".repeat(cell)) ?? " ".repeat(cell));
|
|
945
|
+
if (padding)
|
|
946
|
+
lines.push(blank);
|
|
947
|
+
const inputStart = lines.length;
|
|
948
|
+
const highlight = createAttachmentHighlighter(this.buffer, this.modal ? undefined : this.h.attachmentStyle);
|
|
949
|
+
visibleRows.forEach((row, k) => {
|
|
950
|
+
const pre = k === 0 && rowOffset === 0 ? prompt : " ".repeat(pcols);
|
|
951
|
+
const placeholder = !this.buffer && !this.modal && !this.queueEditor ? this.h.placeholder?.() : undefined;
|
|
952
|
+
const highlighted = placeholder ? fitLine(placeholder, tw) : highlight(row, starts[rowOffset + k]);
|
|
953
|
+
const content = pre + padEnd(highlighted, tw) + (k === 0 ? badgeSuffix : " ".repeat(badgeCols));
|
|
954
|
+
lines.push(" " + (this.h.composerStyle?.(content) ?? content));
|
|
955
|
+
});
|
|
956
|
+
if (padding)
|
|
957
|
+
lines.push(blank);
|
|
958
|
+
if (this.roomStatus?.state === "done") {
|
|
959
|
+
const notice = ` ${this.roomStatus.label}`;
|
|
960
|
+
lines.push(fitLine(this.h.statusStyle?.(notice) ?? notice, cols - 1));
|
|
961
|
+
}
|
|
962
|
+
if (!this.modal) {
|
|
963
|
+
for (const row of this.h.belowInputLines?.() ?? [])
|
|
964
|
+
lines.push(fitLine(row, cols - 1));
|
|
965
|
+
}
|
|
966
|
+
const action = this.ctrlCConfirmation?.action;
|
|
967
|
+
const warning = action === "stop" ? tr("fermare il lavoro e mettere in pausa la coda", "stop work and pause the queue") : action === "clear" ? tr("svuotare la bozza", "clear the draft") : tr("uscire", "exit");
|
|
968
|
+
const hint = action
|
|
969
|
+
? tr(" Ctrl+C ancora entro 2s per {warning}. Altro tasto annulla.", " Ctrl+C again within 2s to {warning}. Any other key cancels.", { warning })
|
|
970
|
+
: this.capturingImage ? tr(" Lettura immagine dagli appunti...", " Reading clipboard image...") : this.h.hint(this.buffer);
|
|
971
|
+
if (hint) {
|
|
972
|
+
const hints = action ? wrapText(hint, Math.max(8, cols - 1)) : [hint];
|
|
973
|
+
for (const row of hints)
|
|
974
|
+
lines.push(fitLine(this.h.statusStyle ? this.h.statusStyle(row) : row, cols - 1));
|
|
975
|
+
}
|
|
976
|
+
const prefixCols = 1 + pcols;
|
|
977
|
+
return {
|
|
978
|
+
lines,
|
|
979
|
+
cursorRow: this.questions.focused && questions.lines.length ? questionStart + questions.cursorRow : inputStart + crow - rowOffset,
|
|
980
|
+
cursorCol: this.questions.focused && questions.lines.length ? questions.cursorCol : prefixCols + ccol + 1,
|
|
981
|
+
inputStart, inputRows: this.questions.focused ? 0 : visibleRows.length, textWidth: tw, prefixCols,
|
|
982
|
+
rowStarts: starts.slice(rowOffset, rowOffset + inputCap),
|
|
983
|
+
rowLengths: visibleRows.map((r) => r.length),
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
// Segmento VIVO della stanza Danilov per la status-line: stato animato
|
|
987
|
+
// (running pulsa col frame, done/failed con glyph fisso). Vuoto se nessuna.
|
|
988
|
+
roomSegment() {
|
|
989
|
+
if (!this.roomStatus || this.roomStatus.state === "done")
|
|
990
|
+
return "";
|
|
991
|
+
const style = this.h.statusStyle ?? ((s) => s);
|
|
992
|
+
const { label, state } = this.roomStatus;
|
|
993
|
+
if (state === "running") {
|
|
994
|
+
const g = SPINNER[this.frame % SPINNER.length];
|
|
995
|
+
return `${g} ${label}`;
|
|
996
|
+
}
|
|
997
|
+
const glyph = state === "failed" ? "✗" : "✦";
|
|
998
|
+
return style(`${glyph} ${label}`);
|
|
999
|
+
}
|
|
1000
|
+
statusLine() {
|
|
1001
|
+
const elapsed = Math.round((Date.now() - this.startedAt) / 1000);
|
|
1002
|
+
const style = this.h.statusStyle ?? ((s) => s);
|
|
1003
|
+
const label = this.statusLabel ?? tr("Elaborazione", "Working");
|
|
1004
|
+
const glyph = this.statusTool ? pulseGlyph(this.statusTool, this.frame) : SPINNER[this.frame % SPINNER.length];
|
|
1005
|
+
const name = this.statusTool ? pulseToolName(this.statusTool, label, this.frame) : style(label);
|
|
1006
|
+
const meta = [elapsed >= 1 ? fmtElapsed(elapsed) : "", this.h.runningHint?.() ?? "", this.roomSegment()].filter(Boolean);
|
|
1007
|
+
const right = style(this.cols() >= 64 && (!this.queue.length || this.modal || this.queueEditor)
|
|
1008
|
+
? tr("Alt+S subito · Esc ferma", "Alt+S send now · Esc stop") : tr("Esc ferma", "Esc stop"));
|
|
1009
|
+
const budget = Math.max(0, this.cols() - terminalWidth(right) - 5);
|
|
1010
|
+
let left = ` ${glyph} ${name}`;
|
|
1011
|
+
for (const segment of meta) {
|
|
1012
|
+
const candidate = left + style(` · ${segment}`);
|
|
1013
|
+
if (terminalWidth(candidate) > budget)
|
|
1014
|
+
break;
|
|
1015
|
+
left = candidate;
|
|
1016
|
+
}
|
|
1017
|
+
left = fitLine(left, budget);
|
|
1018
|
+
return fitLine(left + " ".repeat(Math.max(2, this.cols() - 2 - terminalWidth(left) - terminalWidth(right))) + right, this.cols() - 1);
|
|
1019
|
+
}
|
|
1020
|
+
// ── input ─────────────────────────────────────────────────────────────────
|
|
1021
|
+
// Parser RAW dello stdin: mouse (SGR), sequenze escape (frecce/Home/End/Page/
|
|
1022
|
+
// Delete con modificatori), Alt+lettera, control char, caratteri stampabili.
|
|
1023
|
+
clearPendingEscape() {
|
|
1024
|
+
if (this.escapeTimer)
|
|
1025
|
+
clearTimeout(this.escapeTimer);
|
|
1026
|
+
this.escapeTimer = undefined;
|
|
1027
|
+
this.escapePending = "";
|
|
1028
|
+
}
|
|
1029
|
+
feed(s, flushEscape = false) {
|
|
1030
|
+
if (!this.active)
|
|
1031
|
+
return;
|
|
1032
|
+
s = this.escapePending + s;
|
|
1033
|
+
this.clearPendingEscape();
|
|
1034
|
+
if (s !== "\x03")
|
|
1035
|
+
this.cancelCtrlC();
|
|
1036
|
+
// Continuazione di un incollo (bracketed paste) iniziato in un frame precedente.
|
|
1037
|
+
if (this.pasteBuffer !== null) {
|
|
1038
|
+
const combined = this.pasteBuffer + s;
|
|
1039
|
+
const endIdx = combined.indexOf("\x1b[201~");
|
|
1040
|
+
if (endIdx >= 0) {
|
|
1041
|
+
this.pasteBuffer = null;
|
|
1042
|
+
this.handlePaste(combined.slice(0, endIdx));
|
|
1043
|
+
s = combined.slice(endIdx + 6);
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
this.pasteBuffer = combined;
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
let i = 0;
|
|
1051
|
+
while (i < s.length) {
|
|
1052
|
+
const ch = s[i];
|
|
1053
|
+
if (ch === "\x1b") {
|
|
1054
|
+
const rest = s.slice(i);
|
|
1055
|
+
// A terminal can split even the paste-start marker between stdin
|
|
1056
|
+
// chunks. Preserve the escape prefix instead of typing "200~" and
|
|
1057
|
+
// losing the first image paste. A lone Esc still acts after 30 ms.
|
|
1058
|
+
if (rest === "\x1b" || /^\x1b(?:\[[0-9;:<=>?]*|O)$/.test(rest)) {
|
|
1059
|
+
if (!flushEscape && rest.length < 64) {
|
|
1060
|
+
this.escapePending = rest;
|
|
1061
|
+
this.escapeTimer = setTimeout(() => this.feed("", true), 30);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (rest !== "\x1b")
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
// Bracketed paste: ESC[200~ <testo> ESC[201~. Catturiamo il blocco intero
|
|
1068
|
+
// (puo' essere su piu' frame: bufferizziamo finche' non arriva 201~).
|
|
1069
|
+
if (rest.startsWith("\x1b[200~")) {
|
|
1070
|
+
const body = rest.slice(6);
|
|
1071
|
+
const endIdx = body.indexOf("\x1b[201~");
|
|
1072
|
+
if (endIdx >= 0) {
|
|
1073
|
+
this.handlePaste(body.slice(0, endIdx));
|
|
1074
|
+
i += 6 + endIdx + 6;
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
// Fine non ancora arrivata: bufferizza il resto, attende il prossimo frame.
|
|
1078
|
+
this.pasteBuffer = body;
|
|
1079
|
+
i = s.length;
|
|
1080
|
+
}
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
const mouse = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/.exec(rest);
|
|
1084
|
+
if (mouse) {
|
|
1085
|
+
this.handleMouse(+mouse[1], +mouse[2], +mouse[3], mouse[4] === "M");
|
|
1086
|
+
i += mouse[0].length;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const csi = /^\x1b[[O]([0-9;]*)([A-Za-z~])/.exec(rest);
|
|
1090
|
+
if (csi) {
|
|
1091
|
+
this.handleEscape(csi[1] ?? "", csi[2]);
|
|
1092
|
+
i += csi[0].length;
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
const alt = s[i + 1];
|
|
1096
|
+
if (alt === "\r" || alt === "\n") {
|
|
1097
|
+
this.dispatchKey("", { meta: true, name: "return" });
|
|
1098
|
+
i += 2;
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
if (alt && alt >= " " && alt !== "\x7f") {
|
|
1102
|
+
this.dispatchKey(alt, { meta: true, name: alt.toLowerCase() });
|
|
1103
|
+
i += 2;
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
// Esc isolato (\x1b non seguito da una sequenza riconosciuta): risolve in
|
|
1107
|
+
// ordine di priorita'. Mentre un run e' in corso, FERMA la generazione
|
|
1108
|
+
// (come promette la status-line "esc ferma"); prima era gestito solo
|
|
1109
|
+
// Ctrl+C, da cui il bug "Esc non interrompe".
|
|
1110
|
+
if (this.screenVisible) {
|
|
1111
|
+
if (this.screen.key("", { name: "escape" }) === "close")
|
|
1112
|
+
this.closeScreen();
|
|
1113
|
+
this.render();
|
|
1114
|
+
}
|
|
1115
|
+
else if (this.questions.focused && !this.modal)
|
|
1116
|
+
this.questions.hide();
|
|
1117
|
+
else if (this.modal)
|
|
1118
|
+
this.resolveModal(null); // annulla la domanda
|
|
1119
|
+
else if (this.queueEditor)
|
|
1120
|
+
this.finishQueueEdit(false);
|
|
1121
|
+
else if (this.vassalOpen) {
|
|
1122
|
+
this.vassalOpen = false;
|
|
1123
|
+
this.render();
|
|
1124
|
+
} // esci dal viewer
|
|
1125
|
+
else if (this.nativeScroll && this.scrollOffset > 0) {
|
|
1126
|
+
this.scrollOffset = 0;
|
|
1127
|
+
this.render();
|
|
1128
|
+
}
|
|
1129
|
+
else if (this.running) {
|
|
1130
|
+
this.stopRun();
|
|
1131
|
+
}
|
|
1132
|
+
i += 1; // ESC isolato
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
const code = ch.charCodeAt(0);
|
|
1136
|
+
if (code === 13 || code === 10) {
|
|
1137
|
+
this.dispatchKey("", { name: "return" });
|
|
1138
|
+
i += 1;
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
if (code === 9) {
|
|
1142
|
+
this.dispatchKey("\t", { name: "tab" });
|
|
1143
|
+
i += 1;
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
if (code === 127) {
|
|
1147
|
+
this.dispatchKey("", { name: "backspace" });
|
|
1148
|
+
i += 1;
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if (code === 8) {
|
|
1152
|
+
this.dispatchKey("\x08", { name: "backspace", ctrl: true });
|
|
1153
|
+
i += 1;
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
if (code < 32) {
|
|
1157
|
+
this.dispatchKey(ch, { ctrl: true, name: String.fromCharCode(code + 96) });
|
|
1158
|
+
i += 1;
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
this.dispatchKey(ch, {});
|
|
1162
|
+
i += 1;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
handleEscape(params, final) {
|
|
1166
|
+
const parts = params.split(";");
|
|
1167
|
+
const mod = parts.length > 1 ? Math.max(0, Number(parts[1]) - 1) : 0;
|
|
1168
|
+
const shift = Boolean(mod & 1), meta = Boolean(mod & 2), ctrl = Boolean(mod & 4);
|
|
1169
|
+
// Shift+Tab arriva come CSI Z (CBT): cicla la modalita' permessi.
|
|
1170
|
+
if (final === "Z") {
|
|
1171
|
+
this.dispatchKey("", { name: "tab", shift: true });
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if ((final === "u" && parts[0] === "13") || (final === "~" && parts[0] === "27" && parts[2] === "13")) {
|
|
1175
|
+
this.dispatchKey("", { name: "return", shift, meta, ctrl });
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
const codepoint = final === "u" ? parts[0] : final === "~" && parts[0] === "27" ? parts[2] : undefined;
|
|
1179
|
+
if ((codepoint === "118" || codepoint === "86") && (ctrl || meta)) {
|
|
1180
|
+
this.dispatchKey("", { name: "v", shift, meta, ctrl });
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
const arrows = { A: "up", B: "down", C: "right", D: "left", H: "home", F: "end" };
|
|
1184
|
+
let name = arrows[final];
|
|
1185
|
+
if (final === "~") {
|
|
1186
|
+
const tilde = { "1": "home", "7": "home", "4": "end", "8": "end", "5": "pageup", "6": "pagedown", "3": "delete" };
|
|
1187
|
+
name = tilde[parts[0] ?? ""];
|
|
1188
|
+
}
|
|
1189
|
+
// Freccia su/giu' NUDA: disambigua scroll(touchpad) vs pressione (timer).
|
|
1190
|
+
if ((name === "up" || name === "down") && !ctrl && !meta && !shift && !this.screenVisible
|
|
1191
|
+
&& !this.questions.focused && (!this.nativeScroll || this.scrollOffset > 0)) {
|
|
1192
|
+
this.onArrow(name);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
if (name)
|
|
1196
|
+
this.dispatchKey("", { name, ctrl, meta, shift });
|
|
1197
|
+
}
|
|
1198
|
+
// Una freccia isolata mantiene history/cursore; la seconda riconosce subito
|
|
1199
|
+
// una raffica wheel. La finestra arrowScrollUntil include gli eventi residui,
|
|
1200
|
+
// evitando sia il debounce dell'intero gesto sia richiami spurii alla history.
|
|
1201
|
+
onArrow(dir) {
|
|
1202
|
+
const now = Date.now();
|
|
1203
|
+
if (now <= this.arrowScrollUntil || this.scrollOffset > 0) {
|
|
1204
|
+
const step = now <= this.arrowScrollUntil ? WHEEL_ARROW_LINES : 1;
|
|
1205
|
+
this.scrollBy(dir === "up" ? step : -step);
|
|
1206
|
+
this.arrowScrollUntil = now + 140;
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
this.pendingArrows.push(dir);
|
|
1210
|
+
// Due eventi bastano a riconoscere il gesto. Non attendere il silenzio:
|
|
1211
|
+
// un debounce continuamente rinnovato accumula tutta la corsa del touchpad.
|
|
1212
|
+
// flushArrows apre la finestra wheel anche per il terzo evento e i residui.
|
|
1213
|
+
if (this.pendingArrows.length >= 2) {
|
|
1214
|
+
this.flushArrows();
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
this.arrowTimer = setTimeout(() => this.flushArrows(), 80);
|
|
1218
|
+
}
|
|
1219
|
+
flushArrows() {
|
|
1220
|
+
if (this.arrowTimer) {
|
|
1221
|
+
clearTimeout(this.arrowTimer);
|
|
1222
|
+
this.arrowTimer = undefined;
|
|
1223
|
+
}
|
|
1224
|
+
const arr = this.pendingArrows;
|
|
1225
|
+
this.pendingArrows = [];
|
|
1226
|
+
if (!arr.length)
|
|
1227
|
+
return;
|
|
1228
|
+
if (arr.length >= 2) {
|
|
1229
|
+
this.arrowScrollUntil = Date.now() + 140;
|
|
1230
|
+
const up = arr.filter((d) => d === "up").length;
|
|
1231
|
+
this.scrollBy((up - (arr.length - up)) * WHEEL_ARROW_LINES);
|
|
1232
|
+
}
|
|
1233
|
+
else if (this.scrollOffset > 0) {
|
|
1234
|
+
// Se la chat e' gia' scrollata, una freccia nuda continua a navigare il
|
|
1235
|
+
// viewport. La history torna disponibile quando si e' di nuovo in fondo.
|
|
1236
|
+
this.scrollBy(arr[0] === "up" ? 1 : -1);
|
|
1237
|
+
}
|
|
1238
|
+
else if (arr[0] === "up") {
|
|
1239
|
+
this.onUp();
|
|
1240
|
+
}
|
|
1241
|
+
else {
|
|
1242
|
+
this.onDown();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
dispatchKey(s, k) {
|
|
1246
|
+
if (!this.active)
|
|
1247
|
+
return;
|
|
1248
|
+
const seq = s;
|
|
1249
|
+
if (!(k.ctrl && k.name === "c"))
|
|
1250
|
+
this.cancelCtrlC();
|
|
1251
|
+
if (this.screenVisible) {
|
|
1252
|
+
if (this.screen.key(s, k) === "close")
|
|
1253
|
+
this.closeScreen();
|
|
1254
|
+
this.render();
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (!this.modal && !this.overlay && !this.queueEditor) {
|
|
1258
|
+
if (k.shift && k.name === "left" && this.questions.count) {
|
|
1259
|
+
this.questions.open();
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (this.questions.focused && !(k.ctrl && k.name === "c")) {
|
|
1263
|
+
this.questions.key(s, k);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (k.meta && k.name === "q" && !this.modal) {
|
|
1268
|
+
this.showQueue();
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (k.meta && k.name === "t" && !this.modal && !this.overlay && !this.queueEditor) {
|
|
1272
|
+
if (this.queue.sendToTeam())
|
|
1273
|
+
this.startNextQueued();
|
|
1274
|
+
else
|
|
1275
|
+
this.showQueue();
|
|
1276
|
+
this.render();
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (k.ctrl && k.name === "c" && this.queueEditor && !this.modal) {
|
|
1280
|
+
this.finishQueueEdit(false);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (k.ctrl && k.name === "r" && !this.modal) {
|
|
1284
|
+
this.showReview();
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (k.ctrl && k.name === "c")
|
|
1288
|
+
return this.onCtrlC();
|
|
1289
|
+
if (k.ctrl && k.name === "g") {
|
|
1290
|
+
// Con un regno attivo: Ctrl+G entra nel viewer e cicla i vassalli (poi esce).
|
|
1291
|
+
if (this.vassals && this.vassals.length) {
|
|
1292
|
+
if (!this.vassalOpen) {
|
|
1293
|
+
this.vassalOpen = true;
|
|
1294
|
+
this.vassalIdx = 0;
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
this.vassalIdx += 1;
|
|
1298
|
+
if (this.vassalIdx >= this.vassals.length)
|
|
1299
|
+
this.vassalOpen = false;
|
|
1300
|
+
}
|
|
1301
|
+
this.render();
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
this.h.onShowPlan?.();
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (k.ctrl && k.name === "b") {
|
|
1308
|
+
this.h.onBackground?.();
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
// Ctrl+V (e Alt+V come alias): incolla un'immagine dalla clipboard di sistema
|
|
1312
|
+
// (i terminali non mandano i byte binari via stdin, quindi li chiediamo
|
|
1313
|
+
// direttamente all'OS). Nota: se il terminale gestisce Ctrl+V come incolla
|
|
1314
|
+
// nativo, arriva invece come bracketed paste (handlePaste se ne occupa);
|
|
1315
|
+
// questo ramo scatta quando Ctrl+V raggiunge l'app come control char (0x16).
|
|
1316
|
+
if ((k.ctrl || k.meta) && k.name === "v") {
|
|
1317
|
+
void this.captureImage();
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (k.ctrl && k.name === "o" && !this.modal) {
|
|
1321
|
+
this.showReview("modifiche");
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
// Scroll della CHAT (input resta fisso): PageUp/Down, Shift+frecce.
|
|
1325
|
+
if (k.name === "pageup")
|
|
1326
|
+
return this.scrollBy(this.pageStep());
|
|
1327
|
+
if (k.name === "pagedown")
|
|
1328
|
+
return this.scrollBy(-this.pageStep());
|
|
1329
|
+
if (k.shift && k.name === "up")
|
|
1330
|
+
return this.scrollBy(1);
|
|
1331
|
+
if (k.shift && k.name === "down")
|
|
1332
|
+
return this.scrollBy(-1);
|
|
1333
|
+
if (k.meta && k.name === "s")
|
|
1334
|
+
return this.submit(true);
|
|
1335
|
+
if (k.name === "return" || k.name === "enter")
|
|
1336
|
+
return this.submit(Boolean(k.meta));
|
|
1337
|
+
if (k.shift && k.name === "tab") {
|
|
1338
|
+
this.h.onCycleMode?.();
|
|
1339
|
+
this.render();
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
if (k.name === "tab")
|
|
1343
|
+
return this.complete();
|
|
1344
|
+
if ((k.ctrl || k.meta) && k.name === "backspace")
|
|
1345
|
+
return this.edit(() => this.deleteWord());
|
|
1346
|
+
if ((k.ctrl || k.meta) && k.name === "delete")
|
|
1347
|
+
return this.edit(() => this.deleteWordForward());
|
|
1348
|
+
if (seq === "\x08" || seq === "\x17")
|
|
1349
|
+
return this.edit(() => this.deleteWord());
|
|
1350
|
+
if (k.name === "backspace")
|
|
1351
|
+
return this.edit(() => {
|
|
1352
|
+
if (this.cursor <= 0)
|
|
1353
|
+
return;
|
|
1354
|
+
// Cancellazione ATOMICA del token "img #N": se il cursore tocca un
|
|
1355
|
+
// riferimento immagine, lo rimuove per intero (l'allegato si scollega).
|
|
1356
|
+
const span = this.h.onBackspaceToken?.(this.buffer, this.cursor);
|
|
1357
|
+
if (span) {
|
|
1358
|
+
this.buffer = this.buffer.slice(0, span.start) + this.buffer.slice(span.end);
|
|
1359
|
+
this.cursor = span.start;
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
this.buffer = this.buffer.slice(0, this.cursor - 1) + this.buffer.slice(this.cursor);
|
|
1363
|
+
this.cursor -= 1;
|
|
1364
|
+
});
|
|
1365
|
+
if (k.name === "delete")
|
|
1366
|
+
return this.edit(() => {
|
|
1367
|
+
this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(this.cursor + 1);
|
|
1368
|
+
});
|
|
1369
|
+
if ((k.ctrl || k.meta) && k.name === "left")
|
|
1370
|
+
return this.edit(() => { this.cursor = wordLeft(this.buffer, this.cursor); });
|
|
1371
|
+
if ((k.ctrl || k.meta) && k.name === "right")
|
|
1372
|
+
return this.edit(() => { this.cursor = wordRight(this.buffer, this.cursor); });
|
|
1373
|
+
if (k.name === "left")
|
|
1374
|
+
return this.edit(() => { this.cursor = Math.max(0, this.cursor - 1); });
|
|
1375
|
+
if (k.name === "right")
|
|
1376
|
+
return this.edit(() => { this.cursor = Math.min(this.buffer.length, this.cursor + 1); });
|
|
1377
|
+
if (k.name === "home" || (k.ctrl && k.name === "a"))
|
|
1378
|
+
return this.edit(() => { this.cursor = 0; });
|
|
1379
|
+
if (k.name === "end" || (k.ctrl && k.name === "e"))
|
|
1380
|
+
return this.edit(() => { this.cursor = this.buffer.length; });
|
|
1381
|
+
if (k.ctrl && k.name === "u")
|
|
1382
|
+
return this.edit(() => { this.buffer = this.buffer.slice(this.cursor); this.cursor = 0; });
|
|
1383
|
+
if (k.ctrl && k.name === "k")
|
|
1384
|
+
return this.edit(() => { this.buffer = this.buffer.slice(0, this.cursor); });
|
|
1385
|
+
if (k.ctrl && k.name === "w")
|
|
1386
|
+
return this.edit(() => this.deleteWord());
|
|
1387
|
+
if (k.name === "up")
|
|
1388
|
+
return this.onUp();
|
|
1389
|
+
if (k.name === "down")
|
|
1390
|
+
return this.onDown();
|
|
1391
|
+
if (s && !k.ctrl && !k.meta && !isControl(s)) {
|
|
1392
|
+
return this.edit(() => {
|
|
1393
|
+
const clean = s.replace(/[\r\n]+/g, " ");
|
|
1394
|
+
this.buffer = this.buffer.slice(0, this.cursor) + clean + this.buffer.slice(this.cursor);
|
|
1395
|
+
this.cursor += clean.length;
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
// Testo incollato (bracketed paste): l'hook onPaste puo' trasformarlo (es.
|
|
1400
|
+
// un path/data-URI immagine -> "img #N"); altrimenti si inserisce com'e'.
|
|
1401
|
+
// I newline si normalizzano a spazio: l'input e' una riga sola (Invio invia).
|
|
1402
|
+
handlePaste(text) {
|
|
1403
|
+
if (this.screenVisible) {
|
|
1404
|
+
this.screen.paste?.(text);
|
|
1405
|
+
this.render();
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (this.questions.focused && !this.modal && !this.overlay) {
|
|
1409
|
+
this.questions.paste(text);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
// Incollo di un'IMMAGINE con Ctrl+V: i terminali intercettano Ctrl+V come
|
|
1413
|
+
// paste nativo, ma un'immagine non ha rappresentazione testuale, quindi il
|
|
1414
|
+
// bracketed paste arriva vuoto (o con soli spazi/whitespace). In quel caso
|
|
1415
|
+
// proviamo a leggere i byte dalla clipboard di sistema (stesso flusso di
|
|
1416
|
+
// Alt+V). Solo fuori dai modali e con la sessione attiva.
|
|
1417
|
+
if ((!text || !text.trim()) && !this.modal && this.active) {
|
|
1418
|
+
void this.captureImage();
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
if (!text)
|
|
1422
|
+
return;
|
|
1423
|
+
if (this.modal) {
|
|
1424
|
+
// Nelle domande a risposta libera l'incollo va comunque nel buffer testuale.
|
|
1425
|
+
const clean = text.replace(/[\r\n]+/g, " ");
|
|
1426
|
+
this.edit(() => {
|
|
1427
|
+
this.buffer = this.buffer.slice(0, this.cursor) + clean + this.buffer.slice(this.cursor);
|
|
1428
|
+
this.cursor += clean.length;
|
|
1429
|
+
});
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const transformed = this.h.onPaste?.(text);
|
|
1433
|
+
const insert = (transformed ?? text).replace(/[\r\n]+/g, " ");
|
|
1434
|
+
this.edit(() => {
|
|
1435
|
+
this.buffer = this.buffer.slice(0, this.cursor) + insert + this.buffer.slice(this.cursor);
|
|
1436
|
+
this.cursor += insert.length;
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
// Ctrl+V: chiede all'host i byte dell'immagine nella clipboard di sistema. Se
|
|
1440
|
+
// c'e' un'immagine, l'host ritorna il riferimento "img #N" da inserire nel
|
|
1441
|
+
// buffer (l'host stesso mostra eventuale feedback/anteprima). Async perche'
|
|
1442
|
+
// la lettura della clipboard passa da un sottoprocesso di sistema.
|
|
1443
|
+
async captureImage() {
|
|
1444
|
+
if (!this.active || this.modal || this.screenVisible || this.capturingImage || !this.h.onCaptureImage)
|
|
1445
|
+
return;
|
|
1446
|
+
this.capturingImage = true;
|
|
1447
|
+
const generation = this.inputGeneration;
|
|
1448
|
+
const editor = this.queueEditor;
|
|
1449
|
+
let inserted = false;
|
|
1450
|
+
this.render();
|
|
1451
|
+
try {
|
|
1452
|
+
const ref = await this.h.onCaptureImage();
|
|
1453
|
+
// Do not attach a late result to another draft, a modal or a closed UI.
|
|
1454
|
+
if (!ref || !this.active || this.modal || this.screenVisible
|
|
1455
|
+
|| this.inputGeneration !== generation || this.queueEditor !== editor)
|
|
1456
|
+
return;
|
|
1457
|
+
const insert = ref.replace(/[\r\n]+/g, " ");
|
|
1458
|
+
this.edit(() => {
|
|
1459
|
+
this.buffer = this.buffer.slice(0, this.cursor) + insert + this.buffer.slice(this.cursor);
|
|
1460
|
+
this.cursor += insert.length;
|
|
1461
|
+
});
|
|
1462
|
+
inserted = true;
|
|
1463
|
+
}
|
|
1464
|
+
catch (error) {
|
|
1465
|
+
if (this.active && !this.modal && !this.screenVisible && this.inputGeneration === generation && this.queueEditor === editor) {
|
|
1466
|
+
this.reportFailure(`${tr("Errore clipboard", "Clipboard error")}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
finally {
|
|
1470
|
+
this.capturingImage = false;
|
|
1471
|
+
const submit = this.deferredImageSubmit;
|
|
1472
|
+
this.deferredImageSubmit = null;
|
|
1473
|
+
this.render();
|
|
1474
|
+
if (inserted && submit !== null)
|
|
1475
|
+
this.submit(submit);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
pageStep() {
|
|
1479
|
+
const header = this.h.headerLines?.().length ?? 0;
|
|
1480
|
+
return Math.max(1, this.rows() - this.liveContent().lines.length - header - 1);
|
|
1481
|
+
}
|
|
1482
|
+
handleMouse(button, x, y, press) {
|
|
1483
|
+
if (this.screenVisible) {
|
|
1484
|
+
if (press && (button & 0b1000000)) {
|
|
1485
|
+
this.screen.scroll(button & 1 ? WHEEL_MOUSE_LINES : -WHEEL_MOUSE_LINES);
|
|
1486
|
+
this.queueScrollRender();
|
|
1487
|
+
}
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
// Rotella (bit 6): SEMPRE scroll della CHAT, anche sul rilascio e ovunque sia
|
|
1491
|
+
// il puntatore. L'input resta fisso: lo scroll NON "ruba" mai la posizione
|
|
1492
|
+
// del cursore ne' digita nell'input. 64 = su, 65 = giu'.
|
|
1493
|
+
if (button & 0b1000000) {
|
|
1494
|
+
if (press)
|
|
1495
|
+
this.scrollBy(button & 1 ? -WHEEL_MOUSE_LINES : WHEEL_MOUSE_LINES);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
// Da qui in giu' solo i CLICK veri: il rilascio (press=false) e i movimenti
|
|
1499
|
+
// (bit 5, drag) non toccano l'input.
|
|
1500
|
+
if (!press)
|
|
1501
|
+
return;
|
|
1502
|
+
if (button & 0b0100000)
|
|
1503
|
+
return; // movimento/drag
|
|
1504
|
+
if ((button & 0b11) !== 0)
|
|
1505
|
+
return; // solo click sinistro (no middle/right)
|
|
1506
|
+
const lay = this.lastLayout;
|
|
1507
|
+
if (!lay || this.overlay || lay.inputRows === 0)
|
|
1508
|
+
return;
|
|
1509
|
+
// Il click sposta il cursore SOLO se cade DENTRO le righe del box input.
|
|
1510
|
+
const within = (y - this.liveStartRow) - lay.inputStart;
|
|
1511
|
+
if (within < 0 || within >= lay.inputRows)
|
|
1512
|
+
return;
|
|
1513
|
+
// Mappa colonna->offset usando lo start REALE della riga (word-wrap: righe
|
|
1514
|
+
// di lunghezza variabile, non multipli fissi di textWidth).
|
|
1515
|
+
const rowStart = lay.rowStarts[within] ?? this.buffer.length;
|
|
1516
|
+
const rowLen = lay.rowLengths[within] ?? 0;
|
|
1517
|
+
const col = Math.max(0, Math.min((x - 1) - lay.prefixCols, rowLen));
|
|
1518
|
+
this.cursor = Math.max(0, Math.min(this.buffer.length, rowStart + col));
|
|
1519
|
+
this.render();
|
|
1520
|
+
}
|
|
1521
|
+
onUp() {
|
|
1522
|
+
const tw = this.lastLayout?.textWidth ?? this.buffer.length + 1;
|
|
1523
|
+
if (this.cursor >= tw) {
|
|
1524
|
+
this.edit(() => { this.cursor = Math.max(0, this.cursor - tw); });
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
this.historyPrev();
|
|
1528
|
+
}
|
|
1529
|
+
onDown() {
|
|
1530
|
+
const tw = this.lastLayout?.textWidth ?? this.buffer.length + 1;
|
|
1531
|
+
if (this.buffer.length - this.cursor >= tw) {
|
|
1532
|
+
this.edit(() => { this.cursor = Math.min(this.buffer.length, this.cursor + tw); });
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
this.historyNext();
|
|
1536
|
+
}
|
|
1537
|
+
edit(mutate) { mutate(); this.render(); }
|
|
1538
|
+
deleteWord() {
|
|
1539
|
+
const left = this.buffer.slice(0, this.cursor).replace(/\s*\S+\s*$/, "");
|
|
1540
|
+
this.buffer = left + this.buffer.slice(this.cursor);
|
|
1541
|
+
this.cursor = left.length;
|
|
1542
|
+
}
|
|
1543
|
+
deleteWordForward() {
|
|
1544
|
+
const right = this.buffer.slice(this.cursor).replace(/^\s*\S+\s*/, "");
|
|
1545
|
+
this.buffer = this.buffer.slice(0, this.cursor) + right;
|
|
1546
|
+
}
|
|
1547
|
+
complete() {
|
|
1548
|
+
const next = this.h.completion(this.buffer);
|
|
1549
|
+
if (next === null || next === this.buffer)
|
|
1550
|
+
return;
|
|
1551
|
+
this.buffer = next;
|
|
1552
|
+
this.cursor = next.length;
|
|
1553
|
+
this.render();
|
|
1554
|
+
}
|
|
1555
|
+
historyPrev() {
|
|
1556
|
+
if (!this.history.length)
|
|
1557
|
+
return;
|
|
1558
|
+
if (this.histIdx === -1)
|
|
1559
|
+
this.draft = this.buffer;
|
|
1560
|
+
this.histIdx = this.histIdx === -1 ? this.history.length - 1 : Math.max(0, this.histIdx - 1);
|
|
1561
|
+
this.buffer = this.history[this.histIdx] ?? "";
|
|
1562
|
+
this.cursor = this.buffer.length;
|
|
1563
|
+
this.render();
|
|
1564
|
+
}
|
|
1565
|
+
historyNext() {
|
|
1566
|
+
if (this.histIdx === -1)
|
|
1567
|
+
return;
|
|
1568
|
+
this.histIdx += 1;
|
|
1569
|
+
if (this.histIdx >= this.history.length) {
|
|
1570
|
+
this.histIdx = -1;
|
|
1571
|
+
this.buffer = this.draft;
|
|
1572
|
+
}
|
|
1573
|
+
else
|
|
1574
|
+
this.buffer = this.history[this.histIdx] ?? "";
|
|
1575
|
+
this.cursor = this.buffer.length;
|
|
1576
|
+
this.render();
|
|
1577
|
+
}
|
|
1578
|
+
onCtrlC() {
|
|
1579
|
+
if (this.modal) {
|
|
1580
|
+
this.resolveModal(null);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
const action = this.running ? "stop" : this.buffer ? "clear" : "quit";
|
|
1584
|
+
if (this.ctrlCConfirmation?.action === action && Date.now() < this.ctrlCConfirmation.until) {
|
|
1585
|
+
this.cancelCtrlC();
|
|
1586
|
+
if (action === "stop") {
|
|
1587
|
+
this.stopRun();
|
|
1588
|
+
this.render();
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
if (action === "clear") {
|
|
1592
|
+
this.inputGeneration++;
|
|
1593
|
+
this.buffer = "";
|
|
1594
|
+
this.cursor = 0;
|
|
1595
|
+
this.histIdx = -1;
|
|
1596
|
+
this.render();
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
this.quit();
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
this.cancelCtrlC();
|
|
1603
|
+
this.ctrlCConfirmation = { action, until: Date.now() + 2000 };
|
|
1604
|
+
this.ctrlCTimer = setTimeout(() => { this.cancelCtrlC(); if (this.active)
|
|
1605
|
+
this.render(); }, 2000);
|
|
1606
|
+
this.render();
|
|
1607
|
+
}
|
|
1608
|
+
cancelCtrlC() {
|
|
1609
|
+
if (this.ctrlCTimer)
|
|
1610
|
+
clearTimeout(this.ctrlCTimer);
|
|
1611
|
+
this.ctrlCTimer = undefined;
|
|
1612
|
+
this.ctrlCConfirmation = null;
|
|
1613
|
+
}
|
|
1614
|
+
stopRun() {
|
|
1615
|
+
this.cancelCtrlC();
|
|
1616
|
+
const queued = this.queue.length;
|
|
1617
|
+
this.queue.paused = true;
|
|
1618
|
+
this.runAbort?.abort();
|
|
1619
|
+
if (queued)
|
|
1620
|
+
this.setRoomStatus(tr("Lavoro interrotto. {queued} messaggi conservati in pausa · Alt+Q o /queue per gestirli.", "Work interrupted. {queued} messages paused · Alt+Q or /queue to manage.", { queued }), "failed");
|
|
1621
|
+
}
|
|
1622
|
+
// Domanda a risposta libera: l'editing finisce nel buffer, Invio risolve.
|
|
1623
|
+
queueQuestion(title, options, signal) {
|
|
1624
|
+
if (!this.active)
|
|
1625
|
+
return Promise.resolve(null);
|
|
1626
|
+
return this.questions.enqueue(title, options, signal);
|
|
1627
|
+
}
|
|
1628
|
+
promptInput(label) {
|
|
1629
|
+
if (!this.active)
|
|
1630
|
+
return Promise.resolve(null);
|
|
1631
|
+
this.inputGeneration++;
|
|
1632
|
+
this.cancelCtrlC();
|
|
1633
|
+
const labelRows = wrapText(label, Math.max(2, this.cols() - 3), true);
|
|
1634
|
+
if (labelRows.length > Math.max(2, Math.floor(this.rows() / 3))) {
|
|
1635
|
+
const style = this.h.statusStyle ?? ((s) => s);
|
|
1636
|
+
this.write("\n" + labelRows.map(line => style(" " + line)).join("\n") + "\n");
|
|
1637
|
+
label = tr("Rispondi qui · Invio conferma · Esc annulla", "Reply here · Enter confirm · Esc cancel");
|
|
1638
|
+
}
|
|
1639
|
+
const draft = { buffer: this.buffer, cursor: this.cursor, histIdx: this.histIdx };
|
|
1640
|
+
this.buffer = "";
|
|
1641
|
+
this.cursor = 0;
|
|
1642
|
+
this.histIdx = -1;
|
|
1643
|
+
return new Promise((resolve) => {
|
|
1644
|
+
this.modal = { label, resolve, draft };
|
|
1645
|
+
this.render();
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
resolveModal(value) {
|
|
1649
|
+
const m = this.modal;
|
|
1650
|
+
if (!m)
|
|
1651
|
+
return;
|
|
1652
|
+
this.modal = null;
|
|
1653
|
+
this.buffer = m.draft.buffer;
|
|
1654
|
+
this.cursor = m.draft.cursor;
|
|
1655
|
+
this.histIdx = m.draft.histIdx;
|
|
1656
|
+
this.render();
|
|
1657
|
+
m.resolve(value);
|
|
1658
|
+
this.startNextQueued();
|
|
1659
|
+
}
|
|
1660
|
+
submit(immediate = false) {
|
|
1661
|
+
if (this.modal) {
|
|
1662
|
+
this.resolveModal(this.buffer);
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
// Enter during a clipboard read waits for the attachment. On failure the
|
|
1666
|
+
// draft stays editable instead of sending a prompt with a missing image.
|
|
1667
|
+
if (this.capturingImage) {
|
|
1668
|
+
this.deferredImageSubmit = Boolean(this.deferredImageSubmit || immediate);
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
if (this.queueEditor) {
|
|
1672
|
+
this.finishQueueEdit(true, immediate);
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
const line = this.buffer;
|
|
1676
|
+
this.buffer = "";
|
|
1677
|
+
this.cursor = 0;
|
|
1678
|
+
this.histIdx = -1;
|
|
1679
|
+
if (!line.trim()) {
|
|
1680
|
+
if (immediate) {
|
|
1681
|
+
const next = this.queue.items().find(entry => !entry.text.trimStart().startsWith("/"));
|
|
1682
|
+
if (next)
|
|
1683
|
+
this.queue.sendNow(next.id);
|
|
1684
|
+
}
|
|
1685
|
+
else if (!this.running && this.queue.paused)
|
|
1686
|
+
this.queue.paused = false;
|
|
1687
|
+
this.startNextQueued();
|
|
1688
|
+
this.render();
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
if (/^\/(?:queue|coda)\s*$/i.test(line.trim())) {
|
|
1692
|
+
this.showQueue();
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const review = /^\/(?:review|revisione)(?:\s+(\S+))?\s*$/i.exec(line.trim());
|
|
1696
|
+
if (review && this.h.createReviewScreen) {
|
|
1697
|
+
this.showReview(review[1]);
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
this.dispatch(line, immediate);
|
|
1701
|
+
}
|
|
1702
|
+
dispatch(line, immediate = false) {
|
|
1703
|
+
// Una coda ripristinata in pausa non deve bloccare /new, /resume, /help
|
|
1704
|
+
// o /quit. A riposo i comandi controllano la UI senza riprendere la coda.
|
|
1705
|
+
if (!this.running && line.trimStart().startsWith("/")) {
|
|
1706
|
+
this.runLine(line);
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
if (this.running || this.queue.length) {
|
|
1710
|
+
try {
|
|
1711
|
+
this.queue.add(line, immediate ? "immediate" : "after-turn");
|
|
1712
|
+
// Aprire una chat non avvia lavoro da solo. Un nuovo Invio a riposo
|
|
1713
|
+
// riprende invece la coda: il prompt appena scritto non resta bloccato
|
|
1714
|
+
// dietro la pausa applicata durante il ripristino.
|
|
1715
|
+
if (!this.running && !immediate)
|
|
1716
|
+
this.queue.paused = false;
|
|
1717
|
+
this.h.onQueued?.(line);
|
|
1718
|
+
}
|
|
1719
|
+
catch (error) {
|
|
1720
|
+
this.buffer = line;
|
|
1721
|
+
this.cursor = line.length;
|
|
1722
|
+
this.reportFailure(`${tr("Messaggio non accodato", "Message not queued")}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1723
|
+
}
|
|
1724
|
+
this.startNextQueued();
|
|
1725
|
+
this.render();
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
this.queue.paused = false;
|
|
1729
|
+
this.runLine(line);
|
|
1730
|
+
}
|
|
1731
|
+
startNextQueued() {
|
|
1732
|
+
if (!this.active || this.running || this.modal || this.overlay || this.queueEditor)
|
|
1733
|
+
return;
|
|
1734
|
+
const next = this.queue.takeNext();
|
|
1735
|
+
if (next) {
|
|
1736
|
+
this.h.onDequeue?.(next.text);
|
|
1737
|
+
this.runLine(next.text, true, next);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
runLine(line, fromQueue = false, queued) {
|
|
1741
|
+
this.setRoomStatus(null);
|
|
1742
|
+
this.history.push(line);
|
|
1743
|
+
this.running = true;
|
|
1744
|
+
this.startedAt = Date.now();
|
|
1745
|
+
this.tokensNew = 0;
|
|
1746
|
+
this.tokensCache = 0;
|
|
1747
|
+
this.tokensOut = 0;
|
|
1748
|
+
const ac = new AbortController();
|
|
1749
|
+
this.runAbort = ac;
|
|
1750
|
+
this.render();
|
|
1751
|
+
Promise.resolve().then(() => this.h.onSubmit(line, ac.signal, fromQueue, queued))
|
|
1752
|
+
.catch(error => {
|
|
1753
|
+
this.queue.paused = true;
|
|
1754
|
+
this.reportFailure(`${tr("Errore", "Error")}: ${error instanceof Error ? error.message : String(error)}. ${tr("Coda in pausa.", "Queue paused.")}`);
|
|
1755
|
+
})
|
|
1756
|
+
.finally(() => {
|
|
1757
|
+
this.cancelCtrlC();
|
|
1758
|
+
this.runAbort = undefined;
|
|
1759
|
+
this.running = false;
|
|
1760
|
+
this.setStatus(null);
|
|
1761
|
+
if (!this.active)
|
|
1762
|
+
return;
|
|
1763
|
+
this.startNextQueued();
|
|
1764
|
+
this.render();
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
quit() {
|
|
1768
|
+
if (!this.active)
|
|
1769
|
+
return;
|
|
1770
|
+
this.active = false;
|
|
1771
|
+
this.questions.clear();
|
|
1772
|
+
this.clearPendingEscape();
|
|
1773
|
+
this.cancelCtrlC();
|
|
1774
|
+
if (this.spinnerTimer) {
|
|
1775
|
+
clearInterval(this.spinnerTimer);
|
|
1776
|
+
this.spinnerTimer = undefined;
|
|
1777
|
+
}
|
|
1778
|
+
if (this.arrowTimer) {
|
|
1779
|
+
clearTimeout(this.arrowTimer);
|
|
1780
|
+
this.arrowTimer = undefined;
|
|
1781
|
+
}
|
|
1782
|
+
if (this.roomTimer) {
|
|
1783
|
+
clearTimeout(this.roomTimer);
|
|
1784
|
+
this.roomTimer = undefined;
|
|
1785
|
+
}
|
|
1786
|
+
if (this.scrollRenderTimer) {
|
|
1787
|
+
clearTimeout(this.scrollRenderTimer);
|
|
1788
|
+
this.scrollRenderTimer = undefined;
|
|
1789
|
+
}
|
|
1790
|
+
if (this.dataListener)
|
|
1791
|
+
stdin.off("data", this.dataListener);
|
|
1792
|
+
if (this.resizeListener)
|
|
1793
|
+
stdout.off("resize", this.resizeListener);
|
|
1794
|
+
if (this.resizeTimer) {
|
|
1795
|
+
clearTimeout(this.resizeTimer);
|
|
1796
|
+
this.resizeTimer = undefined;
|
|
1797
|
+
}
|
|
1798
|
+
if (this.alternateScreen) {
|
|
1799
|
+
stdout.write(ALT_OFF);
|
|
1800
|
+
this.alternateScreen = false;
|
|
1801
|
+
}
|
|
1802
|
+
if (this.nativeScroll)
|
|
1803
|
+
stdout.write(this.nativeOutput.release());
|
|
1804
|
+
stdout.write(PASTE_OFF + NATIVE_SCROLL_MODE + "\x1b[?25h");
|
|
1805
|
+
if (typeof stdin.setRawMode === "function")
|
|
1806
|
+
stdin.setRawMode(false);
|
|
1807
|
+
this.h.onQuit();
|
|
1808
|
+
this.quitResolve?.();
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
// Word-wrap dell'input: manda a capo le PAROLE INTERE invece di spezzarle a
|
|
1812
|
+
// meta'. Ogni riga porta il suo offset di partenza nel buffer, cosi' la
|
|
1813
|
+
// posizione del cursore (crow/ccol) resta esatta. Una parola piu' lunga della
|
|
1814
|
+
// larghezza disponibile viene comunque spezzata (hard-cut), altrimenti non
|
|
1815
|
+
// entrerebbe mai.
|
|
1816
|
+
export function wrapInput(buffer, cursor, tw) {
|
|
1817
|
+
const width = Math.max(1, tw);
|
|
1818
|
+
// Ogni riga: { text, start } dove start = indice nel buffer del primo char.
|
|
1819
|
+
const segs = wrapSegments(buffer, width);
|
|
1820
|
+
const rows = segs.map((s) => s.text);
|
|
1821
|
+
const starts = segs.map((s) => s.start);
|
|
1822
|
+
// Mappa il cursore alla riga giusta: l'ultima riga il cui start <= cursor.
|
|
1823
|
+
let crow = 0;
|
|
1824
|
+
for (let r = 0; r < segs.length; r += 1) {
|
|
1825
|
+
if (segs[r].start <= cursor)
|
|
1826
|
+
crow = r;
|
|
1827
|
+
else
|
|
1828
|
+
break;
|
|
1829
|
+
}
|
|
1830
|
+
const seg = segs[crow];
|
|
1831
|
+
let ccol = cursor - seg.start;
|
|
1832
|
+
// Se il cursore cade oltre la fine del testo visibile di questa riga (lo
|
|
1833
|
+
// spazio "mangiato" a fine riga dal wrap), tienilo a fine riga.
|
|
1834
|
+
if (ccol > seg.text.length)
|
|
1835
|
+
ccol = seg.text.length;
|
|
1836
|
+
// Cursore in coda al buffer e l'ultima riga e' piena: apri una riga nuova
|
|
1837
|
+
// cosi' il cursore non si sovrappone all'ultimo carattere.
|
|
1838
|
+
const last = segs[segs.length - 1];
|
|
1839
|
+
if (cursor === buffer.length && cursor > 0 && last.start + last.text.length === cursor && last.text.length >= width) {
|
|
1840
|
+
rows.push("");
|
|
1841
|
+
starts.push(cursor);
|
|
1842
|
+
crow = rows.length - 1;
|
|
1843
|
+
ccol = 0;
|
|
1844
|
+
}
|
|
1845
|
+
return { rows, crow, ccol, starts };
|
|
1846
|
+
}
|
|
1847
|
+
// Suddivide il buffer in righe larghe al massimo `width`, preferendo i confini
|
|
1848
|
+
// di parola (spazi). Ritorna per ogni riga il testo e l'offset di partenza.
|
|
1849
|
+
function wrapSegments(buffer, width) {
|
|
1850
|
+
if (buffer.length === 0)
|
|
1851
|
+
return [{ text: "", start: 0 }];
|
|
1852
|
+
const segs = [];
|
|
1853
|
+
let start = 0;
|
|
1854
|
+
const n = buffer.length;
|
|
1855
|
+
while (start < n) {
|
|
1856
|
+
let end = Math.min(start + width, n);
|
|
1857
|
+
if (end < n) {
|
|
1858
|
+
// C'e' ancora testo dopo: cerca l'ultimo spazio entro [start, end] per
|
|
1859
|
+
// spezzare su confine di parola.
|
|
1860
|
+
let brk = -1;
|
|
1861
|
+
for (let i = end; i > start; i -= 1) {
|
|
1862
|
+
if (buffer[i] === " ") {
|
|
1863
|
+
brk = i;
|
|
1864
|
+
break;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if (brk > start) {
|
|
1868
|
+
// Spezza dopo lo spazio: lo spazio resta in coda alla riga corrente,
|
|
1869
|
+
// la riga successiva parte dalla parola.
|
|
1870
|
+
end = brk + 1;
|
|
1871
|
+
}
|
|
1872
|
+
// Se nessuno spazio (brk<=start): parola piu' lunga di width -> hard-cut.
|
|
1873
|
+
}
|
|
1874
|
+
segs.push({ text: buffer.slice(start, end), start });
|
|
1875
|
+
start = end;
|
|
1876
|
+
}
|
|
1877
|
+
if (segs.length === 0)
|
|
1878
|
+
segs.push({ text: "", start: 0 });
|
|
1879
|
+
return segs;
|
|
1880
|
+
}
|
|
1881
|
+
function wordLeft(buffer, cursor) {
|
|
1882
|
+
let i = cursor;
|
|
1883
|
+
while (i > 0 && /\s/.test(buffer[i - 1] ?? ""))
|
|
1884
|
+
i -= 1;
|
|
1885
|
+
while (i > 0 && !/\s/.test(buffer[i - 1] ?? ""))
|
|
1886
|
+
i -= 1;
|
|
1887
|
+
return i;
|
|
1888
|
+
}
|
|
1889
|
+
function wordRight(buffer, cursor) {
|
|
1890
|
+
let i = cursor;
|
|
1891
|
+
const n = buffer.length;
|
|
1892
|
+
while (i < n && /\s/.test(buffer[i] ?? ""))
|
|
1893
|
+
i += 1;
|
|
1894
|
+
while (i < n && !/\s/.test(buffer[i] ?? ""))
|
|
1895
|
+
i += 1;
|
|
1896
|
+
return i;
|
|
1897
|
+
}
|
|
1898
|
+
function fmtElapsed(seconds) {
|
|
1899
|
+
if (seconds < 60)
|
|
1900
|
+
return `${seconds}s`;
|
|
1901
|
+
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
1902
|
+
}
|
|
1903
|
+
function isControl(s) {
|
|
1904
|
+
return s.length === 1 && s.charCodeAt(0) < 0x20;
|
|
1905
|
+
}
|
|
1906
|
+
function padEnd(value, width) {
|
|
1907
|
+
return value + " ".repeat(Math.max(0, width - visibleLength(value)));
|
|
1908
|
+
}
|
|
1909
|
+
function visibleLength(value) {
|
|
1910
|
+
return value.replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\][^\x07]*\x07/g, "").length;
|
|
1911
|
+
}
|
|
1912
|
+
// Tronca alla larghezza VISIBILE preservando le sequenze ANSI (colori, OSC 8).
|
|
1913
|
+
// Sul troncamento chiude SGR e hyperlink OSC 8 aperti (anti-bleed).
|
|
1914
|
+
export function clipVisible(s, max) {
|
|
1915
|
+
if (max <= 0)
|
|
1916
|
+
return "";
|
|
1917
|
+
let out = "";
|
|
1918
|
+
let vis = 0;
|
|
1919
|
+
let i = 0;
|
|
1920
|
+
while (i < s.length) {
|
|
1921
|
+
if (s[i] === "\x1b") {
|
|
1922
|
+
const rest = s.slice(i);
|
|
1923
|
+
const sgr = /^\x1b\[[0-9;]*m/.exec(rest);
|
|
1924
|
+
const osc = /^\x1b\][\s\S]*?(?:\x07|\x1b\\)/.exec(rest);
|
|
1925
|
+
const csi = /^\x1b\[[0-9;?]*[A-Za-z]/.exec(rest);
|
|
1926
|
+
const seq = sgr?.[0] ?? osc?.[0] ?? csi?.[0];
|
|
1927
|
+
if (seq) {
|
|
1928
|
+
out += seq;
|
|
1929
|
+
i += seq.length;
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
i += 1; // ESC isolato: scartalo
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
if (vis >= max) {
|
|
1936
|
+
out += "\x1b[0m\x1b]8;;\x1b\\";
|
|
1937
|
+
return out;
|
|
1938
|
+
}
|
|
1939
|
+
out += s[i];
|
|
1940
|
+
vis += 1;
|
|
1941
|
+
i += 1;
|
|
1942
|
+
}
|
|
1943
|
+
return out;
|
|
1944
|
+
}
|
|
1945
|
+
function truncate(value, max) {
|
|
1946
|
+
if (max <= 0)
|
|
1947
|
+
return "";
|
|
1948
|
+
if (visibleLength(value) <= max)
|
|
1949
|
+
return value;
|
|
1950
|
+
return value.slice(0, max - 1) + "…";
|
|
1951
|
+
}
|
|
1952
|
+
// Sovrappone un suggerimento a destra di una riga (per "altro sopra/sotto").
|
|
1953
|
+
function scrollHint(line, hint, width) {
|
|
1954
|
+
const room = width - visibleLength(line) - hint.length - 2;
|
|
1955
|
+
if (room < 1)
|
|
1956
|
+
return line;
|
|
1957
|
+
return line + " ".repeat(room) + hint;
|
|
1958
|
+
}
|