rovecode 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +662 -0
- package/README.md +737 -0
- package/THIRD_PARTY_NOTICES.md +268 -0
- package/bin/rovecode.ts +21 -0
- package/package.json +56 -0
- package/src/acp/server.ts +374 -0
- package/src/cli/auth-login.ts +122 -0
- package/src/cli/connect.ts +244 -0
- package/src/cli/context-cmd.ts +199 -0
- package/src/cli/dispatch.ts +82 -0
- package/src/cli/doctor.ts +362 -0
- package/src/cli/export.ts +276 -0
- package/src/cli/help.ts +293 -0
- package/src/cli/is-tui-invocation.ts +8 -0
- package/src/cli/main.ts +583 -0
- package/src/cli/market-cmd.ts +658 -0
- package/src/cli/mcp-login.ts +141 -0
- package/src/cli/mcp-market-cmd.ts +302 -0
- package/src/cli/output.ts +382 -0
- package/src/cli/repl.ts +250 -0
- package/src/cli/repomap-root.ts +14 -0
- package/src/cli/resume.ts +57 -0
- package/src/cli/run-flags.ts +43 -0
- package/src/cli/run-limits.ts +78 -0
- package/src/cli/runtime.ts +931 -0
- package/src/cli/session-arg.ts +30 -0
- package/src/cli/sessions-cmd.ts +145 -0
- package/src/cli/setup.ts +153 -0
- package/src/cli/skills-cmd.ts +194 -0
- package/src/cli/start-chat.ts +65 -0
- package/src/cli/trust-cmd.ts +52 -0
- package/src/coding/bash.ts +148 -0
- package/src/coding/checkpoints.ts +327 -0
- package/src/coding/diff.ts +138 -0
- package/src/coding/files.ts +341 -0
- package/src/coding/hashline.ts +274 -0
- package/src/coding/lsp-gate.ts +254 -0
- package/src/coding/lsp-servers.ts +147 -0
- package/src/coding/lsp.ts +283 -0
- package/src/coding/repomap-cache.ts +99 -0
- package/src/coding/repomap-files.ts +192 -0
- package/src/coding/repomap.ts +481 -0
- package/src/core/agents.ts +255 -0
- package/src/core/compaction.ts +259 -0
- package/src/core/config.ts +289 -0
- package/src/core/context-report.ts +228 -0
- package/src/core/context.ts +60 -0
- package/src/core/count-remote.ts +107 -0
- package/src/core/execpolicy-rules.ts +196 -0
- package/src/core/execpolicy.ts +385 -0
- package/src/core/executor.ts +454 -0
- package/src/core/guardrails.ts +400 -0
- package/src/core/hooks.ts +411 -0
- package/src/core/images.ts +230 -0
- package/src/core/intro.ts +266 -0
- package/src/core/loop.ts +567 -0
- package/src/core/modes.ts +372 -0
- package/src/core/orchestrator.ts +245 -0
- package/src/core/proc-group.ts +48 -0
- package/src/core/project-trust.ts +98 -0
- package/src/core/reflection.ts +165 -0
- package/src/core/sandbox-config.ts +186 -0
- package/src/core/session-id.ts +24 -0
- package/src/core/session-images.ts +73 -0
- package/src/core/session-ops.ts +183 -0
- package/src/core/session-text.ts +29 -0
- package/src/core/session.ts +469 -0
- package/src/core/settings.ts +170 -0
- package/src/core/tasks.ts +646 -0
- package/src/core/token-scale.ts +108 -0
- package/src/core/tools.ts +309 -0
- package/src/core/trust.ts +104 -0
- package/src/core/types.ts +330 -0
- package/src/core/update-check.ts +171 -0
- package/src/core/usage.ts +204 -0
- package/src/core/validate.ts +121 -0
- package/src/core/verify-gate.ts +159 -0
- package/src/core/verify.ts +236 -0
- package/src/core/voice.ts +158 -0
- package/src/core/win-job.ts +183 -0
- package/src/core/workspace.ts +184 -0
- package/src/design/audit.ts +797 -0
- package/src/design/direction.ts +190 -0
- package/src/design/rules.ts +157 -0
- package/src/eval/bench.ts +150 -0
- package/src/eval/gauntlet-runner.ts +215 -0
- package/src/eval/gauntlet-support.ts +84 -0
- package/src/eval/gauntlet-wave3.ts +269 -0
- package/src/eval/gauntlet-wave4.ts +217 -0
- package/src/eval/gauntlet.ts +253 -0
- package/src/index.ts +17 -0
- package/src/lanes/agy.ts +95 -0
- package/src/lanes/approval.ts +24 -0
- package/src/lanes/claude.ts +129 -0
- package/src/lanes/codex.ts +127 -0
- package/src/lanes/events.ts +130 -0
- package/src/lanes/job.ts +142 -0
- package/src/lanes/opencode.ts +122 -0
- package/src/lanes/process.ts +184 -0
- package/src/lanes/progress.ts +183 -0
- package/src/lanes/registry.ts +178 -0
- package/src/lanes/runner.ts +124 -0
- package/src/lanes/types.ts +112 -0
- package/src/market/catalogs/mcp-docs.json +111 -0
- package/src/market/catalogs/plugins.json +111 -0
- package/src/market/catalogs/skills.json +478 -0
- package/src/market/clone.ts +72 -0
- package/src/market/context-cost.ts +121 -0
- package/src/market/digest.ts +106 -0
- package/src/market/index.ts +22 -0
- package/src/market/install.ts +578 -0
- package/src/market/manifest.ts +187 -0
- package/src/market/prereq.ts +145 -0
- package/src/market/registry.ts +363 -0
- package/src/market/resolve.ts +111 -0
- package/src/market/types.ts +236 -0
- package/src/market/validate.ts +227 -0
- package/src/mcp/client.ts +449 -0
- package/src/mcp/config.ts +252 -0
- package/src/mcp/local-package.ts +211 -0
- package/src/mcp/market-catalog.ts +84 -0
- package/src/mcp/market-install.ts +289 -0
- package/src/mcp/market.ts +362 -0
- package/src/mcp/oauth.ts +251 -0
- package/src/mcp/prompts-resources.ts +249 -0
- package/src/mcp/shared.ts +149 -0
- package/src/mcp/status.ts +67 -0
- package/src/mcp/tools.ts +275 -0
- package/src/mcp/transport.ts +122 -0
- package/src/mcp/trust.ts +25 -0
- package/src/memory/blocks.ts +278 -0
- package/src/memory/recall.ts +355 -0
- package/src/memory/scope.ts +182 -0
- package/src/memory/store.ts +105 -0
- package/src/memory/tools.ts +99 -0
- package/src/plugins/cli.ts +119 -0
- package/src/plugins/discover.ts +108 -0
- package/src/plugins/index.ts +50 -0
- package/src/plugins/install.ts +184 -0
- package/src/plugins/load.ts +124 -0
- package/src/plugins/manifest.ts +92 -0
- package/src/plugins/state.ts +83 -0
- package/src/providers/auth.ts +408 -0
- package/src/providers/cache.ts +223 -0
- package/src/providers/catalog-local.ts +160 -0
- package/src/providers/catalog.ts +421 -0
- package/src/providers/middleware-context.ts +86 -0
- package/src/providers/middleware.ts +373 -0
- package/src/providers/model-list.ts +23 -0
- package/src/providers/models-index.json +1 -0
- package/src/providers/oauth/common.ts +105 -0
- package/src/providers/oauth/device-code.ts +107 -0
- package/src/providers/oauth/github-copilot.ts +146 -0
- package/src/providers/oauth/loopback.ts +158 -0
- package/src/providers/oauth/openai.ts +163 -0
- package/src/providers/oauth/openrouter.ts +89 -0
- package/src/providers/oauth/pkce.ts +45 -0
- package/src/providers/oauth/registry.ts +39 -0
- package/src/providers/oauth/seam.ts +89 -0
- package/src/providers/profile-glm53.ts +111 -0
- package/src/providers/profile-sonnet5-persona.ts +65 -0
- package/src/providers/profile-sonnet5-voice.ts +23 -0
- package/src/providers/profiles.ts +156 -0
- package/src/providers/provider-config.ts +311 -0
- package/src/providers/registry.ts +333 -0
- package/src/providers/responses.ts +209 -0
- package/src/providers/retry.ts +234 -0
- package/src/providers/router.ts +294 -0
- package/src/providers/sse.ts +26 -0
- package/src/providers/stream-errors.ts +117 -0
- package/src/providers/stream.ts +566 -0
- package/src/providers/thinking.ts +189 -0
- package/src/providers/wire-messages.ts +129 -0
- package/src/providers/wire-responses.ts +79 -0
- package/src/providers/wire-select.ts +53 -0
- package/src/server/http.ts +291 -0
- package/src/server/openapi.ts +246 -0
- package/src/sextant/card-hits.ts +102 -0
- package/src/sextant/card-keys.ts +55 -0
- package/src/sextant/context-source.ts +157 -0
- package/src/sextant/crew-cards.ts +350 -0
- package/src/sextant/draw-agents.ts +273 -0
- package/src/sextant/draw-code.ts +388 -0
- package/src/sextant/draw-context.ts +222 -0
- package/src/sextant/draw-frame.ts +164 -0
- package/src/sextant/draw-market.ts +573 -0
- package/src/sextant/draw-messages.ts +386 -0
- package/src/sextant/draw-pet.ts +230 -0
- package/src/sextant/draw-plan.ts +187 -0
- package/src/sextant/draw-tabs.ts +85 -0
- package/src/sextant/draw-util.ts +65 -0
- package/src/sextant/draw-wizard.ts +378 -0
- package/src/sextant/engine.ts +230 -0
- package/src/sextant/frame-hits.ts +25 -0
- package/src/sextant/frame.ts +101 -0
- package/src/sextant/git-status.ts +197 -0
- package/src/sextant/grid.ts +59 -0
- package/src/sextant/input.ts +119 -0
- package/src/sextant/keys.ts +521 -0
- package/src/sextant/layout.ts +86 -0
- package/src/sextant/local-commands.ts +169 -0
- package/src/sextant/market-source.ts +287 -0
- package/src/sextant/mentions.ts +200 -0
- package/src/sextant/message-hits.ts +26 -0
- package/src/sextant/model.ts +387 -0
- package/src/sextant/overlays.ts +456 -0
- package/src/sextant/panel-hits.ts +38 -0
- package/src/sextant/pet.ts +399 -0
- package/src/sextant/screen.ts +324 -0
- package/src/sextant/scroll-hits.ts +66 -0
- package/src/sextant/scrollbar.ts +82 -0
- package/src/sextant/sextant-bridge.ts +174 -0
- package/src/sextant/sextant-cards.ts +142 -0
- package/src/sextant/sextant-diff-base.ts +63 -0
- package/src/sextant/sextant-files.ts +154 -0
- package/src/sextant/sextant-frame-loop.ts +335 -0
- package/src/sextant/sextant-renderer.ts +574 -0
- package/src/sextant/sextant-repo.ts +140 -0
- package/src/sextant/theme.ts +66 -0
- package/src/sextant/tool-rows.ts +189 -0
- package/src/sextant/types.ts +493 -0
- package/src/skills/index.ts +387 -0
- package/src/skills/pack.ts +220 -0
- package/src/skills/spec.ts +162 -0
- package/src/skills/tools.ts +69 -0
- package/src/skills/versioned.ts +227 -0
- package/src/telemetry/otel-export.ts +122 -0
- package/src/telemetry/otel-lanes.ts +89 -0
- package/src/telemetry/otel-logs.ts +131 -0
- package/src/telemetry/otel-metrics.ts +136 -0
- package/src/telemetry/otel.ts +397 -0
- package/src/telemetry/otlp.ts +76 -0
- package/src/tools/ask-user.ts +156 -0
- package/src/tools/bash-bg.ts +94 -0
- package/src/tools/bash-jobs.ts +237 -0
- package/src/tools/design.ts +151 -0
- package/src/tools/evalcell.ts +338 -0
- package/src/tools/html-text.ts +139 -0
- package/src/tools/provider.ts +149 -0
- package/src/tools/task.ts +250 -0
- package/src/tools/todo.ts +320 -0
- package/src/tools/webfetch.ts +332 -0
- package/src/tools/websearch.ts +359 -0
- package/src/tui/agents-cmd.ts +41 -0
- package/src/tui/app.ts +749 -0
- package/src/tui/attach.ts +127 -0
- package/src/tui/boot-notes.ts +41 -0
- package/src/tui/builtin-prompts.ts +59 -0
- package/src/tui/checkpoints-cmd.ts +70 -0
- package/src/tui/clipboard-image.ts +81 -0
- package/src/tui/clipboard.ts +78 -0
- package/src/tui/commands.ts +283 -0
- package/src/tui/config-view.ts +53 -0
- package/src/tui/context-cmds.ts +282 -0
- package/src/tui/cost.ts +108 -0
- package/src/tui/crash-guard.ts +173 -0
- package/src/tui/focus-terminal.ts +34 -0
- package/src/tui/git-cmds.ts +273 -0
- package/src/tui/git-plain.ts +58 -0
- package/src/tui/info-cmd.ts +150 -0
- package/src/tui/input-plain.ts +76 -0
- package/src/tui/mcp-cmd.ts +128 -0
- package/src/tui/memory-note.ts +77 -0
- package/src/tui/modes-cmd.ts +45 -0
- package/src/tui/notify-seq.ts +100 -0
- package/src/tui/notify.ts +318 -0
- package/src/tui/overlays.ts +97 -0
- package/src/tui/pi-renderer.ts +428 -0
- package/src/tui/providers-cmd.ts +377 -0
- package/src/tui/reasoning-view.ts +56 -0
- package/src/tui/renderer.ts +128 -0
- package/src/tui/replay-marker.ts +29 -0
- package/src/tui/session-cmd.ts +148 -0
- package/src/tui/session-manage.ts +95 -0
- package/src/tui/sextant-attach.ts +102 -0
- package/src/tui/sextant-io.ts +202 -0
- package/src/tui/sextant-smoke.ts +110 -0
- package/src/tui/shell-cmd.ts +158 -0
- package/src/tui/smoke.ts +72 -0
- package/src/tui/staged-terminal.ts +50 -0
- package/src/tui/startup.ts +12 -0
- package/src/tui/theme.ts +59 -0
- package/src/tui/todo-label.ts +7 -0
- package/src/tui/trust-card.ts +107 -0
- package/src/tui/tui-commands.ts +87 -0
- package/tsconfig.json +30 -0
- package/vendor/pi-tui/LICENSE +21 -0
- package/vendor/pi-tui/PATCHES.md +12 -0
- package/vendor/pi-tui/PROVENANCE.md +12 -0
- package/vendor/pi-tui/README.upstream.md +854 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
- package/vendor/pi-tui/src/autocomplete.ts +827 -0
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
- package/vendor/pi-tui/src/components/box.ts +138 -0
- package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
- package/vendor/pi-tui/src/components/editor.ts +2364 -0
- package/vendor/pi-tui/src/components/h-stack.ts +45 -0
- package/vendor/pi-tui/src/components/image.ts +128 -0
- package/vendor/pi-tui/src/components/input.ts +448 -0
- package/vendor/pi-tui/src/components/loader.ts +93 -0
- package/vendor/pi-tui/src/components/markdown.ts +1016 -0
- package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
- package/vendor/pi-tui/src/components/select-list.ts +230 -0
- package/vendor/pi-tui/src/components/settings-list.ts +277 -0
- package/vendor/pi-tui/src/components/spacer.ts +29 -0
- package/vendor/pi-tui/src/components/stack.ts +155 -0
- package/vendor/pi-tui/src/components/text.ts +108 -0
- package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
- package/vendor/pi-tui/src/components/v-stack.ts +34 -0
- package/vendor/pi-tui/src/editor-component.ts +75 -0
- package/vendor/pi-tui/src/fuzzy.ts +138 -0
- package/vendor/pi-tui/src/index.ts +149 -0
- package/vendor/pi-tui/src/keybindings.ts +321 -0
- package/vendor/pi-tui/src/keys.ts +1402 -0
- package/vendor/pi-tui/src/kill-ring.ts +47 -0
- package/vendor/pi-tui/src/latex.ts +1381 -0
- package/vendor/pi-tui/src/layout-node.ts +52 -0
- package/vendor/pi-tui/src/layout.ts +411 -0
- package/vendor/pi-tui/src/native-modifiers.ts +60 -0
- package/vendor/pi-tui/src/native-module-path.ts +32 -0
- package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
- package/vendor/pi-tui/src/terminal-colors.ts +74 -0
- package/vendor/pi-tui/src/terminal-image.ts +701 -0
- package/vendor/pi-tui/src/terminal.ts +554 -0
- package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
- package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
- package/vendor/pi-tui/src/tui.ts +1264 -0
- package/vendor/pi-tui/src/undo-stack.ts +29 -0
- package/vendor/pi-tui/src/utils.ts +1327 -0
- package/vendor/pi-tui/src/word-navigation.ts +118 -0
- package/vendor/pi-tui/test/test-themes.ts +39 -0
- package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
package/src/tui/app.ts
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
/** TUI chat app (port #1): wires the ONE agentLoop (ADR-003) into a Renderer.
|
|
2
|
+
* All vendor contact lives behind Renderer (renderer.ts) — swap-friendly. Slash handlers live
|
|
3
|
+
* beside it (ADR-002 cap): info-cmd.ts (/help /status /cost /skills /memory /export /todos
|
|
4
|
+
* /tasks), session-cmd.ts (/new /rewind /sessions /resume), checkpoints-cmd.ts, modes-cmd.ts. */
|
|
5
|
+
|
|
6
|
+
import { agentLoop, outstandingClause, outstandingTone } from "../core/loop.ts";
|
|
7
|
+
import { resetTurnFailureCount } from "../memory/tools.ts";
|
|
8
|
+
import { createRuntime } from "../cli/runtime.ts";
|
|
9
|
+
import { SandboxConfigError } from "../core/sandbox-config.ts";
|
|
10
|
+
import { SessionStore } from "../core/session.ts";
|
|
11
|
+
import { adoptLegacyMemory } from "../memory/scope.ts";
|
|
12
|
+
import { runMemoryNote } from "./memory-note.ts";
|
|
13
|
+
import { runShellLine, shellLine, type ShellCtx } from "./shell-cmd.ts";
|
|
14
|
+
import { cmdCommit, cmdUndo, type GitCmdCtx } from "./git-cmds.ts"; // port #65: /commit /undo
|
|
15
|
+
import { ModelCatalog } from "../providers/catalog.ts";
|
|
16
|
+
import { ModeManager, loadModesConfig, modeFromEntries, type AgentMode } from "../core/modes.ts";
|
|
17
|
+
import { isTerminal, taskNote } from "../core/tasks.ts";
|
|
18
|
+
import { togglePlanAct, applyModeToRun, flushModeSwitch } from "./modes-cmd.ts";
|
|
19
|
+
import { cmdCheckpoints, cmdRestore, type CheckpointCmdCtx } from "./checkpoints-cmd.ts";
|
|
20
|
+
import type { SessionCmdCtx } from "./session-cmd.ts";
|
|
21
|
+
import type { InfoCmdCtx } from "./info-cmd.ts";
|
|
22
|
+
import { todoLabel } from "./todo-label.ts";
|
|
23
|
+
import { cmdAttach, cmdPasteImage, carryOverAttachments, queuedAttachNote, userTurnLine, type AttachCtx } from "./attach.ts";
|
|
24
|
+
import { cmdConnect, cmdModel as cmdModelSwitch, cmdModels, cmdProvider, cmdSetup, listModelIds, watchProviders, MODEL_COMMAND, type ProviderCmdCtx } from "./providers-cmd.ts";
|
|
25
|
+
import { SETUP_PICKS } from "../cli/setup.ts";
|
|
26
|
+
import { isConfigured } from "../providers/provider-config.ts";
|
|
27
|
+
import { cmdMcp } from "./mcp-cmd.ts";
|
|
28
|
+
import { cmdTrustCard } from "./trust-card.ts";
|
|
29
|
+
import { ReasoningViews } from "./reasoning-view.ts";
|
|
30
|
+
import { cmdAgents } from "./agents-cmd.ts";
|
|
31
|
+
import { cmdConfigView } from "./config-view.ts";
|
|
32
|
+
import { runContextCommand, type ContextCmdCtx } from "./context-cmds.ts"; // port #53
|
|
33
|
+
import { emitBootNotes } from "./boot-notes.ts"; // boot-note extraction port
|
|
34
|
+
import { waitForStartup } from "./startup.ts";
|
|
35
|
+
import { acceptEditsNote, effortNote, modeSwitchNote, noModelHint } from "../core/voice.ts";
|
|
36
|
+
import { checkForUpdate, updateLine } from "../core/update-check.ts";
|
|
37
|
+
import pkg from "../../package.json";
|
|
38
|
+
import { rovecodeHome, saveCredential } from "../providers/auth.ts";
|
|
39
|
+
import { installCrashGuard } from "./crash-guard.ts";
|
|
40
|
+
import { compactionNote } from "./replay-marker.ts";
|
|
41
|
+
import { previewDiff } from "../coding/diff.ts";
|
|
42
|
+
import { discoverCommands, commandsForPalette, dispatchCustomCommand, type CustomCommandCtx } from "./commands.ts";
|
|
43
|
+
import type { Renderer, AssistantView, StatusInfo } from "./renderer.ts";
|
|
44
|
+
// pi-renderer.ts (and the vendored pi-tui under it) costs ~27 MB resident; a sextant session never
|
|
45
|
+
// constructs it, so it is required where it is constructed, not imported here (tests: pi-renderer-lazy)
|
|
46
|
+
type PiRendererMod = typeof import("./pi-renderer.ts");
|
|
47
|
+
import { buildSextantAttach, SEXTANT_LOCAL_NAMES } from "./sextant-attach.ts";
|
|
48
|
+
import type { ApprovalFn, ModelRef, PermissionLevel, RunEvent, ThinkingEffort } from "../core/types.ts";
|
|
49
|
+
import { thinkingLine } from "../providers/thinking.ts";
|
|
50
|
+
import { anthropicShapeFor } from "../providers/stream.ts";
|
|
51
|
+
import { parseEffort, THINKING_EFFORTS } from "../core/types.ts";
|
|
52
|
+
import { resolvePermission, saveSetting } from "../core/settings.ts";
|
|
53
|
+
import { TUI_COMMANDS, type TuiAppOptions } from "./tui-commands.ts"; // options/table extraction port
|
|
54
|
+
export { TUI_COMMANDS, type TuiAppOptions } from "./tui-commands.ts"; // existing import paths keep working
|
|
55
|
+
import { join } from "node:path";
|
|
56
|
+
|
|
57
|
+
export { buildCostNote } from "./cost.ts"; // moved for the ADR-002 cap; re-exported for tests
|
|
58
|
+
|
|
59
|
+
// lazy loaders — info-cmd and session-cmd are deferred until the first slash command
|
|
60
|
+
type InfoCmdMod = typeof import("./info-cmd.ts");
|
|
61
|
+
let _infoCmdMod: InfoCmdMod | null = null;
|
|
62
|
+
function lazyInfoCmd(): InfoCmdMod {
|
|
63
|
+
if (_infoCmdMod === null) {
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
65
|
+
_infoCmdMod = require("./info-cmd.ts") as InfoCmdMod;
|
|
66
|
+
}
|
|
67
|
+
return _infoCmdMod;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type SessionCmdMod = typeof import("./session-cmd.ts");
|
|
71
|
+
let _sessionCmdMod: SessionCmdMod | null = null;
|
|
72
|
+
function lazySessionCmd(): SessionCmdMod {
|
|
73
|
+
if (_sessionCmdMod === null) {
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
75
|
+
_sessionCmdMod = require("./session-cmd.ts") as SessionCmdMod;
|
|
76
|
+
}
|
|
77
|
+
return _sessionCmdMod;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type SessionManageMod = typeof import("./session-manage.ts");
|
|
81
|
+
let _sessionManageMod: SessionManageMod | null = null;
|
|
82
|
+
function lazySessionManage(): SessionManageMod {
|
|
83
|
+
if (_sessionManageMod === null) {
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
85
|
+
_sessionManageMod = require("./session-manage.ts") as SessionManageMod;
|
|
86
|
+
}
|
|
87
|
+
return _sessionManageMod;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface TuiState {
|
|
91
|
+
yolo: boolean;
|
|
92
|
+
/** the middle tier (port: Claude Code's acceptEdits): writes inside the workspace stop asking,
|
|
93
|
+
* shell/spawn/network and writes outside it still do. Ignored while `yolo` is on — auto already
|
|
94
|
+
* covers everything. Turned on by `/accept-edits`, `--accept-edits`, or the `all edits` button on
|
|
95
|
+
* a write approval card. */
|
|
96
|
+
acceptEdits: boolean;
|
|
97
|
+
provider: string; model: string; mode: AgentMode;
|
|
98
|
+
turns: number; tokensIn: number; tokensOut: number;
|
|
99
|
+
busy: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function runTui(opts: TuiAppOptions = {}): Promise<void> {
|
|
103
|
+
const _t0 = process.env.ROVECODE_TRACE_BOOT === "1" ? (Number(process.env._ROVECODE_BOOT_T0) || Date.now()) : -1;
|
|
104
|
+
const _trace = _t0 >= 0 ? (label: string) => process.stderr.write(`[boot] +${Date.now() - _t0}ms ${label}\n`) : (_: string) => {};
|
|
105
|
+
_trace("runTui entered");
|
|
106
|
+
opts.startup?.signal?.throwIfAborted();
|
|
107
|
+
opts.startup?.status("loading workspace");
|
|
108
|
+
// opts.sessionId is already the full id of an existing session, or undefined (see TuiAppOptions.sessionId)
|
|
109
|
+
const boot = { id: opts.sessionId, warn: opts.bootNote };
|
|
110
|
+
// opts.stream passes through verbatim: a StreamFn overrides, explicit null forces
|
|
111
|
+
// "no provider", undefined defers to the runtime's env-resolved provider
|
|
112
|
+
// port #27: a sandbox MISCONFIG throws synchronously here (before any side effect) — a clean
|
|
113
|
+
// one-line startup error (exit 2), never a stack, never a silent direct fallback. The rung
|
|
114
|
+
// PROBE verdict (rt.sandbox.ready) is awaited at the end of boot: runTui must stay
|
|
115
|
+
// synchronous until the renderer's input handlers are wired (tests/smoke send input right
|
|
116
|
+
// after calling runTui), so no await may sit above that point.
|
|
117
|
+
const rt = (() => {
|
|
118
|
+
try {
|
|
119
|
+
_trace("createRuntime start");
|
|
120
|
+
const r = createRuntime({ cwd: opts.cwd, stream: opts.stream, sessionId: boot.id, spawnRunner: opts.spawnRunner, platform: opts.platform, ...(opts.addDirs !== undefined ? { addDirs: opts.addDirs } : {}) });
|
|
121
|
+
_trace("createRuntime done");
|
|
122
|
+
return r;
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
if (e instanceof SandboxConfigError && opts.exitOnClose !== false) { console.error(`error: ${e.message}`); process.exit(2); }
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
const renderer: Renderer = opts.renderer ?? new (require("./pi-renderer.ts") as PiRendererMod).PiTuiRenderer({ cwd: rt.cwd }); // eslint-disable-line @typescript-eslint/no-require-imports
|
|
130
|
+
_trace("renderer created");
|
|
131
|
+
rt.setAskUser((q, signal) => renderer.askQuestion(q, signal)); // port #33: ask_user → the question overlay (Esc/abort dismisses it via signal)
|
|
132
|
+
const sessionsDir = join(rt.cwd, ".rovecode", "sessions");
|
|
133
|
+
// /cost pricing + context window. Boots from the offline snapshot; the live models.dev
|
|
134
|
+
// half is user-invoked only (/cost refresh), cached to .rovecode/cache with a 24h TTL —
|
|
135
|
+
// lookup() itself never fetches, so the TUI stays network-free unless asked.
|
|
136
|
+
const catalog = new ModelCatalog({ fetchFn: fetch, cacheDir: join(rt.cwd, ".rovecode", "cache") });
|
|
137
|
+
// session-scoped stores are swappable at runtime (/sessions, /rewind-to-root)
|
|
138
|
+
let store = rt.store;
|
|
139
|
+
let blocks = rt.blockStore;
|
|
140
|
+
// port #26: the runtime's ONE steering queue — background-task completion notes land on the
|
|
141
|
+
// next model turn; settled tasks also show in the transcript as they happen (failed → warn)
|
|
142
|
+
const steering = rt.steering;
|
|
143
|
+
rt.tasks.subscribe((t) => { if (isTerminal(t.status)) renderer.addSystemNote(taskNote(t), t.status === "failed" ? "warn" : "info"); });
|
|
144
|
+
// port #20: per-mode model slots from .rovecode/modes.json, restored from session entries
|
|
145
|
+
const modesCfg = loadModesConfig(rt.cwd);
|
|
146
|
+
const modes = new ModeManager(modesCfg, {
|
|
147
|
+
provider: rt.provider?.id ?? "mock",
|
|
148
|
+
model: opts.model ?? process.env.ROVECODE_MODEL ?? rt.defaultModel ?? "",
|
|
149
|
+
});
|
|
150
|
+
modes.restore(modeFromEntries(store.messages()) ?? modes.mode);
|
|
151
|
+
// port #30: custom slash commands — .rovecode/commands/*.md, project shadows ~/.rovecode/commands (commands.ts);
|
|
152
|
+
// LOW-1: /quit is a `case` alias of /exit below, not a TUI_COMMANDS entry — reserve it explicitly;
|
|
153
|
+
// port #44: the sextant surface's own /theme /open /diff /focus /agents never reach handleSlash — reserved too
|
|
154
|
+
// plugins (src/plugins): an ACTIVE plugin's commands folder is one more root, after the folder of its own
|
|
155
|
+
// scope — the same path setCommands feeds the palette and suggestions from, so nothing else changes
|
|
156
|
+
const pluginCommandDirs = rt.plugins.found.flatMap((p) => (p.status === "active" && p.commandsDir ? [{ dir: p.commandsDir, scope: p.scope }] : []));
|
|
157
|
+
const custom = discoverCommands(rt.cwd, { reserved: [...TUI_COMMANDS.map((c) => c.name), "quit", ...SEXTANT_LOCAL_NAMES], extraDirs: pluginCommandDirs });
|
|
158
|
+
if (opts.effort !== undefined) rt.setEffort(opts.effort);
|
|
159
|
+
// one resolved answer instead of two independent booleans: flag → env → project file → user file →
|
|
160
|
+
// "ask" (core/settings.ts). This is what makes `/yolo --save` survive the terminal closing.
|
|
161
|
+
// opts.permission is the flag rung for in-process callers; yolo/acceptEdits stay the boolean flags they were
|
|
162
|
+
// (true = demand, false = say nothing) — see TuiAppOptions.permission for why false cannot mean "ask"
|
|
163
|
+
const flagLevel: PermissionLevel | undefined = opts.permission ?? (opts.yolo === true ? "auto" : opts.acceptEdits === true ? "accept-edits" : undefined);
|
|
164
|
+
const startLevel = resolvePermission(rt.cwd, flagLevel, { ROVECODE_PERMISSION: process.env.ROVECODE_PERMISSION, ROVECODE_YOLO: process.env.ROVECODE_YOLO, ROVECODE_ACCEPT_EDITS: process.env.ROVECODE_ACCEPT_EDITS });
|
|
165
|
+
const state: TuiState = {
|
|
166
|
+
yolo: startLevel === "auto",
|
|
167
|
+
acceptEdits: startLevel === "accept-edits",
|
|
168
|
+
provider: modes.modelFor().provider,
|
|
169
|
+
model: modes.modelFor().model,
|
|
170
|
+
mode: modes.mode,
|
|
171
|
+
turns: 0, tokensIn: 0, tokensOut: 0, busy: false,
|
|
172
|
+
};
|
|
173
|
+
let run: AsyncGenerator<RunEvent> | null = null; let runAbort: AbortController | null = null; // port #21: one controller per run
|
|
174
|
+
let closed = false;
|
|
175
|
+
let resolveClosed: () => void = () => {};
|
|
176
|
+
const closedP = new Promise<void>((r) => { resolveClosed = r; });
|
|
177
|
+
|
|
178
|
+
const status = (): StatusInfo => {
|
|
179
|
+
const todos = todoLabel(join(sessionsDir, store.id)); // port #32: "todos done/total"; key omitted while the list is empty
|
|
180
|
+
return {
|
|
181
|
+
provider: state.provider, model: state.model, yolo: state.yolo, mode: state.mode,
|
|
182
|
+
permission: state.yolo ? "auto" : state.acceptEdits ? "accept-edits" : "ask",
|
|
183
|
+
effort: rt.effort,
|
|
184
|
+
turns: state.turns, tokensIn: state.tokensIn, tokensOut: state.tokensOut,
|
|
185
|
+
...(todos !== undefined ? { todos } : {}),
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
const pushStatus = () => renderer.setStatus(status());
|
|
189
|
+
|
|
190
|
+
/** set once the surface owns the screen (below renderer.start); puts Node's warning printer back */
|
|
191
|
+
let restoreWarnings: (() => void) | undefined;
|
|
192
|
+
/** set once renderer.start() has put the terminal into raw/alt mode (crash-guard.ts); cleared on a
|
|
193
|
+
* clean quit, which performs its own restore and its own process.exit — a guard still listening
|
|
194
|
+
* would restore a second time and log an "exit" it caused itself. */
|
|
195
|
+
let uninstallCrashGuard: (() => void) | null = null;
|
|
196
|
+
const close = (exitProcess = true) => {
|
|
197
|
+
if (closed) return;
|
|
198
|
+
closed = true;
|
|
199
|
+
uninstallCrashGuard?.(); uninstallCrashGuard = null;
|
|
200
|
+
// port #20 MED-2: /plan then quit resumes in plan (append is sync — lands pre-exit)
|
|
201
|
+
flushModeSwitch(modes, store);
|
|
202
|
+
runAbort?.abort(); // abort kills in-flight fetch/tools; return() settles the generator — kept, so the exit below waits for it
|
|
203
|
+
const settled = run?.return(undefined as never).then(() => undefined, () => undefined) ?? Promise.resolve();
|
|
204
|
+
const mapSettled = rt.stopRepoMapWarmup(); // no background scan or cache write after the app resolves
|
|
205
|
+
rt.tasks.cancelAll(); // port #26: background children die with the surface, never after it
|
|
206
|
+
const mcpSettled = rt.mcp?.close().catch(() => {}); // stop and drain MCP child processes/connections
|
|
207
|
+
restoreWarnings?.(); // the screen is going away; Node's own printer is the right one again
|
|
208
|
+
renderer.stop();
|
|
209
|
+
// port #29: session_close fires ONCE, after the aborted run settled and its in-flight on_event
|
|
210
|
+
// taps drained (hooks.close() waits for those) — cmdRun's exit() order; the app promise
|
|
211
|
+
// resolves (and the process exits) only after it, so a quit never outruns the hook
|
|
212
|
+
void (async () => {
|
|
213
|
+
await Promise.all([settled, mapSettled, mcpSettled]);
|
|
214
|
+
await rt.hooks.close().catch(() => {});
|
|
215
|
+
// the sextant renderer's git children (repo watcher) must be GONE before the process exits — on
|
|
216
|
+
// Windows a live child holds its cwd, so a scratch repo removed at quit throws EBUSY (fee2e8c root
|
|
217
|
+
// cause). Optional: the Renderer seam stays untouched; FakeRenderer and pi-tui have no drain()
|
|
218
|
+
await (renderer as { drain?: () => Promise<void> }).drain?.()?.catch(() => {});
|
|
219
|
+
resolveClosed();
|
|
220
|
+
if (exitProcess && opts.exitOnClose !== false) process.exit(0);
|
|
221
|
+
})();
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// both read the ACTIVE store live — /sessions and a root /rewind swap it (session-cmd.ts helpers)
|
|
225
|
+
const refreshUsage = () => { const u = lazySessionCmd().usageOf(store); state.tokensIn = u.tokensIn; state.tokensOut = u.tokensOut; };
|
|
226
|
+
const replayHistory = () => lazySessionCmd().replayTranscript(renderer, store);
|
|
227
|
+
// port #34: /attach context — the stage lives on the ACTIVE store (read live); the vision check uses the current mode's model
|
|
228
|
+
const attachCtx: AttachCtx = { renderer, cwd: rt.cwd, store: () => store, modelRef: () => modes.modelFor() };
|
|
229
|
+
|
|
230
|
+
const switchSession = (id: string, announce = true) => {
|
|
231
|
+
flushModeSwitch(modes, store); // port #20 MED-2: don't discard a pending switch on /sessions away
|
|
232
|
+
const pending = store.stagedAttachments; // port #34: the stage lives on the instance — re-staged on the new one below
|
|
233
|
+
store = new SessionStore(sessionsDir, id);
|
|
234
|
+
// memory is PROJECT-scoped (memory/scope.ts), so switching sessions KEEPS the store — it is the same project.
|
|
235
|
+
// The exception: a switched-to session that still carries a legacy per-session store copies it forward once,
|
|
236
|
+
// and then the prompt should show the migrated text now rather than next run, so that hands back a fresh store.
|
|
237
|
+
const adopted = adoptLegacyMemory(rt.cwd, sessionsDir, id);
|
|
238
|
+
if (adopted.blocks) blocks = adopted.blocks;
|
|
239
|
+
if (adopted.note !== null) for (const line of adopted.note.split("\n")) renderer.addSystemNote(line);
|
|
240
|
+
// rebind BOTH consumers: the memory tool AND the system prompt's memory block
|
|
241
|
+
// (critic finding: prompt kept reading the boot session's memory after /resume)
|
|
242
|
+
rt.setBlockStore(blocks);
|
|
243
|
+
rt.setSessionStore(store); // port #11: checkpoint entryId capture follows the active session
|
|
244
|
+
// port #20: the switched-to session resumes ITS last recorded mode
|
|
245
|
+
modes.restore(modeFromEntries(store.messages()) ?? modesCfg.defaultMode ?? "act");
|
|
246
|
+
const cur = modes.modelFor();
|
|
247
|
+
state.mode = modes.mode; state.model = cur.model; state.provider = cur.provider;
|
|
248
|
+
state.turns = 0;
|
|
249
|
+
replayHistory();
|
|
250
|
+
refreshUsage();
|
|
251
|
+
pushStatus();
|
|
252
|
+
if (announce) renderer.addSystemNote(`session ${id.slice(0, 8)} (${store.messages().length} messages)`);
|
|
253
|
+
carryOverAttachments(attachCtx, pending); // port #34: a swap must never lose staged images silently
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// port #11: checkpoint command context (store/busy read live via closures)
|
|
257
|
+
const cpCtx: CheckpointCmdCtx = {
|
|
258
|
+
renderer,
|
|
259
|
+
busy: () => state.busy,
|
|
260
|
+
sessionId: () => store.id,
|
|
261
|
+
checkpointsFor: (sid) => rt.checkpointsFor(sid),
|
|
262
|
+
branchTo: (entryId) => store.branch(entryId),
|
|
263
|
+
replayAndRefresh: () => { replayHistory(); refreshUsage(); pushStatus(); },
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// port #2: session navigation context (store read live via closure — /sessions and a root /rewind swap it)
|
|
267
|
+
const sessCtx: SessionCmdCtx = {
|
|
268
|
+
renderer,
|
|
269
|
+
cwd: rt.cwd, // /sessions delete removes the checkpoints shadow dir under the cwd as well (session-manage.ts)
|
|
270
|
+
sessionsDir,
|
|
271
|
+
busy: () => state.busy,
|
|
272
|
+
store: () => store,
|
|
273
|
+
switchSession,
|
|
274
|
+
replayHistory,
|
|
275
|
+
refreshUsage,
|
|
276
|
+
pushStatus,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// read-only info commands (info-cmd.ts) — store/blocks read live, the status slice is `state` itself
|
|
280
|
+
const infoCtx: InfoCmdCtx = {
|
|
281
|
+
renderer, rt, sessionsDir, catalog, state,
|
|
282
|
+
store: () => store, blocks: () => blocks,
|
|
283
|
+
commands: { builtin: TUI_COMMANDS, custom: custom.commands },
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// port #30: custom command dispatch context (submit = the plain user-turn path, defined below)
|
|
287
|
+
const cmdCtx: CustomCommandCtx = { renderer, modes, state, pushStatus, submit: (t) => submit(t) };
|
|
288
|
+
|
|
289
|
+
// port #53: context commands (/compact /clear /init /copy) — store/modes read live, like sessCtx;
|
|
290
|
+
// bindAbort is the app's interrupt target (runAbort), the same seam `!cmd` holds its child under
|
|
291
|
+
const ctxCmdCtx: ContextCmdCtx = {
|
|
292
|
+
renderer, cwd: rt.cwd,
|
|
293
|
+
busy: () => state.busy,
|
|
294
|
+
store: () => store,
|
|
295
|
+
modes, state,
|
|
296
|
+
defaultMode: modesCfg.defaultMode ?? "act",
|
|
297
|
+
buildCfg: () => rt.buildCfg(permissionLevel(), humanApprover()),
|
|
298
|
+
// no summarizer is wired on this tree (context-cmds.ts header): /compact falls back to
|
|
299
|
+
// keep-window and says so — the seam is here for #68's runtime wiring to fill
|
|
300
|
+
bindAbort: (ac) => { runAbort = ac; },
|
|
301
|
+
switchSession,
|
|
302
|
+
replayHistory,
|
|
303
|
+
refreshUsage,
|
|
304
|
+
pushStatus,
|
|
305
|
+
submit: (t) => submit(t),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// /model /models /provider (providers-cmd.ts) read the live registry; built lazily so pushStatus is bound
|
|
309
|
+
const provCtx = (): ProviderCmdCtx => ({ rt, modes, state, renderer, pushStatus, catalog }); // port #60: existing picker reads /cost metadata
|
|
310
|
+
/** `--save` writes the level the toggles just produced, so the next launch starts there;
|
|
311
|
+
* `--project` pins it to this checkout instead of to you. Without --save nothing is written —
|
|
312
|
+
* a toggle you meant for one run must not follow you into the next. */
|
|
313
|
+
const persistLevel = (arg: string): string => {
|
|
314
|
+
const words = arg.split(/\s+/).filter((w) => w.length > 0);
|
|
315
|
+
if (!words.includes("--save")) return "this session only — add --save to make it the default (--project pins it to this repo)";
|
|
316
|
+
const scope = words.includes("--project") ? "project" : "user";
|
|
317
|
+
const level: PermissionLevel = state.yolo ? "auto" : state.acceptEdits ? "accept-edits" : "ask";
|
|
318
|
+
try {
|
|
319
|
+
const path = saveSetting("permission", level, scope, rt.cwd);
|
|
320
|
+
return `saved: ${level} is the default now (${path})`;
|
|
321
|
+
} catch (e) {
|
|
322
|
+
return `could not save it: ${e instanceof Error ? e.message : String(e)}`;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/** what the CURRENT model's endpoint receives for a level — the /effort note's second line (providers/thinking.ts).
|
|
327
|
+
* Built like buildDef builds a ref (catalog reasoning flag), without touching the runtime's active model. */
|
|
328
|
+
const receives = (level: ThinkingEffort): string => {
|
|
329
|
+
const info = catalog.lookup(state.provider, state.model);
|
|
330
|
+
const ref: ModelRef = { provider: state.provider, model: state.model, effort: level, ...(info?.supportsReasoning !== undefined ? { reasoning: info.supportsReasoning } : {}) };
|
|
331
|
+
return thinkingLine(ref, rt.providers.get(state.provider)?.protocol ?? "openai", { shape: anthropicShapeFor(ref) });
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const handleSlash = (text: string): boolean => {
|
|
335
|
+
const [cmd, ...rest] = text.slice(1).split(/\s+/);
|
|
336
|
+
const arg = rest.join(" ").trim();
|
|
337
|
+
switch (cmd) {
|
|
338
|
+
case "exit": case "quit": close(); return true;
|
|
339
|
+
case "help": lazyInfoCmd().cmdHelp(infoCtx); return true;
|
|
340
|
+
case "effort": {
|
|
341
|
+
const want = arg.trim();
|
|
342
|
+
if (want.length === 0) { renderer.addSystemNote(effortNote(rt.effort, receives(rt.effort))); return true; }
|
|
343
|
+
const level = parseEffort(want);
|
|
344
|
+
if (level === undefined) { renderer.addSystemNote(`"${want}" is not a level — ${THINKING_EFFORTS.join(" · ")}`, "warn"); return true; }
|
|
345
|
+
rt.setEffort(level);
|
|
346
|
+
renderer.addSystemNote(effortNote(level, receives(level)));
|
|
347
|
+
pushStatus(); return true;
|
|
348
|
+
}
|
|
349
|
+
case "accept-edits":
|
|
350
|
+
state.acceptEdits = !state.acceptEdits;
|
|
351
|
+
renderer.addSystemNote(state.yolo
|
|
352
|
+
? `${acceptEditsNote(state.acceptEdits)} (auto mode is on, so nothing asks either way — /yolo turns it off)`
|
|
353
|
+
: acceptEditsNote(state.acceptEdits));
|
|
354
|
+
renderer.addSystemNote(persistLevel(arg));
|
|
355
|
+
pushStatus(); return true;
|
|
356
|
+
case "yolo":
|
|
357
|
+
state.yolo = !state.yolo;
|
|
358
|
+
renderer.addSystemNote(modeSwitchNote(state.yolo)); // "ask first" / "auto (never asks)" — the flag keeps its name
|
|
359
|
+
renderer.addSystemNote(persistLevel(arg));
|
|
360
|
+
pushStatus(); return true;
|
|
361
|
+
// port #20: model writes land in the CURRENT mode's slot (mirrored to both when
|
|
362
|
+
// planActSeparateModels is off); the selector may name another provider — the registry's
|
|
363
|
+
// dispatcher routes per call, so the switch needs no restart. --save persists the default.
|
|
364
|
+
case "model": cmdModelSwitch(provCtx(), arg); return true;
|
|
365
|
+
case "models": void cmdModels(provCtx(), arg); return true;
|
|
366
|
+
case "provider": void cmdProvider(provCtx(), arg); return true;
|
|
367
|
+
case "setup": void cmdSetup(provCtx()); return true; // the classic chain; the cockpit answers /setup with the wizard before handleSlash runs
|
|
368
|
+
// the same job on one line (cli/connect.ts through the live registry); bare, it hands over to /setup
|
|
369
|
+
case "connect": void cmdConnect(provCtx(), arg); return true;
|
|
370
|
+
case "plan": case "act":
|
|
371
|
+
togglePlanAct(modes, cmd as AgentMode, state, renderer, pushStatus);
|
|
372
|
+
return true;
|
|
373
|
+
case "checkpoints": void cmdCheckpoints(cpCtx); return true;
|
|
374
|
+
case "restore": void cmdRestore(cpCtx, arg); return true;
|
|
375
|
+
case "commit": void cmdCommit(gitCtx, text.slice(1 + (cmd ?? "").length)); return true; // port #65: RAW remainder — a message body keeps its newlines
|
|
376
|
+
case "undo": void cmdUndo(gitCtx); return true; // port #65
|
|
377
|
+
case "status": lazyInfoCmd().cmdStatus(infoCtx); return true;
|
|
378
|
+
case "cost": lazyInfoCmd().cmdCost(infoCtx, arg); return true;
|
|
379
|
+
case "skills": lazyInfoCmd().cmdSkills(infoCtx); return true;
|
|
380
|
+
case "memory": lazyInfoCmd().cmdMemory(infoCtx, arg); return true; // bare = show both blocks; `--user` one; with text = append (memory-note.ts)
|
|
381
|
+
case "todos": lazyInfoCmd().cmdTodos(infoCtx); return true; // port #32
|
|
382
|
+
case "tasks": lazyInfoCmd().cmdTasks(infoCtx, arg); return true; // port #26
|
|
383
|
+
case "new": lazySessionCmd().cmdNew(sessCtx); return true;
|
|
384
|
+
case "rewind": case "tree": void lazySessionCmd().cmdRewind(sessCtx); return true;
|
|
385
|
+
case "sessions": void lazySessionManage().cmdSessionsVerb(sessCtx, arg); return true; // bare = the picker; rename/delete/fork/search = session-manage.ts
|
|
386
|
+
case "compact": case "clear": case "init": case "copy": void runContextCommand(ctxCmdCtx, cmd ?? "", arg); return true; // port #53
|
|
387
|
+
case "trust": void cmdTrustCard({ renderer, cwd: rt.cwd }, arg); return true; // the project trust gate, answerable without quitting (trust-card.ts)
|
|
388
|
+
case "agents": cmdAgents({ renderer, agents: rt.agents }); return true; // the definitions list (port #62); the BARE /agents is the sextant crew board (local-commands.ts pass-through: only "/agents list" reaches here)
|
|
389
|
+
case "config": cmdConfigView({ renderer, cwd: rt.cwd }, arg); return true; // the read-only settings view (config-view.ts): what THIS run actually loads
|
|
390
|
+
case "resume":
|
|
391
|
+
if (arg) void lazySessionCmd().cmdSessions(sessCtx, arg); else void lazySessionCmd().cmdSessions(sessCtx);
|
|
392
|
+
return true;
|
|
393
|
+
case "export": lazyInfoCmd().cmdExport(infoCtx, arg); return true;
|
|
394
|
+
case "attach": cmdAttach(attachCtx, arg); return true; // port #34
|
|
395
|
+
case "paste": cmdPasteImage(attachCtx); return true; // clipboard image → attachment (⌃v)
|
|
396
|
+
case "mcp": void cmdMcp({ renderer, cwd: rt.cwd }, arg); return true; // the MCP market (mcp-cmd.ts): palette → approval card → mcp.json
|
|
397
|
+
default:
|
|
398
|
+
// port #30: a discovered custom command renders its template and submits it as a user turn.
|
|
399
|
+
// MED-2: it gets the RAW remainder of the line (whitespace runs and pasted newlines intact —
|
|
400
|
+
// renderCommand trims the ends itself); built-ins keep the collapsed `arg` above.
|
|
401
|
+
if (!dispatchCustomCommand(cmdCtx, custom.commands, cmd ?? "", text.slice(1 + (cmd ?? "").length))) renderer.addSystemNote(`unknown command: /${cmd} (try /help)`, "warn");
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
/** the session's permission level as a model turn would run under */
|
|
407
|
+
const permissionLevel = (): PermissionLevel => state.yolo ? "auto" : state.acceptEdits ? "accept-edits" : "ask";
|
|
408
|
+
|
|
409
|
+
/** THE human approver — one implementation for a model-issued call and for the person's own `!cmd`, so the diff
|
|
410
|
+
* preview and the "all edits" door behave identically either way. undefined under `auto`: nothing is asked. */
|
|
411
|
+
const humanApprover = (): ApprovalFn | undefined => state.yolo ? undefined : async (req) => {
|
|
412
|
+
// port #24: edit/write approvals carry a bounded unified diff of the pending change
|
|
413
|
+
// (in-memory preview; any failure degrades to the plain overlay, never blocks the ask)
|
|
414
|
+
const isEdit = req.tool === "edit" || req.tool === "write";
|
|
415
|
+
// `all edits` pressed DURING this run: the rules were built before it, so the switch is honored
|
|
416
|
+
// here too — otherwise the mode would only start at the next prompt, which is not what the
|
|
417
|
+
// button says. The rules still gate the call; this only skips the card.
|
|
418
|
+
if (isEdit && state.acceptEdits) return "once";
|
|
419
|
+
let detail: string | undefined;
|
|
420
|
+
if (isEdit) {
|
|
421
|
+
try { detail = previewDiff(req.tool as "edit" | "write", req.revisedArgs, rt.cwd).text || undefined; } catch { detail = undefined; }
|
|
422
|
+
}
|
|
423
|
+
const answer = await renderer.askApproval(req.tool, JSON.stringify(req.revisedArgs).slice(0, 140), detail);
|
|
424
|
+
if (answer !== "all-edits") return answer;
|
|
425
|
+
// the surface-level door: flip the session and let THIS call through once. The core approval
|
|
426
|
+
// engine stays a three-verdict system — "all-edits" never crosses into it.
|
|
427
|
+
state.acceptEdits = true;
|
|
428
|
+
renderer.addSystemNote(acceptEditsNote(true));
|
|
429
|
+
pushStatus();
|
|
430
|
+
return "once";
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
/** `!cmd` (tui/shell-cmd.ts): the person's own command, through the same dispatch, rules and approver */
|
|
434
|
+
const shellCtx: ShellCtx = {
|
|
435
|
+
renderer, rt, store: () => store,
|
|
436
|
+
level: permissionLevel,
|
|
437
|
+
approve: humanApprover,
|
|
438
|
+
busy: () => state.busy,
|
|
439
|
+
setBusy: (b) => { state.busy = b; pushStatus(); },
|
|
440
|
+
bindAbort: (ac) => { runAbort = ac; },
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// port #65 (tui/git-cmds.ts): /commit and /undo run on the SAME seams as `!cmd` — one busy flag,
|
|
444
|
+
// one abort binding, one approver — plus the router (the COMMIT role drafts the message) and the
|
|
445
|
+
// session's checkpoints (/undo restores through them; the conversation is left to /restore).
|
|
446
|
+
const gitCtx: GitCmdCtx = {
|
|
447
|
+
renderer, rt, store: () => store,
|
|
448
|
+
approve: humanApprover,
|
|
449
|
+
yolo: () => state.yolo,
|
|
450
|
+
busy: () => state.busy,
|
|
451
|
+
setBusy: (b) => { state.busy = b; pushStatus(); },
|
|
452
|
+
bindAbort: (ac) => { runAbort = ac; },
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const startRun = async (goal: string) => {
|
|
456
|
+
const stream = rt.stream; // runtime already applied any opts.stream override
|
|
457
|
+
// live check: /provider add + /provider key (or `rovecode provider add` in another terminal) clears
|
|
458
|
+
// it for the next prompt — no restart
|
|
459
|
+
const reason = rt.noProviderReason();
|
|
460
|
+
if (!stream || reason !== null) {
|
|
461
|
+
renderer.addSystemNote(reason !== null ? noModelHint("tui") : "no provider stream", "error");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
state.busy = true;
|
|
465
|
+
renderer.setBusy(true, "thinking…");
|
|
466
|
+
pushStatus();
|
|
467
|
+
const level = permissionLevel();
|
|
468
|
+
const cfg = rt.buildCfg(level, humanApprover());
|
|
469
|
+
// port #20: per-mode model resolution + plan-mode rule/prompt enforcement
|
|
470
|
+
const cur = modes.modelFor();
|
|
471
|
+
const def = rt.buildDef({ provider: cur.provider, model: cur.model });
|
|
472
|
+
applyModeToRun(modes, cfg, def);
|
|
473
|
+
const views = new Map<string, AssistantView>();
|
|
474
|
+
const reasoning = new ReasoningViews(renderer); // port #60: one collapsed reasoning block per assistant message
|
|
475
|
+
let lastView: AssistantView | null = null;
|
|
476
|
+
runAbort = new AbortController();
|
|
477
|
+
// port #26: Esc/quit cancel the background tasks THIS run starts; a normal end leaves them running.
|
|
478
|
+
// The label is the run's own prompt: it titles the crew card that groups this run's agents
|
|
479
|
+
// (sextant/crew-cards.ts). tasks.ts one-lines and clips it — a surface passes what the person typed.
|
|
480
|
+
rt.tasks.bindRun(runAbort.signal, { label: goal });
|
|
481
|
+
run = agentLoop(def, goal, {}, cfg, {
|
|
482
|
+
stream, registry: rt.registry, store,
|
|
483
|
+
tools: rt.registry.list().map((t) => t.schema),
|
|
484
|
+
guard: rt.guard, planReminder: rt.planReminder, signal: runAbort.signal, // port #21: Esc aborts this run's controller
|
|
485
|
+
cwd: rt.cwd, // cwd must be threaded — tools resolve relative paths against it, same as checkpoints/LSP/preview
|
|
486
|
+
hooks: rt.hooks, // port #29: pre_tool/approval/post_tool at dispatch, pre_run/compaction/post_run/on_event via the loop observer
|
|
487
|
+
}, steering);
|
|
488
|
+
try {
|
|
489
|
+
for await (const ev of run) {
|
|
490
|
+
renderer.onEvent?.(ev); // port #44: FIRST — the sextant reducer is its rows' source of truth; the calls below are duplicates it ignores while busy
|
|
491
|
+
reasoning.onEvent(ev); // port #60: reasoning_update → the collapsed block (settled when the text starts)
|
|
492
|
+
if (ev.type === "turn_start") { resetTurnFailureCount(); state.turns++; pushStatus(); } // pushStatus here + in the finally also refreshes the port #32 todo label after a todo_write
|
|
493
|
+
else if (ev.type === "message_update") {
|
|
494
|
+
let v = views.get(ev.messageId);
|
|
495
|
+
if (!v) { v = renderer.beginAssistant(); views.set(ev.messageId, v); lastView?.done(); lastView = v; }
|
|
496
|
+
v.append(ev.delta);
|
|
497
|
+
} else if (ev.type === "tool_execution_start") {
|
|
498
|
+
renderer.toolStart(ev.callId, ev.tool, JSON.stringify(ev.args).slice(0, 120));
|
|
499
|
+
} else if (ev.type === "tool_execution_update") {
|
|
500
|
+
renderer.toolUpdate(ev.callId, ev.note);
|
|
501
|
+
} else if (ev.type === "tool_execution_end") {
|
|
502
|
+
renderer.toolEnd(ev.callId, ev.ok, ev.output.slice(0, 160).replace(/\n/g, " ⏎ "), ev.durationMs);
|
|
503
|
+
} else if (ev.type === "tool_call_failed") {
|
|
504
|
+
renderer.toolEnd(ev.callId, false, `${ev.reason}: ${ev.detail}`.slice(0, 160), 0);
|
|
505
|
+
} else if (ev.type === "compaction") {
|
|
506
|
+
renderer.addSystemNote(compactionNote(ev)); // one wording with the replayed marker (port #25 LOW-4)
|
|
507
|
+
} else if (ev.type === "steer") {
|
|
508
|
+
renderer.addSystemNote("↪ steering applied");
|
|
509
|
+
} else if (ev.type === "verify") {
|
|
510
|
+
// the verify gate (core/verify-gate.ts): say that the check is running — a silent two minutes reads as a hang
|
|
511
|
+
renderer.addSystemNote(ev.state === "running" ? `⧗ verify: ${ev.command.slice(0, 120)}` : `verify ${ev.state}: ${ev.detail ?? ""}`, ev.state === "running" || ev.state === "passed" ? "info" : "warn");
|
|
512
|
+
} else if (ev.type === "run_end") {
|
|
513
|
+
lastView?.done();
|
|
514
|
+
if (ev.status === "error") renderer.addSystemNote(ev.summary, "error");
|
|
515
|
+
else if (ev.status !== "done") renderer.addSystemNote(`run ${ev.status}: ${ev.summary}`, "warn");
|
|
516
|
+
else if (ev.outstanding) { const c = outstandingClause(ev.outstanding); if (c !== null) renderer.addSystemNote(`done · ${c}`, outstandingTone(ev.outstanding)); } // "done" ≠ finished: say what was left (core/loop.ts)
|
|
517
|
+
// if the model produced no streaming deltas, surface the final text
|
|
518
|
+
if (views.size === 0 && ev.status === "done" && ev.summary) {
|
|
519
|
+
const v = renderer.beginAssistant(); v.append(ev.summary); v.done();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} finally {
|
|
524
|
+
run = null; runAbort = null;
|
|
525
|
+
state.busy = false;
|
|
526
|
+
refreshUsage();
|
|
527
|
+
// port #14: surface any fallback-chain advances the router made during the run
|
|
528
|
+
for (const n of rt.drainRouterNotes()) renderer.addSystemNote(n, "warn");
|
|
529
|
+
renderer.setBusy(false);
|
|
530
|
+
pushStatus();
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/** A plain user turn — also the path custom commands submit their rendered prompt through (port #30). */
|
|
535
|
+
const submit = (text: string): Promise<void> => {
|
|
536
|
+
// port #34: text is required. The editor drops an empty Enter before onSubmit (pi-renderer.ts),
|
|
537
|
+
// but a custom command whose template renders to "" (`/ask` on a bare `$ARGUMENTS`) lands here:
|
|
538
|
+
// an empty goal never starts a run, is never queued as a steer, and leaves the stage (and any
|
|
539
|
+
// pending mode switch) for the next real message
|
|
540
|
+
if (!text.trim()) {
|
|
541
|
+
const n = store.stagedAttachments.length;
|
|
542
|
+
renderer.addSystemNote(n === 0 ? "nothing to send — the message is empty" : `type a message to send with the attached image${n === 1 ? "" : "s"}`, "warn");
|
|
543
|
+
return Promise.resolve();
|
|
544
|
+
}
|
|
545
|
+
renderer.addUser(userTurnLine(text, store.stagedAttachments)); // port #34: image chips under the text — the stage folds into this message
|
|
546
|
+
// port #20: a pending mode switch becomes a durable session entry on the next
|
|
547
|
+
// submit (round-trip cancellation: toggling back before submitting records nothing)
|
|
548
|
+
flushModeSwitch(modes, store);
|
|
549
|
+
if (state.busy) { steering.push(text); renderer.addSystemNote(`queued as steering (applies before the next model turn)${queuedAttachNote(store)}`); return Promise.resolve(); }
|
|
550
|
+
return startRun(text);
|
|
551
|
+
};
|
|
552
|
+
const failedStartup = async (e: unknown): Promise<never> => {
|
|
553
|
+
opts.startup?.finish();
|
|
554
|
+
close(false);
|
|
555
|
+
await closedP;
|
|
556
|
+
if (e instanceof SandboxConfigError && opts.exitOnClose !== false) { console.error(`error: ${e.message}`); process.exit(2); }
|
|
557
|
+
throw e;
|
|
558
|
+
};
|
|
559
|
+
const ready = async (): Promise<void> => {
|
|
560
|
+
try { await waitForStartup(rt.sandbox.ready, opts.startup?.signal); }
|
|
561
|
+
catch (e) { await failedStartup(e); }
|
|
562
|
+
};
|
|
563
|
+
// CLI startup keeps the intro on screen through the required probe. Embedders without a loading
|
|
564
|
+
// screen retain the synchronous input-wiring contract (tests may type immediately after runTui()).
|
|
565
|
+
if (opts.startup) { opts.startup.status("checking sandbox"); await ready(); opts.startup.status("preparing interface"); }
|
|
566
|
+
// port #44: a renderer with panels (sextant) reads the runtime through this handle — once, before start()
|
|
567
|
+
_trace("renderer.attach");
|
|
568
|
+
renderer.attach?.(buildSextantAttach({
|
|
569
|
+
cwd: rt.cwd, sessionsDir, store: () => store, tasks: rt.tasks, model: () => modes.modelFor(), catalog, runtime: () => rt, petName: opts.pet,
|
|
570
|
+
// the connect wizard's registry seams (draw-wizard.ts): the same ones /setup walks — SETUP_PICKS
|
|
571
|
+
// for the rows, saveCredential + refresh for the key, reg.models for the list, probe + setDefault
|
|
572
|
+
// for the test. Classic keeps cmdSetup's pickOne chain; only the sextant surface opens the wizard.
|
|
573
|
+
wizard: {
|
|
574
|
+
// the LIVE registry is the list: configured providers first (the ones one ⏎ from working),
|
|
575
|
+
// then every other row the registry knows (built-ins without a key, custom rows like a proxy
|
|
576
|
+
// added with /provider add), then the two "your own URL" doors. SETUP_PICKS alone hid the
|
|
577
|
+
// person's own rows — a default of kaesra or an hn added by hand never appeared, and the
|
|
578
|
+
// walk had no way to reach them.
|
|
579
|
+
providers: () => {
|
|
580
|
+
const rows: { key: string; label: string; configured?: boolean; local?: boolean; url?: "openai" | "anthropic" }[] = [];
|
|
581
|
+
const seen = new Set<string>();
|
|
582
|
+
const ordered = [...rt.providers.list()].sort((a, b) => Number(isConfigured(b)) - Number(isConfigured(a)));
|
|
583
|
+
for (const p of ordered) {
|
|
584
|
+
if (seen.has(p.id) || p.id === "custom") continue; // the env ROVECODE_BASE_URL pair is not a wizard row
|
|
585
|
+
seen.add(p.id);
|
|
586
|
+
rows.push({ key: p.id, label: p.id === "kaesra" ? "kaesra — the default host" : p.id, ...(isConfigured(p) ? { configured: true } : {}), ...(p.noKey === true ? { local: true } : {}) });
|
|
587
|
+
}
|
|
588
|
+
for (const p of SETUP_PICKS) if (p.url !== undefined) rows.push({ key: p.key, label: p.label, url: p.url });
|
|
589
|
+
return rows;
|
|
590
|
+
},
|
|
591
|
+
// a url door (SETUP_PICKS 8/9): register the row FIRST — without it, KEY had "unknown provider 8"
|
|
592
|
+
register: (id, url, protocol) => {
|
|
593
|
+
const r = rt.providers.add({ id, baseUrl: url, protocol }, "user");
|
|
594
|
+
return "error" in r ? { ok: false, error: r.error } : { ok: true };
|
|
595
|
+
},
|
|
596
|
+
storeKey: (provider, secret) => {
|
|
597
|
+
try {
|
|
598
|
+
// a registry row's key IS its id; a url door was rewritten to its id at registration
|
|
599
|
+
const row = rt.providers.get(provider);
|
|
600
|
+
const id = row?.id ?? provider;
|
|
601
|
+
if (row === undefined) return { ok: false, error: `unknown provider "${provider}"` };
|
|
602
|
+
saveCredential(row.id, secret, row.keyEnv);
|
|
603
|
+
rt.providers.refresh();
|
|
604
|
+
return { ok: true };
|
|
605
|
+
} catch (e) { return { ok: false, error: e instanceof Error ? e.message : String(e) }; }
|
|
606
|
+
},
|
|
607
|
+
models: async (provider) => {
|
|
608
|
+
const id = provider; // a registry row's key is its id; a door key never reaches here (it registers first)
|
|
609
|
+
const row = rt.providers.get(id);
|
|
610
|
+
// each row carries the provider's OWN facts so the step's checked set can seed from them:
|
|
611
|
+
// `active` = pinned today (spec.models), `defaultModel` = the row's default. The list itself
|
|
612
|
+
// is the same source /models reads — a pinned provider serves its file list, an unpinned one
|
|
613
|
+
// its endpoint — so the rows and what /models would answer cannot disagree.
|
|
614
|
+
const pinned = new Set(row?.models ?? []);
|
|
615
|
+
const def = row?.defaultModel;
|
|
616
|
+
const rows: { id: string; note?: string; active?: boolean; defaultModel?: boolean }[] = [];
|
|
617
|
+
const mark = (m: string): void => {
|
|
618
|
+
rows.push({ id: m, ...(pinned.has(m) ? { active: true } : {}), ...(m === def ? { defaultModel: true, note: "default" } : {}) });
|
|
619
|
+
};
|
|
620
|
+
const r = await rt.providers.models(id);
|
|
621
|
+
if (r.ok) { for (const m of r.models) if (!rows.some((x) => x.id === m)) mark(m); }
|
|
622
|
+
// a pinned list the endpoint no longer names still shows (the file is the truth until edited);
|
|
623
|
+
// fetch failed AND nothing pinned: an empty list → the caller falls back to typing the id
|
|
624
|
+
for (const m of pinned) if (!rows.some((x) => x.id === m)) mark(m);
|
|
625
|
+
return rows;
|
|
626
|
+
},
|
|
627
|
+
// the model step's ACTIVATE answer: pin the checked rows as the provider's active list
|
|
628
|
+
// (spec.models — /models serves them from the file after this), or un-pin with an empty
|
|
629
|
+
// list ("use them all": the endpoint is asked every time, nothing frozen). `makeDefault`
|
|
630
|
+
// also becomes the row's default and the session's model, like the test step does.
|
|
631
|
+
activateModels: (provider, models, makeDefault) => {
|
|
632
|
+
const id = provider;
|
|
633
|
+
if (rt.providers.get(id) === undefined) return { ok: false, error: "no provider behind this row" };
|
|
634
|
+
const r = rt.providers.setModels(id, models, "user");
|
|
635
|
+
if ("error" in r) return { ok: false, error: r.error };
|
|
636
|
+
if (makeDefault !== undefined) {
|
|
637
|
+
const d = rt.providers.setDefault(`${id}/${makeDefault}`, "user");
|
|
638
|
+
if (!("error" in d)) {
|
|
639
|
+
modes.setModel({ provider: d.provider, model: d.model });
|
|
640
|
+
state.provider = modes.modelFor().provider;
|
|
641
|
+
state.model = modes.modelFor().model;
|
|
642
|
+
pushStatus();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return { ok: true, ...r };
|
|
646
|
+
},
|
|
647
|
+
test: async (provider, model) => {
|
|
648
|
+
const id = provider;
|
|
649
|
+
if (rt.providers.get(id) === undefined) return { ok: false, detail: "no provider behind this row" };
|
|
650
|
+
const probe = await rt.providers.probe(id, model);
|
|
651
|
+
if (!probe.ok) return { ok: false, detail: probe.detail };
|
|
652
|
+
const d = rt.providers.setDefault(`${id}/${model}`, "user");
|
|
653
|
+
if ("error" in d) return { ok: false, detail: d.error };
|
|
654
|
+
// the session switches with the default (providers-cmd applyRef's three lines, inlined —
|
|
655
|
+
// that helper needs a ProviderCmdCtx; the wizard seam already holds the same pieces)
|
|
656
|
+
modes.setModel({ provider: d.provider, model: d.model });
|
|
657
|
+
state.provider = modes.modelFor().provider;
|
|
658
|
+
state.model = modes.modelFor().model;
|
|
659
|
+
pushStatus();
|
|
660
|
+
return { ok: true, detail: `${probe.detail} — default → ${id}/${model}` };
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
}));
|
|
664
|
+
// /model suggestions: the ids of every configured provider's models, fetched off the boot path and again
|
|
665
|
+
// whenever the registry changes; the sextant reads the list at suggestion time (SlashCommand.choices)
|
|
666
|
+
const modelChoices: string[] = [];
|
|
667
|
+
const refreshModelChoices = (): void => { void listModelIds(rt.providers).then((ids) => { modelChoices.splice(0, modelChoices.length, ...ids); }).catch(() => {}); };
|
|
668
|
+
renderer.setCommands([...TUI_COMMANDS.map((c) => (c.name === MODEL_COMMAND.name ? { ...c, choices: () => modelChoices } : c)), ...commandsForPalette(custom.commands)]);
|
|
669
|
+
setTimeout(refreshModelChoices, 0);
|
|
670
|
+
// Node prints warnings on stderr, and stderr goes straight onto the alternate screen. A
|
|
671
|
+
// MaxListenersExceededWarning does not just say its sentence — it dumps the emitter it is complaining
|
|
672
|
+
// about, which for a stream is pages of `[Function: …]`, over the panels. Berkay hit exactly that
|
|
673
|
+
// during a Playwright MCP session (the leak itself is fixed in mcp/client.ts; this is the other half:
|
|
674
|
+
// no warning from anywhere should be able to garble the screen). Node's own printer is removed and the
|
|
675
|
+
// warning becomes a note — still said, never drawn over anything — and put back on the way out.
|
|
676
|
+
const nodeWarnListeners = process.listeners("warning");
|
|
677
|
+
// assigned here, read by `close` above (declared before this point, called only after start)
|
|
678
|
+
process.removeAllListeners("warning");
|
|
679
|
+
const onWarning = (w: Error): void => {
|
|
680
|
+
// the first line only: a MaxListenersExceededWarning's body is the emitter it is complaining about
|
|
681
|
+
const first = w.message.split("\n")[0] ?? w.message;
|
|
682
|
+
renderer.addSystemNote(`node: ${w.name === "Warning" ? "" : `${w.name}: `}${first}`, "warn");
|
|
683
|
+
};
|
|
684
|
+
process.on("warning", onWarning);
|
|
685
|
+
restoreWarnings = (): void => {
|
|
686
|
+
process.off("warning", onWarning);
|
|
687
|
+
for (const l of nodeWarnListeners) process.on("warning", l as (w: Error) => void);
|
|
688
|
+
};
|
|
689
|
+
let seeded = false;
|
|
690
|
+
const seed = (): void => {
|
|
691
|
+
if (seeded) return;
|
|
692
|
+
seeded = true;
|
|
693
|
+
if (boot.id !== undefined) { replayHistory(); refreshUsage(); }
|
|
694
|
+
emitBootNotes({ renderer, rt, yolo: state.yolo, modelRef: state, mode: state.mode, version: pkg.version, width: process.stdout.columns ?? 80, sessionId: boot.id !== undefined ? store.id : undefined, bootWarn: boot.warn, commandWarnings: custom.warnings });
|
|
695
|
+
pushStatus();
|
|
696
|
+
};
|
|
697
|
+
let revealed = false;
|
|
698
|
+
const reveal = (): void => {
|
|
699
|
+
if (revealed) return;
|
|
700
|
+
opts.startup?.signal?.throwIfAborted();
|
|
701
|
+
revealed = true;
|
|
702
|
+
_trace("complete first frame ready; releasing intro");
|
|
703
|
+
opts.startup?.finish();
|
|
704
|
+
};
|
|
705
|
+
_trace("renderer.start");
|
|
706
|
+
try {
|
|
707
|
+
const starting = renderer.start({
|
|
708
|
+
// `#<text>` is a memory note, not a prompt: it short-circuits before any model turn (tui/memory-note.ts),
|
|
709
|
+
// the way `/` does. The classifier is narrow on purpose — a pasted `#include` block or a `# heading` is a
|
|
710
|
+
// prompt for the model, so only a single line of `#` + a letter or digit is taken as a note.
|
|
711
|
+
onSubmit: (text) => {
|
|
712
|
+
if (text.startsWith("/")) handleSlash(text);
|
|
713
|
+
else if (runMemoryNote({ renderer, blocks: () => blocks }, text)) return;
|
|
714
|
+
else if (shellLine(text) !== null) void runShellLine(shellCtx, text);
|
|
715
|
+
else void submit(text);
|
|
716
|
+
},
|
|
717
|
+
// port #21: abort FIRST (kills in-flight fetch/subprocesses), then return() settles the generator
|
|
718
|
+
onInterrupt: () => { runAbort?.abort(); void run?.return(undefined as never); renderer.addSystemNote("run interrupted", "warn"); },
|
|
719
|
+
onExit: () => close(),
|
|
720
|
+
}, { beforeFirstRender: seed, onReveal: reveal, ...(opts.startup ? {
|
|
721
|
+
animate: false,
|
|
722
|
+
beforeReveal: () => {
|
|
723
|
+
_trace("first frame prepared; waiting for intro");
|
|
724
|
+
return waitForStartup(opts.startup?.animationDone ?? Promise.resolve(), opts.startup?.signal);
|
|
725
|
+
},
|
|
726
|
+
} : {}) });
|
|
727
|
+
seed(); // compatibility with injected renderers that ignore beforeFirstRender
|
|
728
|
+
rt.warmRepoMap(); // cooperate with the intro while the prepared scene waits to be revealed
|
|
729
|
+
if (starting) await waitForStartup(starting, opts.startup?.signal);
|
|
730
|
+
reveal(); // legacy renderers without the optional handoff callback
|
|
731
|
+
} catch (e) { await failedStartup(e); }
|
|
732
|
+
if (closed) { await closedP; return; }
|
|
733
|
+
_trace("interface ready");
|
|
734
|
+
// From here the terminal is in a mode the shell cannot live in: alt screen, raw stdin, mouse and focus
|
|
735
|
+
// reporting. `close()` undoes all of it; a crash used to undo none of it, leaving the prompt inside the
|
|
736
|
+
// alt screen under a frame that never erased, with every mouse movement typing escape sequences at it.
|
|
737
|
+
// The guard restores the terminal FIRST and only then writes the reason to a file — the screen a stack
|
|
738
|
+
// trace would print on is the one the restore erases, so the file is the only copy that survives.
|
|
739
|
+
uninstallCrashGuard = installCrashGuard({
|
|
740
|
+
restore: () => renderer.restoreTerminal?.(),
|
|
741
|
+
logDir: join(rovecodeHome(), "logs"),
|
|
742
|
+
version: pkg.version,
|
|
743
|
+
});
|
|
744
|
+
// The update fetch stays outside the note emitter: fresh sessions only, never blocking the first prompt.
|
|
745
|
+
if (boot.id === undefined) void checkForUpdate(pkg.version).then((s) => { const l = updateLine(s); if (l !== null) renderer.addSystemNote(l); }).catch(() => {});
|
|
746
|
+
watchProviders(provCtx()); // follow a default-model change made elsewhere; announce the first provider
|
|
747
|
+
if (!opts.startup) await ready();
|
|
748
|
+
await closedP;
|
|
749
|
+
}
|