rovecode 0.4.0-beta.2 → 0.4.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 +67 -69
- package/THIRD_PARTY_NOTICES.md +0 -44
- package/bin/rovecode.ts +21 -0
- package/package.json +16 -37
- package/src/account/keys.ts +97 -0
- package/src/account/login.ts +158 -0
- package/src/account/provision.ts +47 -0
- package/src/account/store.ts +63 -0
- package/src/acp/server.ts +373 -0
- package/src/cli/account-cmd.ts +116 -0
- package/src/cli/connect.ts +244 -0
- package/src/cli/context-cmd.ts +199 -0
- package/src/cli/dispatch.ts +109 -0
- package/src/cli/doctor.ts +324 -0
- package/src/cli/export.ts +278 -0
- package/src/cli/help.ts +240 -0
- package/src/cli/is-tui-invocation.ts +8 -0
- package/src/cli/main.ts +599 -0
- package/src/cli/market-cmd.ts +658 -0
- package/src/cli/mcp-market-cmd.ts +299 -0
- package/src/cli/output.ts +382 -0
- package/src/cli/repl.ts +172 -0
- package/src/cli/resume.ts +32 -0
- package/src/cli/run-limits.ts +78 -0
- package/src/cli/runtime.ts +792 -0
- package/src/cli/setup.ts +187 -0
- package/src/cli/update-cmd.ts +78 -0
- package/src/cli/workflow-cmd.ts +100 -0
- package/src/coding/checkpoints.ts +270 -0
- package/src/coding/diff.ts +136 -0
- package/src/coding/files.ts +339 -0
- package/src/coding/hashline.ts +319 -0
- package/src/coding/lsp.ts +406 -0
- package/src/coding/repomap-cache.ts +99 -0
- package/src/coding/repomap-files.ts +110 -0
- package/src/coding/repomap.ts +392 -0
- package/src/core/compaction.ts +399 -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 +397 -0
- package/src/core/guardrails.ts +400 -0
- package/src/core/hooks.ts +398 -0
- package/src/core/images.ts +230 -0
- package/src/core/intro.ts +236 -0
- package/src/core/loop.ts +621 -0
- package/src/core/modes.ts +372 -0
- package/src/core/orchestrator.ts +207 -0
- package/src/core/reflection.ts +165 -0
- package/src/core/sandbox-config.ts +167 -0
- package/src/core/session-images.ts +73 -0
- package/src/core/session.ts +398 -0
- package/src/core/settings.ts +98 -0
- package/src/core/stuck-detector.ts +273 -0
- package/src/core/tasks.ts +374 -0
- package/src/core/token-scale.ts +108 -0
- package/src/core/tool-output-budget.ts +166 -0
- package/src/core/tools.ts +288 -0
- package/src/core/types.ts +330 -0
- package/src/core/update-check.ts +171 -0
- package/src/core/update.ts +158 -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 +237 -0
- package/src/core/voice.ts +158 -0
- package/src/core/win-job.ts +183 -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 +218 -0
- package/src/eval/gauntlet.ts +226 -0
- package/src/eval/grader.ts +186 -0
- package/src/eval/record.ts +202 -0
- package/src/eval/redact.ts +141 -0
- package/src/eval/replay.ts +147 -0
- package/src/eval/trajectory.ts +373 -0
- package/src/index.ts +17 -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 +431 -0
- package/src/mcp/config.ts +239 -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 +0 -0
- package/src/mcp/tools.ts +131 -0
- package/src/mcp/trust.ts +49 -0
- package/src/memory/blocks.ts +175 -0
- package/src/memory/recall.ts +355 -0
- package/src/memory/store.ts +105 -0
- package/src/memory/tools.ts +99 -0
- package/src/plugins/cli.ts +123 -0
- package/src/plugins/discover.ts +108 -0
- package/src/plugins/index.ts +50 -0
- package/src/plugins/init.ts +140 -0
- package/src/plugins/install.ts +184 -0
- package/src/plugins/load.ts +149 -0
- package/src/plugins/manifest.ts +106 -0
- package/src/plugins/state.ts +83 -0
- package/src/providers/auth.ts +293 -0
- package/src/providers/cache.ts +223 -0
- package/src/providers/catalog-local.ts +160 -0
- package/src/providers/catalog.ts +408 -0
- package/src/providers/middleware-context.ts +86 -0
- package/src/providers/middleware.ts +373 -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 +302 -0
- package/src/providers/response-validation.ts +80 -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 +569 -0
- package/src/providers/thinking.ts +189 -0
- package/src/providers/wire-messages.ts +129 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/index.ts +3 -0
- package/src/server/dashboard.ts +144 -0
- package/src/server/http.ts +343 -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/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 +159 -0
- package/src/sextant/draw-tabs.ts +85 -0
- package/src/sextant/draw-util.ts +65 -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 +488 -0
- package/src/sextant/layout.ts +86 -0
- package/src/sextant/local-commands.ts +156 -0
- package/src/sextant/market-source.ts +287 -0
- package/src/sextant/mentions.ts +141 -0
- package/src/sextant/message-hits.ts +26 -0
- package/src/sextant/model.ts +387 -0
- package/src/sextant/overlays.ts +451 -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/selection.ts +123 -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 +314 -0
- package/src/sextant/sextant-renderer.ts +478 -0
- package/src/sextant/sextant-repo.ts +131 -0
- package/src/sextant/theme.ts +66 -0
- package/src/sextant/tool-rows.ts +189 -0
- package/src/sextant/types.ts +473 -0
- package/src/skills/index.ts +306 -0
- package/src/skills/tools.ts +69 -0
- package/src/skills/versioned.ts +227 -0
- package/src/telemetry/otel.ts +353 -0
- package/src/telemetry/otlp.ts +68 -0
- package/src/tools/ask-user.ts +156 -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 +216 -0
- package/src/tools/todo.ts +320 -0
- package/src/tools/webfetch.ts +331 -0
- package/src/tui/app.ts +608 -0
- package/src/tui/attach.ts +127 -0
- package/src/tui/checkpoints-cmd.ts +70 -0
- package/src/tui/clipboard-image.ts +81 -0
- package/src/tui/commands.ts +277 -0
- package/src/tui/cost.ts +108 -0
- package/src/tui/info-cmd.ts +144 -0
- package/src/tui/mcp-cmd.ts +128 -0
- package/src/tui/modes-cmd.ts +45 -0
- package/src/tui/overlays.ts +97 -0
- package/src/tui/pi-renderer.ts +424 -0
- package/src/tui/providers-cmd.ts +366 -0
- package/src/tui/renderer.ts +101 -0
- package/src/tui/replay-marker.ts +29 -0
- package/src/tui/session-cmd.ts +146 -0
- package/src/tui/sextant-attach.ts +68 -0
- package/src/tui/sextant-io.ts +184 -0
- package/src/tui/sextant-smoke.ts +110 -0
- package/src/tui/smoke.ts +72 -0
- package/src/tui/theme.ts +59 -0
- package/src/tui/todo-label.ts +7 -0
- package/src/workflow/engine.ts +266 -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/CHANGELOG.md +0 -512
- package/bin/rovecode.js +0 -24
- package/dist/cli/app-dybnr56b.js +0 -2
- package/dist/cli/ask-user-p8hq4xgj.js +0 -2
- package/dist/cli/auth-login-ewpgw5sm.js +0 -2
- package/dist/cli/auth-m8p9grty.js +0 -2
- package/dist/cli/bench-xv3ypwev.js +0 -9
- package/dist/cli/catalog-737wb2s0.js +0 -2
- package/dist/cli/cli-arhg40m0.js +0 -2
- package/dist/cli/client-cf2pxx8q.js +0 -2
- package/dist/cli/commands-3p7e4xxs.js +0 -2
- package/dist/cli/connect-3q93d7cb.js +0 -2
- package/dist/cli/context-cmd-eqxmxhzq.js +0 -2
- package/dist/cli/context-report-hbw9zfes.js +0 -2
- package/dist/cli/count-remote-mby98cd0.js +0 -2
- package/dist/cli/design-122y0axd.js +0 -2
- package/dist/cli/dispatch-b4egzvvh.js +0 -2
- package/dist/cli/doctor-x4jkv72e.js +0 -3
- package/dist/cli/executor-ftvg6tsy.js +0 -2
- package/dist/cli/export-pdgdhkch.js +0 -2
- package/dist/cli/files-cez9a96p.js +0 -2
- package/dist/cli/gauntlet-r3xxaszc.js +0 -2
- package/dist/cli/gauntlet-runner-r515m7kk.js +0 -10
- package/dist/cli/gauntlet-wave3-bnkjk2v2.js +0 -5
- package/dist/cli/gauntlet-wave4-acs9s60q.js +0 -14
- package/dist/cli/hashline-ewg5hbe3.js +0 -2
- package/dist/cli/http-n0kehsk8.js +0 -5
- package/dist/cli/index-z5qt1s76.js +0 -2
- package/dist/cli/install-80mp63kx.js +0 -2
- package/dist/cli/loop-12twjcat.js +0 -2
- package/dist/cli/main-01pv9206.js +0 -4
- package/dist/cli/main-0jys2ccn.js +0 -3
- package/dist/cli/main-1ztz6fkj.js +0 -10
- package/dist/cli/main-23q7cmww.js +0 -9
- package/dist/cli/main-2rzbexn2.js +0 -3
- package/dist/cli/main-2wyax8k9.js +0 -9
- package/dist/cli/main-2yeveeve.js +0 -6
- package/dist/cli/main-2z3dek0b.js +0 -3
- package/dist/cli/main-2zgsknth.js +0 -3
- package/dist/cli/main-45ejth3a.js +0 -4
- package/dist/cli/main-45rn3trk.js +0 -22
- package/dist/cli/main-4p4e2w7x.js +0 -4
- package/dist/cli/main-4y0tnfpa.js +0 -16
- package/dist/cli/main-5py0rkmc.js +0 -4
- package/dist/cli/main-6dtqmbt6.js +0 -7
- package/dist/cli/main-6h9x282m.js +0 -4
- package/dist/cli/main-6vjeds42.js +0 -3
- package/dist/cli/main-78gq4bt9.js +0 -6
- package/dist/cli/main-7jd5vh3x.js +0 -4
- package/dist/cli/main-7kt6r53y.js +0 -4
- package/dist/cli/main-8c1tbazx.js +0 -58
- package/dist/cli/main-9a9rnh47.js +0 -19
- package/dist/cli/main-9ht36z12.js +0 -3
- package/dist/cli/main-a2yfvcy9.js +0 -7
- package/dist/cli/main-a3f51n0x.js +0 -5
- package/dist/cli/main-b8zq261k.js +0 -3
- package/dist/cli/main-bxtvnf6d.js +0 -13
- package/dist/cli/main-edxc3yzt.js +0 -4
- package/dist/cli/main-evgz4mp5.js +0 -21
- package/dist/cli/main-f33fc5je.js +0 -9
- package/dist/cli/main-fvnpq46y.js +0 -12
- package/dist/cli/main-gbbty4d4.js +0 -3
- package/dist/cli/main-gth53dnt.js +0 -25
- package/dist/cli/main-hqbz10aw.js +0 -9
- package/dist/cli/main-hrrvcfan.js +0 -38
- package/dist/cli/main-hzwtsb2m.js +0 -5
- package/dist/cli/main-j7ttv0sd.js +0 -34
- package/dist/cli/main-jak598k9.js +0 -5
- package/dist/cli/main-kba6zeyd.js +0 -6
- package/dist/cli/main-kwwsz6rq.js +0 -3
- package/dist/cli/main-m8vm17zq.js +0 -3
- package/dist/cli/main-mg4f96e1.js +0 -3
- package/dist/cli/main-mg9b20ac.js +0 -18
- package/dist/cli/main-mgb9ccnx.js +0 -3
- package/dist/cli/main-mjt2p7aj.js +0 -3
- package/dist/cli/main-n6qrdbmy.js +0 -3
- package/dist/cli/main-na7wse0x.js +0 -5
- package/dist/cli/main-nqveez48.js +0 -4
- package/dist/cli/main-ntqef02r.js +0 -10
- package/dist/cli/main-nvc3yjay.js +0 -136
- package/dist/cli/main-p0cfn6nr.js +0 -16
- package/dist/cli/main-qj2djy17.js +0 -19
- package/dist/cli/main-qsevpgsv.js +0 -3
- package/dist/cli/main-qvarybsp.js +0 -3
- package/dist/cli/main-rebtt91r.js +0 -5
- package/dist/cli/main-rpg7h8mb.js +0 -3
- package/dist/cli/main-rsy72qmw.js +0 -15
- package/dist/cli/main-rvetps99.js +0 -18
- package/dist/cli/main-s4bb0jav.js +0 -3
- package/dist/cli/main-s9v8k74e.js +0 -3
- package/dist/cli/main-tjvwmscs.js +0 -3
- package/dist/cli/main-tkgarpjj.js +0 -4
- package/dist/cli/main-v8y60bb2.js +0 -3
- package/dist/cli/main-vhrrq337.js +0 -3
- package/dist/cli/main-vp2dfb7s.js +0 -4
- package/dist/cli/main-vqbr22sz.js +0 -8
- package/dist/cli/main-vxnwe5xx.js +0 -18
- package/dist/cli/main-wgph00xf.js +0 -5
- package/dist/cli/main-wk2csfnj.js +0 -5
- package/dist/cli/main-wm997zjx.js +0 -3
- package/dist/cli/main-wpkyraxh.js +0 -3
- package/dist/cli/main-wqt32p5x.js +0 -4
- package/dist/cli/main-x9ct6y1a.js +0 -3
- package/dist/cli/main-xfekqh9m.js +0 -7
- package/dist/cli/main-xt9zc3n6.js +0 -7
- package/dist/cli/main-xx2z3zh5.js +0 -4
- package/dist/cli/main-y5c82rxr.js +0 -3
- package/dist/cli/main-yrjt2sqt.js +0 -14
- package/dist/cli/main-ys6zj3yr.js +0 -3
- package/dist/cli/main-ywbxshqc.js +0 -8
- package/dist/cli/main-z13755t8.js +0 -25
- package/dist/cli/main-zc7pyrbj.js +0 -4
- package/dist/cli/main.js +0 -279
- package/dist/cli/market-cmd-bm5xvn9f.js +0 -5
- package/dist/cli/mcp-login-bthtfpt7.js +0 -2
- package/dist/cli/mcp-market-cmd-mbeshfyd.js +0 -2
- package/dist/cli/notify-54v5z9dz.js +0 -2
- package/dist/cli/oauth-g5gme95c.js +0 -2
- package/dist/cli/output-satndjap.js +0 -16
- package/dist/cli/profiles-sfhpbq3m.js +0 -2
- package/dist/cli/provider-config-hv3xtdt4.js +0 -2
- package/dist/cli/provider-kwzq6g84.js +0 -2
- package/dist/cli/registry-fh0hdnyn.js +0 -2
- package/dist/cli/registry-y1y8e94r.js +0 -2
- package/dist/cli/repl-t4z03mqq.js +0 -11
- package/dist/cli/resume-fqt4chg8.js +0 -2
- package/dist/cli/run-flags-rysbag9t.js +0 -2
- package/dist/cli/runtime-j19fjbsa.js +0 -2
- package/dist/cli/sandbox-config-g4qxd7y5.js +0 -2
- package/dist/cli/server-r0b6bksk.js +0 -5
- package/dist/cli/session-arg-txmn5g4x.js +0 -2
- package/dist/cli/session-ed250d9j.js +0 -2
- package/dist/cli/sessions-cmd-adw7svfn.js +0 -7
- package/dist/cli/settings-y9rzcqx8.js +0 -2
- package/dist/cli/setup-jmbr11j0.js +0 -2
- package/dist/cli/sextant-smoke-tcth0vea.js +0 -5
- package/dist/cli/skills-cmd-zbdy99v6.js +0 -2
- package/dist/cli/smoke-1bg937kx.js +0 -8
- package/dist/cli/start-chat-p01cdks3.js +0 -12
- package/dist/cli/stream-4wmyaypz.js +0 -2
- package/dist/cli/task-eg4s093s.js +0 -2
- package/dist/cli/tasks-12v9rr9k.js +0 -2
- package/dist/cli/thinking-a5ngvqyh.js +0 -2
- package/dist/cli/todo-1wxpcecx.js +0 -2
- package/dist/cli/tools-2ftsya7w.js +0 -2
- package/dist/cli/tools-x1tj4fxm.js +0 -2
- package/dist/cli/trust-cmd-hccxehzb.js +0 -2
- package/dist/cli/update-check-ygt3vd7m.js +0 -2
- package/dist/cli/update-cmd-v23qhr8c.js +0 -2
- package/dist/cli/voice-g1gtck92.js +0 -2
- package/dist/cli/webfetch-0nnrjgb5.js +0 -2
- package/dist/cli/websearch-f0vr2p7d.js +0 -2
- package/dist/cli/workspace-9rq1w4ta.js +0 -2
- package/dist/lib/index.js +0 -62
- package/dist/lib/models-index.json +0 -1
- package/dist/lib/plugins.js +0 -6
- package/dist/lib/providers.js +0 -17
- package/dist/lib/public-api.js +0 -20
- package/dist/rovecode.exe +0 -4
- /package/{dist/cli → src/providers}/models-index.json +0 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/** /model · /models · /provider — the TUI's provider surface over the runtime's LIVE registry
|
|
2
|
+
* (providers/registry.ts). Nothing here rebuilds the runtime: the registry's dispatching stream
|
|
3
|
+
* resolves model.provider per call, so a provider added or a key stored in this session serves the
|
|
4
|
+
* very next prompt. Kept out of app.ts (ADR-002 cap) like info-cmd.ts / session-cmd.ts.
|
|
5
|
+
*
|
|
6
|
+
* Secrets: `/provider key <id> <secret>` stores through auth.ts saveCredential and echoes only the
|
|
7
|
+
* redacted prefix; slash commands never reach the agent or the session store, so the key is not in
|
|
8
|
+
* the transcript. Prefer `--key-env NAME` or `rovecode auth set <id>` (masked prompt) when possible. */
|
|
9
|
+
|
|
10
|
+
import type { Runtime } from "../cli/runtime.ts";
|
|
11
|
+
import type { ModeManager } from "../core/modes.ts";
|
|
12
|
+
import type { ModelRef } from "../core/types.ts";
|
|
13
|
+
import type { Renderer } from "./renderer.ts";
|
|
14
|
+
import { redactSecret, saveCredential } from "../providers/auth.ts";
|
|
15
|
+
import { isConfigured } from "../providers/provider-config.ts";
|
|
16
|
+
import { ADD_USAGE, formatProviderList, parseAddArgs } from "../providers/registry.ts";
|
|
17
|
+
import { PROVIDER_ID_RE } from "../providers/provider-config.ts";
|
|
18
|
+
import type { PickItem } from "./renderer.ts";
|
|
19
|
+
import { SETUP_PICKS } from "../cli/setup.ts";
|
|
20
|
+
import { CONNECT_WAITING, parseConnectArgs, runConnect } from "../cli/connect.ts";
|
|
21
|
+
import { next } from "../core/voice.ts";
|
|
22
|
+
|
|
23
|
+
export interface ProviderCmdCtx {
|
|
24
|
+
rt: Runtime;
|
|
25
|
+
modes: ModeManager;
|
|
26
|
+
state: { provider: string; model: string };
|
|
27
|
+
renderer: Renderer;
|
|
28
|
+
pushStatus: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type NoteLevel = Parameters<Renderer["addSystemNote"]>[1];
|
|
32
|
+
|
|
33
|
+
export const MODEL_COMMAND = { name: "model", description: "Switch the model I use: /model <provider/model | model> [--save]", group: "model & provider" };
|
|
34
|
+
|
|
35
|
+
/** every `provider/model` id the configured providers list — the `/model` suggestions in the sextant. A
|
|
36
|
+
* provider that errors or lists nothing drops out silently here; `/models` is where the reasons show. */
|
|
37
|
+
export async function listModelIds(reg: Pick<Runtime["providers"], "list" | "models">): Promise<string[]> {
|
|
38
|
+
const ids = reg.list().filter(isConfigured).map((p) => p.id);
|
|
39
|
+
const results = await Promise.all(ids.map(async (id) => ({ id, r: await reg.models(id) })));
|
|
40
|
+
return results.flatMap(({ id, r }) => (r.ok ? r.models.map((m) => `${id}/${m}`) : []));
|
|
41
|
+
}
|
|
42
|
+
export const SETUP_COMMAND = { name: "setup", description: "Connect a model step by step: provider, model, key, one test call", group: "start here" };
|
|
43
|
+
export const CONNECT_COMMAND = { name: "connect", description: "Connect a model: /connect (guided, same as /setup) · /connect <id> [<url>] [--model <id>] [--key-env NAME] [--no-key] [--project] [--no-test]", group: "start here" };
|
|
44
|
+
export const PROVIDER_COMMANDS = [
|
|
45
|
+
{ name: "models", description: "Pick a model from a list: /models [provider] [--save]", group: "model & provider" },
|
|
46
|
+
{ name: "provider", description: "Endpoints I can talk to: /provider list · add <id> <url> [--protocol …] [--key-env …] [--model …] [--no-key] [--project] · remove <id> · use <provider/model> · test <id> [model] · key <id> <secret>", group: "model & provider" },
|
|
47
|
+
];
|
|
48
|
+
/** the last line of a finished /setup */
|
|
49
|
+
export const SETUP_DONE_TUI = "Done. Tell me what you want done — try: explain this repo";
|
|
50
|
+
|
|
51
|
+
/** `/model …` / `/provider use` write the default themselves and report it — the watcher must not
|
|
52
|
+
* announce the same change a second time. Set around the write; refresh() fires synchronously. */
|
|
53
|
+
let localWrite = false;
|
|
54
|
+
|
|
55
|
+
const note = (ctx: ProviderCmdCtx, text: string, level?: NoteLevel): void => { ctx.renderer.addSystemNote(text, level); };
|
|
56
|
+
|
|
57
|
+
/** the session's model = the current mode's slot (port #20; mirrored to both when planActSeparateModels is off) */
|
|
58
|
+
function applyRef(ctx: ProviderCmdCtx, ref: ModelRef): void {
|
|
59
|
+
ctx.modes.setModel({ provider: ref.provider, model: ref.model });
|
|
60
|
+
const cur = ctx.modes.modelFor();
|
|
61
|
+
ctx.state.model = cur.model;
|
|
62
|
+
ctx.state.provider = cur.provider;
|
|
63
|
+
ctx.pushStatus();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** /model <provider/model | model> [--save] — switch this session; --save also persists it as the default. */
|
|
67
|
+
export function cmdModel(ctx: ProviderCmdCtx, arg: string): void {
|
|
68
|
+
const words = arg.split(/\s+/).filter((w) => w.length > 0);
|
|
69
|
+
const save = words.includes("--save");
|
|
70
|
+
const sel = words.filter((w) => w !== "--save").join(" ");
|
|
71
|
+
if (sel.length === 0) { note(ctx, "usage: /model <provider/model | model> [--save] · /models [provider] lists ids · /provider list", "warn"); return; }
|
|
72
|
+
const ref = ctx.rt.providers.resolveSelector(sel, ctx.state.provider);
|
|
73
|
+
if ("error" in ref) { note(ctx, `${ref.error} — add one with /provider add <id> <baseUrl>`, "warn"); return; }
|
|
74
|
+
const switchedProvider = ref.provider !== ctx.state.provider;
|
|
75
|
+
applyRef(ctx, ref);
|
|
76
|
+
let saved = "";
|
|
77
|
+
if (save) {
|
|
78
|
+
localWrite = true;
|
|
79
|
+
try {
|
|
80
|
+
const r = ctx.rt.providers.setDefault(`${ref.provider}/${ref.model}`);
|
|
81
|
+
saved = "error" in r ? ` (not saved: ${r.error})` : " (saved as default)";
|
|
82
|
+
} finally { localWrite = false; }
|
|
83
|
+
}
|
|
84
|
+
const p = ctx.rt.providers.get(ref.provider);
|
|
85
|
+
const keyNote = p !== undefined && !isConfigured(p) ? `\n${ctx.rt.providers.keyHint(p)}` : "";
|
|
86
|
+
// same provider → the bare model id (the historical `/model <id>` note); a provider switch names both
|
|
87
|
+
note(ctx, `model → ${switchedProvider ? `${ref.provider}/` : ""}${ref.model}${saved}${ctx.modes.separate ? ` (${ctx.modes.mode} mode)` : ""}${keyNote}`, keyNote.length > 0 ? "warn" : undefined);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** /models [provider] — ids from providers.json `models` or the endpoint's /models. */
|
|
91
|
+
/** /models [provider] [--all] [--save] — PICK a model, do not read a wall of ids.
|
|
92
|
+
*
|
|
93
|
+
* It used to print up to 60 rows as a note and leave you to retype one into `/model <id>`. The ids
|
|
94
|
+
* are long and often differ by a date suffix, so retyping is where the mistakes were. Now the same
|
|
95
|
+
* list opens in the picker the setup flow already uses: arrow, Enter, done — and Enter applies it
|
|
96
|
+
* to this session immediately.
|
|
97
|
+
*
|
|
98
|
+
* Bare, it lists every CONFIGURED provider's models, so switching vendor is the same gesture as
|
|
99
|
+
* switching model; naming a provider narrows it to that one (and reaches an unconfigured one, which
|
|
100
|
+
* is how you look before you commit). `--save` also persists the pick as the default.
|
|
101
|
+
*
|
|
102
|
+
* Fetching is per provider and concurrent — a provider whose endpoint is down or keyless reports
|
|
103
|
+
* itself as one row instead of failing the whole list. */
|
|
104
|
+
export async function cmdModels(ctx: ProviderCmdCtx, arg: string): Promise<void> {
|
|
105
|
+
const words = arg.split(/\s+/).filter((w) => w.length > 0);
|
|
106
|
+
const save = words.includes("--save");
|
|
107
|
+
const named = words.filter((w) => !w.startsWith("-"))[0];
|
|
108
|
+
const reg = ctx.rt.providers;
|
|
109
|
+
|
|
110
|
+
const ids = named !== undefined ? [named] : reg.list().filter(isConfigured).map((p) => p.id);
|
|
111
|
+
if (ids.length === 0) { note(ctx, `no provider is configured yet. ${next("/connect")}`, "warn"); return; }
|
|
112
|
+
|
|
113
|
+
note(ctx, ids.length === 1 ? `fetching ${ids[0]} models…` : `fetching models from ${ids.length} providers…`);
|
|
114
|
+
const results = await Promise.all(ids.map(async (id) => ({ id, r: await reg.models(id) })));
|
|
115
|
+
|
|
116
|
+
const items: PickItem[] = [];
|
|
117
|
+
const problems: string[] = [];
|
|
118
|
+
for (const { id, r } of results) {
|
|
119
|
+
if (!r.ok) { problems.push(`${id}: ${r.error}`); continue; }
|
|
120
|
+
if (r.models.length === 0) { problems.push(`${id}: the endpoint listed no models (no /models route?) — /model ${id}/<model> still works`); continue; }
|
|
121
|
+
for (const m of r.models) {
|
|
122
|
+
const current = ctx.state.provider === id && ctx.state.model === m;
|
|
123
|
+
items.push({ value: `${id}/${m}`, label: `${current ? "* " : " "}${id}/${m}`, ...(current ? { description: "current" } : {}) });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const p of problems) note(ctx, p, "warn");
|
|
127
|
+
if (items.length === 0) return;
|
|
128
|
+
|
|
129
|
+
// the current model first: the list is long and the answer to "what am I on?" should not need scrolling
|
|
130
|
+
items.sort((a, b) => Number(b.label.startsWith("*")) - Number(a.label.startsWith("*")));
|
|
131
|
+
// short on purpose: the box clips its title, and it already draws an `esc` affordance of its own
|
|
132
|
+
const picked = await ctx.renderer.pickOne(items, `pick a model · ${items.length} from ${ids.length} provider${ids.length === 1 ? "" : "s"}`);
|
|
133
|
+
if (picked === null) return;
|
|
134
|
+
cmdModel(ctx, save ? `${picked} --save` : picked); // ONE path for applying a model, flags and all
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** /provider list|add|remove|use|test|key */
|
|
138
|
+
export async function cmdProvider(ctx: ProviderCmdCtx, arg: string): Promise<void> {
|
|
139
|
+
const words = arg.split(/\s+/).filter((w) => w.length > 0);
|
|
140
|
+
const [action, ...rest] = words;
|
|
141
|
+
const reg = ctx.rt.providers;
|
|
142
|
+
switch (action) {
|
|
143
|
+
case undefined:
|
|
144
|
+
case "list":
|
|
145
|
+
note(ctx, formatProviderList(reg, { all: rest.includes("--all") }));
|
|
146
|
+
return;
|
|
147
|
+
case "add": {
|
|
148
|
+
const parsed = parseAddArgs(rest);
|
|
149
|
+
if ("error" in parsed) { note(ctx, parsed.error, "warn"); return; }
|
|
150
|
+
const r = reg.add(parsed.spec, parsed.scope);
|
|
151
|
+
if ("error" in r) { note(ctx, r.error, "warn"); return; }
|
|
152
|
+
const key = isConfigured(r) ? "" : `\nkey: /provider key ${r.id} <secret> · or set ${r.keyEnv} (live, no restart)`;
|
|
153
|
+
note(ctx, `added ${r.id} (${r.protocol}, ${r.baseUrl}) [${parsed.scope}]${key}\nuse it: /model ${r.id}/${r.defaultModel ?? "<model>"}`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
case "remove": {
|
|
157
|
+
if (rest[0] === undefined) break;
|
|
158
|
+
const r = reg.remove(rest[0]);
|
|
159
|
+
if ("error" in r) { note(ctx, r.error, "warn"); return; }
|
|
160
|
+
note(ctx, r.removed.length > 0 ? `removed ${rest[0]} (${r.removed.join(", ")} providers.json)` : `${rest[0]} was not in any providers.json`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
case "use": {
|
|
164
|
+
if (rest[0] === undefined) break;
|
|
165
|
+
localWrite = true;
|
|
166
|
+
let r: ReturnType<typeof reg.setDefault>;
|
|
167
|
+
try { r = reg.setDefault(rest[0]); } finally { localWrite = false; }
|
|
168
|
+
if ("error" in r) { note(ctx, r.error, "warn"); return; }
|
|
169
|
+
applyRef(ctx, r);
|
|
170
|
+
const p = reg.get(r.provider);
|
|
171
|
+
const keyNote = p !== undefined && !isConfigured(p) ? `\n${reg.keyHint(p)}` : "";
|
|
172
|
+
note(ctx, `default → ${r.provider}/${r.model} (saved; this session switched)${keyNote}`, keyNote.length > 0 ? "warn" : undefined);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
case "test": {
|
|
176
|
+
if (rest[0] === undefined) break;
|
|
177
|
+
note(ctx, `testing ${rest[0]}${rest[1] !== undefined ? `/${rest[1]}` : ""}…`);
|
|
178
|
+
const r = await reg.probe(rest[0], rest[1]);
|
|
179
|
+
note(ctx, `${rest[0]}/${r.model}: ${r.detail}`, r.ok ? undefined : "warn");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
case "key": {
|
|
183
|
+
const [id, secret] = rest;
|
|
184
|
+
if (id === undefined || secret === undefined) break;
|
|
185
|
+
const p = reg.get(id);
|
|
186
|
+
if (p === undefined) { note(ctx, `unknown provider "${id}" — /provider add ${id} <baseUrl> first`, "warn"); return; }
|
|
187
|
+
saveCredential(id, secret, p.keyEnv);
|
|
188
|
+
reg.refresh();
|
|
189
|
+
note(ctx, `stored ${p.keyEnv} for ${id} (${redactSecret(secret)}) — live now`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
default:
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
note(ctx, `usage: /provider list [--all] · /provider ${ADD_USAGE} · remove <id> · use <provider/model> · test <id> [model] · key <id> <secret>`, "warn");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------- /setup ----------
|
|
199
|
+
|
|
200
|
+
/** the watcher + poll an unfinished /setup left armed (one at a time; a new /setup replaces it) */
|
|
201
|
+
let pendingSetup: (() => void) | null = null;
|
|
202
|
+
|
|
203
|
+
/** /setup — the TUI's guided connect: provider from the picker, model from a question card, then the
|
|
204
|
+
* key. The cards have no masked input, so the key is a HAND-OFF: `rovecode auth set <id>` in another
|
|
205
|
+
* terminal (masked) or `/provider key` here. The live registry notices the key landing (a 1 s poll keeps
|
|
206
|
+
* its mtime check warm while a setup is pending) and this finishes the job — one test call, default,
|
|
207
|
+
* session switch — without another command. Wording: core/voice.ts. */
|
|
208
|
+
export async function cmdSetup(ctx: ProviderCmdCtx): Promise<void> {
|
|
209
|
+
const reg = ctx.rt.providers;
|
|
210
|
+
pendingSetup?.(); pendingSetup = null;
|
|
211
|
+
const items = SETUP_PICKS.map((p) => ({ value: p.key, label: p.label }));
|
|
212
|
+
const key = await ctx.renderer.pickOne(items, "◆ connect a model — which provider? (Esc = cancel)");
|
|
213
|
+
if (key === null) { note(ctx, "setup cancelled — nothing changed. /setup whenever you like"); return; }
|
|
214
|
+
const pick = SETUP_PICKS.find((p) => p.key === key);
|
|
215
|
+
if (pick === undefined) return;
|
|
216
|
+
const askText = async (question: string): Promise<string | null> => {
|
|
217
|
+
const a = await ctx.renderer.askQuestion({ question, options: [], allowFreeText: true }).catch(() => null);
|
|
218
|
+
return a?.text?.trim() ?? null;
|
|
219
|
+
};
|
|
220
|
+
let id: string;
|
|
221
|
+
if (pick.url !== undefined) {
|
|
222
|
+
const idText = ((await askText("Short name for it? (letters, digits, - _ .)")) ?? "").toLowerCase();
|
|
223
|
+
if (!PROVIDER_ID_RE.test(idText)) { note(ctx, `setup stopped — "${idText}" won't work as an id. ${next("/setup, and pick a name like myproxy")}`, "warn"); return; }
|
|
224
|
+
const baseUrl = (await askText(`Base URL for ${idText}? (e.g. https://host/v1)`)) ?? "";
|
|
225
|
+
if (!/^https?:\/\//.test(baseUrl)) { note(ctx, `setup stopped — "${baseUrl}" is not an http(s) URL. ${next("/setup")}`, "warn"); return; }
|
|
226
|
+
const keyA = await ctx.renderer.askQuestion({ question: "Does it need an API key?", options: ["yes", "no — it is a local server"], allowFreeText: false }).catch(() => null);
|
|
227
|
+
const noKey = keyA?.choice === 1;
|
|
228
|
+
const r = reg.add({ id: idText, baseUrl, protocol: pick.url, ...(noKey ? { noKey: true } : {}) }, "user");
|
|
229
|
+
if ("error" in r) { note(ctx, `setup stopped — ${r.error}`, "warn"); return; }
|
|
230
|
+
id = idText;
|
|
231
|
+
note(ctx, `◆ ${id} registered (${pick.url} protocol, ${baseUrl}).`);
|
|
232
|
+
} else {
|
|
233
|
+
id = pick.id ?? "";
|
|
234
|
+
const known = reg.get(id);
|
|
235
|
+
if (known === undefined) { note(ctx, `setup stopped — ${id} is missing from the built-in table`, "warn"); return; }
|
|
236
|
+
if (pick.local === true && known.noKey !== true) {
|
|
237
|
+
// the built-in row expects a key env; a local server needs none — the user entry says so
|
|
238
|
+
const r = reg.add({ id, baseUrl: known.baseUrl, protocol: known.protocol, noKey: true, ...(known.defaultModel !== undefined ? { defaultModel: known.defaultModel } : {}) }, "user");
|
|
239
|
+
if ("error" in r) { note(ctx, `setup stopped — ${r.error}`, "warn"); return; }
|
|
240
|
+
note(ctx, `◆ ${id} marked as a local server (${known.baseUrl}, no key).`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const spec = reg.get(id);
|
|
244
|
+
if (spec === undefined) return;
|
|
245
|
+
// A provider that ships a default model does not need a question: taking it is right nearly always,
|
|
246
|
+
// and `/model <id>/<other> --save` changes it in one line afterwards. Only an endpoint with no
|
|
247
|
+
// default (a bare proxy, a self-hosted server) still has to be asked — there is nothing to assume.
|
|
248
|
+
let model = spec.defaultModel;
|
|
249
|
+
if (model === undefined) {
|
|
250
|
+
const mA = await ctx.renderer.askQuestion({ question: `Which model id on ${id}?`, options: [], allowFreeText: true }).catch(() => null);
|
|
251
|
+
model = mA?.text?.trim() || mA?.label;
|
|
252
|
+
if (model === undefined || model.length === 0) { note(ctx, `◆ ${id} is set up but has no model yet. ${next(`/models ${id}, then /model ${id}/<model> --save`)}`); return; }
|
|
253
|
+
} else {
|
|
254
|
+
note(ctx, `◆ model → ${id}/${model} (change it any time: /model ${id}/<other> --save)`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const finish = async (): Promise<void> => {
|
|
258
|
+
note(ctx, `◆ testing ${id}/${model} …`);
|
|
259
|
+
const r = await reg.probe(id, model);
|
|
260
|
+
if (r.ok) note(ctx, ` ${r.detail}`);
|
|
261
|
+
else note(ctx, ` that didn't work: ${r.detail}\n ${next(`check the key and the URL, then /provider test ${id} ${model}`)}`, "warn");
|
|
262
|
+
localWrite = true;
|
|
263
|
+
let d: ReturnType<typeof reg.setDefault>;
|
|
264
|
+
try { d = reg.setDefault(`${id}/${model}`, "user"); } finally { localWrite = false; }
|
|
265
|
+
if ("error" in d) { note(ctx, ` ${d.error}`, "warn"); return; }
|
|
266
|
+
applyRef(ctx, d);
|
|
267
|
+
note(ctx, `◆ default → ${d.provider}/${d.model} — this session switched too.\n${SETUP_DONE_TUI}`);
|
|
268
|
+
};
|
|
269
|
+
if (isConfigured(spec)) { await finish(); return; }
|
|
270
|
+
note(ctx, [
|
|
271
|
+
`◆ I need the key for ${id}. Paste it hidden in another terminal:`,
|
|
272
|
+
` rovecode auth set ${id}`,
|
|
273
|
+
` or here (this line only, redacted in the note): /provider key ${id} <secret>`,
|
|
274
|
+
` or set ${spec.keyEnv} in your environment.`,
|
|
275
|
+
" I'll notice as soon as it lands and finish the setup.",
|
|
276
|
+
].join("\n"));
|
|
277
|
+
armKeyWatch(reg, id, finish);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Park until the key for `id` lands, then run `finish` once. The registry's mtime check only runs on
|
|
281
|
+
* access, so a 1 s poll keeps it warm while we wait; one watch at a time — a new /setup or /connect
|
|
282
|
+
* replaces the one before it. */
|
|
283
|
+
function armKeyWatch(reg: ProviderCmdCtx["rt"]["providers"], id: string, finish: () => Promise<void>): void {
|
|
284
|
+
const timer = setInterval(() => { reg.snapshot(); }, 1000);
|
|
285
|
+
timer.unref?.();
|
|
286
|
+
const off = reg.onChange(() => {
|
|
287
|
+
const p = reg.get(id);
|
|
288
|
+
if (p === undefined || !isConfigured(p)) return;
|
|
289
|
+
stop();
|
|
290
|
+
void finish();
|
|
291
|
+
});
|
|
292
|
+
const stop = (): void => { clearInterval(timer); off(); if (pendingSetup === stop) pendingSetup = null; };
|
|
293
|
+
pendingSetup = stop;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** /connect — the same job as /setup, but with the answers on the line instead of in cards, so it is one
|
|
297
|
+
* command to paste. Bare, it IS /setup. It runs cli/connect.ts against this session's LIVE registry and
|
|
298
|
+
* applies the new default to the session, like /setup does.
|
|
299
|
+
*
|
|
300
|
+
* There is no masked input in here, so `--key` and `--key-stdin` are refused rather than quietly
|
|
301
|
+
* ignored: a key typed on this line would sit in the transcript. When a key is simply missing,
|
|
302
|
+
* `deferKey` hands over the same way /setup does and the watcher finishes the job when it lands. */
|
|
303
|
+
export async function cmdConnect(ctx: ProviderCmdCtx, arg: string): Promise<void> {
|
|
304
|
+
const words = arg.split(/\s+/).filter((w) => w.length > 0);
|
|
305
|
+
if (words.length === 0) { await cmdSetup(ctx); return; }
|
|
306
|
+
const parsed = parseConnectArgs(words);
|
|
307
|
+
if ("error" in parsed) { note(ctx, `${parsed.error.replace("usage: rovecode connect", "usage: /connect")}`, "warn"); return; }
|
|
308
|
+
if (parsed.key === "prompt" || parsed.key === "stdin") {
|
|
309
|
+
note(ctx, `there is no hidden prompt in here — a key on this line would sit in the transcript.\n ${next(`rovecode auth set ${parsed.id} in another terminal, or /provider key ${parsed.id} <secret>`)}`, "warn");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const reg = ctx.rt.providers;
|
|
313
|
+
pendingSetup?.(); pendingSetup = null;
|
|
314
|
+
|
|
315
|
+
/** the default is written by the time connect returns (even on a failed test call) — bring the
|
|
316
|
+
* session with it, the way /setup does. Also runs on the deferred pass, after the key lands. */
|
|
317
|
+
const follow = (code: number): void => {
|
|
318
|
+
if (code === CONNECT_WAITING || code === 2) return;
|
|
319
|
+
const d = reg.defaultRef();
|
|
320
|
+
if (d !== null && d.provider === parsed.id && d.model.length > 0) applyRef(ctx, d);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const run = async (): Promise<number> => {
|
|
324
|
+
localWrite = true; // the watcher must not announce a default this command already reported
|
|
325
|
+
try {
|
|
326
|
+
return await runConnect(parsed, {
|
|
327
|
+
registry: reg,
|
|
328
|
+
tty: false, // cards, not a terminal: never let runConnect reach for a prompt or for stdin
|
|
329
|
+
out: (l) => note(ctx, l),
|
|
330
|
+
done: SETUP_DONE_TUI,
|
|
331
|
+
where: "tui", // the "→ next:" steps are slash commands in here, not shell lines
|
|
332
|
+
deferKey: (spec) => {
|
|
333
|
+
note(ctx, [
|
|
334
|
+
`◆ I need the key for ${parsed.id}. Paste it hidden in another terminal:`,
|
|
335
|
+
` rovecode auth set ${parsed.id}`,
|
|
336
|
+
` or here (this line only, redacted in the note): /provider key ${parsed.id} <secret>`,
|
|
337
|
+
` or set ${spec.keyEnv} in your environment.`,
|
|
338
|
+
" I'll notice as soon as it lands and finish the job.",
|
|
339
|
+
].join("\n"));
|
|
340
|
+
armKeyWatch(reg, parsed.id, async () => { follow(await run()); });
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
} finally { localWrite = false; }
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
follow(await run());
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Boot-time subscription: when the persisted default changes underneath this session (another
|
|
350
|
+
* terminal's `rovecode model use`, the agent's provider_edit use) the session follows it; when the
|
|
351
|
+
* first provider appears, say so. In-process slash writes report themselves (localWrite). */
|
|
352
|
+
export function watchProviders(ctx: ProviderCmdCtx): () => void {
|
|
353
|
+
let lastSelector = ctx.rt.providers.defaultSelector();
|
|
354
|
+
let hadProvider = ctx.rt.noProviderReason() === null;
|
|
355
|
+
return ctx.rt.providers.onChange(() => {
|
|
356
|
+
const sel = ctx.rt.providers.defaultSelector();
|
|
357
|
+
if (sel !== lastSelector) {
|
|
358
|
+
lastSelector = sel;
|
|
359
|
+
const d = ctx.rt.providers.defaultRef();
|
|
360
|
+
if (d !== null && d.model.length > 0 && !localWrite) { applyRef(ctx, d); note(ctx, `default model changed → ${d.provider}/${d.model}`); }
|
|
361
|
+
}
|
|
362
|
+
const has = ctx.rt.noProviderReason() === null;
|
|
363
|
+
if (has && !hadProvider) note(ctx, "provider configured — ready for the next prompt");
|
|
364
|
+
hadProvider = has;
|
|
365
|
+
});
|
|
366
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** The Renderer seam (BLUEPRINT port #1): the TUI app talks only to this interface.
|
|
2
|
+
* PiTuiRenderer is today's implementation; OpenTUI or others can slot in later
|
|
3
|
+
* without touching the app/loop. House code outside src/tui must not import vendor. */
|
|
4
|
+
|
|
5
|
+
import type { QuestionAnswer, QuestionPrompt } from "../tools/ask-user.ts";
|
|
6
|
+
import type { PermissionLevel, ThinkingEffort, RunEvent } from "../core/types.ts";
|
|
7
|
+
import type { SextantAttach } from "../sextant/types.ts";
|
|
8
|
+
|
|
9
|
+
export type { QuestionAnswer, QuestionPrompt, SextantAttach };
|
|
10
|
+
|
|
11
|
+
/** `all-edits` is offered only on an edit/write card: it answers THIS call and asks the surface to
|
|
12
|
+
* drop to accept-edits for the rest of the session (writes inside the workspace stop prompting). */
|
|
13
|
+
export type ApprovalAnswer = "once" | "always" | "all-edits" | "deny";
|
|
14
|
+
|
|
15
|
+
export interface StatusInfo {
|
|
16
|
+
provider: string;
|
|
17
|
+
model: string;
|
|
18
|
+
yolo: boolean;
|
|
19
|
+
/** the full permission tier; `yolo` stays for the surfaces that only know two. Optional so a
|
|
20
|
+
* renderer that does not paint it needs no change. */
|
|
21
|
+
permission?: PermissionLevel;
|
|
22
|
+
/** the thinking dial, painted next to the model when it is on */
|
|
23
|
+
effort?: ThinkingEffort;
|
|
24
|
+
turns: number;
|
|
25
|
+
tokensIn: number;
|
|
26
|
+
tokensOut: number;
|
|
27
|
+
/** port #20: plan/act mode indicator (optional — plain surfaces omit it) */
|
|
28
|
+
mode?: "plan" | "act";
|
|
29
|
+
/** port #32: "todos done/total" for the status bar; omitted when the session has no list */
|
|
30
|
+
todos?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RendererHooks {
|
|
34
|
+
/** user submitted a line from the editor (already trimmed, non-empty) */
|
|
35
|
+
onSubmit: (text: string) => void;
|
|
36
|
+
/** user asked to interrupt the in-flight run (Escape on the loader) */
|
|
37
|
+
onInterrupt: () => void;
|
|
38
|
+
/** user asked to leave (Ctrl+C) */
|
|
39
|
+
onExit: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A streaming assistant message in the transcript. */
|
|
43
|
+
export interface AssistantView {
|
|
44
|
+
append(delta: string): void;
|
|
45
|
+
/** finalize; no more appends */
|
|
46
|
+
done(): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SlashCommand {
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
/** /help topic the command is listed under (info-cmd.ts); the palette ignores it */
|
|
53
|
+
group?: string;
|
|
54
|
+
/** the argument values (`/effort` levels; `/model` ids): the sextant lists them as suggestions once
|
|
55
|
+
* the command is typed and completes the one picked. A function is read at suggestion time, for a
|
|
56
|
+
* set that changes while the TUI runs (the models of the providers that have a key). */
|
|
57
|
+
choices?: readonly string[] | (() => readonly string[]);
|
|
58
|
+
/** what Enter does on a picked choice: "submit" (default — `/effort high` runs) or "complete" — the
|
|
59
|
+
* choice is a subcommand that wants more words (`/mcp add ` waits for a name) */
|
|
60
|
+
choicesThen?: "submit" | "complete";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface PickItem { value: string; label: string; description?: string }
|
|
64
|
+
|
|
65
|
+
export interface Renderer {
|
|
66
|
+
start(hooks: RendererHooks): void;
|
|
67
|
+
stop(): void;
|
|
68
|
+
/** slash commands offered by editor autocomplete; call before start() */
|
|
69
|
+
setCommands(cmds: SlashCommand[]): void;
|
|
70
|
+
addUser(text: string): void;
|
|
71
|
+
addSystemNote(text: string, tone?: "info" | "warn" | "error"): void;
|
|
72
|
+
beginAssistant(): AssistantView;
|
|
73
|
+
toolStart(callId: string, tool: string, argsPreview: string): void;
|
|
74
|
+
toolUpdate(callId: string, note: string): void;
|
|
75
|
+
toolEnd(callId: string, ok: boolean, outputPreview: string, durationMs: number): void;
|
|
76
|
+
/** modal approval; resolves deny on cancel/escape. `detail` (port #24): pre-rendered
|
|
77
|
+
* unified diff of a pending edit/write, shown inside the overlay above the verdicts */
|
|
78
|
+
askApproval(tool: string, argsPreview: string, detail?: string): Promise<ApprovalAnswer>;
|
|
79
|
+
/** modal picker (session/turn navigators); resolves null on cancel/escape/stop */
|
|
80
|
+
pickOne(items: PickItem[], title?: string): Promise<string | null>;
|
|
81
|
+
/** modal question for the ask_user tool (port #33): options + a free-text entry when allowed.
|
|
82
|
+
* Resolves the pick/typed text; null when the user declines, `signal` aborts (the run's
|
|
83
|
+
* controller — the card must dismiss, never leak), or the UI stops. Implementations may
|
|
84
|
+
* reject a SECOND concurrent ask with a clear error (the tool turns it into a failed result). */
|
|
85
|
+
askQuestion(q: QuestionPrompt, signal?: AbortSignal): Promise<QuestionAnswer | null>;
|
|
86
|
+
/** remove all transcript items (history replay after rewind/resume) */
|
|
87
|
+
clearTranscript(): void;
|
|
88
|
+
/** place text in the editor for edit-and-resubmit (pi sessions.md:106-116) */
|
|
89
|
+
prefillEditor(text: string): void;
|
|
90
|
+
setBusy(busy: boolean, label?: string): void;
|
|
91
|
+
setStatus(info: StatusInfo): void;
|
|
92
|
+
/** port #44 (OPTIONAL — FakeRenderer/PiTuiRenderer omit it): every RunEvent of the live run, delivered
|
|
93
|
+
* as the FIRST statement of the app's event loop, BEFORE the toolStart/beginAssistant/addSystemNote
|
|
94
|
+
* calls the same event triggers. A renderer that implements it (sextant) treats the event stream as
|
|
95
|
+
* its source of truth for transcript rows and ignores the duplicate per-event calls while busy. */
|
|
96
|
+
onEvent?(ev: RunEvent): void;
|
|
97
|
+
/** port #44 (OPTIONAL): the runtime handles the sextant panels read — cwd, sessions dir, the ACTIVE
|
|
98
|
+
* store, tasks, model + context window, usage math. Called exactly once by runTui, after runtime
|
|
99
|
+
* construction and before start(). */
|
|
100
|
+
attach?(ctx: SextantAttach): void;
|
|
101
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Persisted RunEvent annotations → transcript lines (port #25 LOW-4). The loop persists a
|
|
2
|
+
* compaction as a `kind: "event"` entry (SessionStore.appendEvent) right where it happened, and
|
|
3
|
+
* the live TUI shows it as a system note; a resumed session (--resume, /resume, /sessions)
|
|
4
|
+
* replays the SAME line at the same place, so "was this context compacted?" has one answer on
|
|
5
|
+
* both paths. ONE wording, built here, used by app.ts (live) and session-cmd.ts replayTranscript
|
|
6
|
+
* (replay) — never two strings to keep in step. Any other persisted event type renders nothing:
|
|
7
|
+
* the live TUI shows nothing for a bare event either, and replay must never add a line the run
|
|
8
|
+
* did not (context-drop compactions are yielded but not persisted — loop.ts:191 — so a resumed
|
|
9
|
+
* session cannot show them; nothing to render). */
|
|
10
|
+
|
|
11
|
+
import type { Entry } from "../core/session.ts";
|
|
12
|
+
import type { Message, RunEvent } from "../core/types.ts";
|
|
13
|
+
|
|
14
|
+
export type CompactionEvent = Extract<RunEvent, { type: "compaction" }>;
|
|
15
|
+
/** SessionStore.appendEvent's entry shape — the non-message half of Entry */
|
|
16
|
+
export type EventEntry = Exclude<Entry, Message>;
|
|
17
|
+
|
|
18
|
+
/** the TUI's system note for a compaction — live (app.ts startRun) and replayed alike */
|
|
19
|
+
export function compactionNote(ev: CompactionEvent): string {
|
|
20
|
+
return `compacted (${ev.strategy}): ${ev.tokensBefore} → ${ev.tokensAfter} tokens`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** the transcript line for a persisted event entry, or null when the live TUI shows nothing for
|
|
24
|
+
* it. Tolerant of a foreign/corrupt entry (session.ts never throws on shape; neither does replay). */
|
|
25
|
+
export function replayMarkerLine(entry: EventEntry): string | null {
|
|
26
|
+
const ev = entry.event as RunEvent | undefined;
|
|
27
|
+
if (!ev || typeof ev !== "object") return null;
|
|
28
|
+
return ev.type === "compaction" ? compactionNote(ev) : null;
|
|
29
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/** TUI session navigation (port #2 session tree), extracted from app.ts for the ADR-002
|
|
2
|
+
* cap: /sessions + /resume pick or resolve a session to switch to, /rewind (alias /tree)
|
|
3
|
+
* jumps to an earlier turn for edit-and-resubmit, /new branches back to the session
|
|
4
|
+
* start. The CALLER owns the swappable store and the rebind routine (switchSession), so
|
|
5
|
+
* store reads and switches run through the injected ctx — same shape as checkpoints-cmd.
|
|
6
|
+
* Also home to the pure session helpers the app calls around a switch: transcript replay,
|
|
7
|
+
* usage recount, and the `--resume <prefix>` boot resolution (wiring pass, ADR-002 cap). */
|
|
8
|
+
|
|
9
|
+
import { partsText } from "../core/loop.ts";
|
|
10
|
+
import { listSessions, type SessionStore } from "../core/session.ts";
|
|
11
|
+
import { userTurnLine } from "./attach.ts";
|
|
12
|
+
import { replayLabel } from "./modes-cmd.ts";
|
|
13
|
+
import { replayMarkerLine } from "./replay-marker.ts";
|
|
14
|
+
import type { Renderer } from "./renderer.ts";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
|
|
17
|
+
/** Re-render the whole transcript from the active session path (boot replay, /resume, /rewind,
|
|
18
|
+
* /new, checkpoint restores). Tool calls replay as start+end card pairs; mode switches render
|
|
19
|
+
* as a human line (port #20 LOW-3), never the raw <mode_notice> XML the entry carries; user
|
|
20
|
+
* turns carry their image chips (port #34), so an image-only turn is not a blank line; persisted
|
|
21
|
+
* compaction events replay as the live run's note (port #25 LOW-4, replay-marker.ts). */
|
|
22
|
+
export function replayTranscript(renderer: Renderer, store: SessionStore): void {
|
|
23
|
+
renderer.clearTranscript();
|
|
24
|
+
for (const m of store.path()) {
|
|
25
|
+
if (!("role" in m)) { const line = replayMarkerLine(m); if (line !== null) renderer.addSystemNote(line); continue; } // event entry (appendEvent): marker or nothing
|
|
26
|
+
const text = partsText(m.parts);
|
|
27
|
+
if (m.role === "user") { const line = userTurnLine(text, m.parts); if (line) renderer.addUser(line); }
|
|
28
|
+
else if (m.role === "assistant") {
|
|
29
|
+
if (text) { const v = renderer.beginAssistant(); v.append(text); v.done(); }
|
|
30
|
+
for (const p of m.parts) {
|
|
31
|
+
if (p.kind === "tool_call") renderer.toolStart(p.id, p.tool, JSON.stringify(p.args).slice(0, 120));
|
|
32
|
+
}
|
|
33
|
+
} else if (m.role === "tool") {
|
|
34
|
+
for (const p of m.parts) {
|
|
35
|
+
if (p.kind === "tool_result") renderer.toolEnd(p.callId, p.ok, p.output.slice(0, 160).replace(/\n/g, " ⏎ "), 0);
|
|
36
|
+
}
|
|
37
|
+
} else if (m.role === "system" && text) renderer.addSystemNote(replayLabel(m, text));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Token usage summed over the active path — the status line's counters. */
|
|
42
|
+
export function usageOf(store: SessionStore): { tokensIn: number; tokensOut: number } {
|
|
43
|
+
let tokensIn = 0, tokensOut = 0;
|
|
44
|
+
for (const m of store.messages()) { tokensIn += m.usage?.input ?? 0; tokensOut += m.usage?.output ?? 0; }
|
|
45
|
+
return { tokensIn, tokensOut };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `rovecode --resume <id>` boot resolution, the /resume rule applied before the runtime exists:
|
|
49
|
+
* an exact id or a brand-new id passes through, a UNIQUE prefix resolves, an AMBIGUOUS prefix
|
|
50
|
+
* must not silently pick one — start fresh (id undefined) and say so in `warn`. */
|
|
51
|
+
export function resolveBootSession(sessionsDir: string, id: string | undefined): { id: string | undefined; warn?: string } {
|
|
52
|
+
if (id === undefined) return { id };
|
|
53
|
+
const known = listSessions(sessionsDir);
|
|
54
|
+
if (known.some((s) => s.id === id)) return { id };
|
|
55
|
+
const pre = known.filter((s) => s.id.startsWith(id));
|
|
56
|
+
if (pre.length === 1) return { id: pre[0]!.id };
|
|
57
|
+
if (pre.length > 1) return { id: undefined, warn: `"${id}" matches ${pre.length} sessions — started fresh; use /resume to pick one` };
|
|
58
|
+
return { id };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SessionCmdCtx {
|
|
62
|
+
renderer: Renderer;
|
|
63
|
+
/** <cwd>/.rovecode/sessions — where listSessions looks */
|
|
64
|
+
sessionsDir: string;
|
|
65
|
+
busy(): boolean;
|
|
66
|
+
/** the ACTIVE session store, read live: /sessions and a root /rewind swap it */
|
|
67
|
+
store(): SessionStore;
|
|
68
|
+
/** rebind the app to session id (store, memory, mode) and replay it; announce defaults on */
|
|
69
|
+
switchSession(id: string, announce?: boolean): void;
|
|
70
|
+
/** re-render the whole transcript from the active session path */
|
|
71
|
+
replayHistory(): void;
|
|
72
|
+
/** recount token usage from the active store */
|
|
73
|
+
refreshUsage(): void;
|
|
74
|
+
pushStatus(): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** /rewind (alias /tree) — pick an earlier turn in the overlay; Enter branches the session
|
|
78
|
+
* to before that turn and prefills the editor with its FULL text for edit-and-resubmit. */
|
|
79
|
+
export async function cmdRewind(ctx: SessionCmdCtx): Promise<void> {
|
|
80
|
+
if (ctx.busy()) { ctx.renderer.addSystemNote("finish or interrupt the run first (Esc)", "warn"); return; }
|
|
81
|
+
const points = ctx.store().turnPoints();
|
|
82
|
+
if (points.length === 0) { ctx.renderer.addSystemNote("nothing to rewind — no turns yet"); return; }
|
|
83
|
+
const items = [...points].reverse().map((p) => ({
|
|
84
|
+
value: p.entryId,
|
|
85
|
+
label: `#${p.index} ${p.text}`,
|
|
86
|
+
description: p.branches > 0 ? `◆ ${p.branches} other branch${p.branches > 1 ? "es" : ""}` : undefined,
|
|
87
|
+
}));
|
|
88
|
+
const picked = await ctx.renderer.pickOne(items, "rewind to a turn (Enter = edit & resubmit, Esc = cancel)");
|
|
89
|
+
if (!picked) { ctx.replayHistory(); return; } // cancel: clear the overlay title note
|
|
90
|
+
const point = points.find((p) => p.entryId === picked);
|
|
91
|
+
if (!point) return;
|
|
92
|
+
if (point.parentId === null) {
|
|
93
|
+
// pi resets the leaf to an empty conversation (sessions.md:116); root reset would need
|
|
94
|
+
// core support — v1 approximates it with a fresh session, old one untouched
|
|
95
|
+
ctx.switchSession(randomUUID(), false);
|
|
96
|
+
ctx.renderer.addSystemNote("rewound to the start — fresh session, previous one kept");
|
|
97
|
+
} else {
|
|
98
|
+
if (!ctx.store().branch(point.parentId)) { ctx.renderer.addSystemNote("rewind failed: turn not found", "error"); return; }
|
|
99
|
+
ctx.replayHistory();
|
|
100
|
+
ctx.renderer.addSystemNote(`rewound to before turn #${point.index} — edit and resubmit (branch kept)`);
|
|
101
|
+
}
|
|
102
|
+
ctx.renderer.prefillEditor(point.fullText); // FULL text, never the ≤80-char overlay label
|
|
103
|
+
ctx.pushStatus();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** /sessions — pick a previous session in the overlay; /resume <id> resolves an exact id
|
|
107
|
+
* or a UNIQUE prefix directly (an ambiguous prefix must not silently pick one). */
|
|
108
|
+
export async function cmdSessions(ctx: SessionCmdCtx, directId?: string): Promise<void> {
|
|
109
|
+
if (ctx.busy()) { ctx.renderer.addSystemNote("finish or interrupt the run first (Esc)", "warn"); return; }
|
|
110
|
+
const all = listSessions(ctx.sessionsDir);
|
|
111
|
+
if (directId) {
|
|
112
|
+
// exact id wins outright; a prefix must match exactly ONE session — resolving an
|
|
113
|
+
// ambiguous prefix silently to the first hit resumed the wrong session
|
|
114
|
+
const exact = all.find((s) => s.id === directId);
|
|
115
|
+
const matches = exact ? [exact] : all.filter((s) => s.id.startsWith(directId));
|
|
116
|
+
if (matches.length === 1) ctx.switchSession(matches[0]!.id);
|
|
117
|
+
else if (matches.length > 1) ctx.renderer.addSystemNote(
|
|
118
|
+
`"${directId}" matches ${matches.length} sessions: ${matches.slice(0, 4).map((s) => s.id.slice(0, 8)).join(", ")}${matches.length > 4 ? ", …" : ""} — be more specific`,
|
|
119
|
+
"warn",
|
|
120
|
+
);
|
|
121
|
+
else ctx.renderer.addSystemNote(`no session matching "${directId}"`, "warn");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const items = all.slice(0, 20).map((s) => ({
|
|
125
|
+
value: s.id,
|
|
126
|
+
label: s.preview || "(empty session)",
|
|
127
|
+
description: `${new Date(s.updatedAt).toLocaleString()} · ${s.entryCount} entries · ${s.id.slice(0, 8)}${s.id === ctx.store().id ? " · current" : ""}`,
|
|
128
|
+
}));
|
|
129
|
+
if (items.length === 0) { ctx.renderer.addSystemNote("no sessions found"); return; }
|
|
130
|
+
const picked = await ctx.renderer.pickOne(items, "resume a session (Esc = cancel)");
|
|
131
|
+
if (picked && picked !== ctx.store().id) ctx.switchSession(picked);
|
|
132
|
+
else if (!picked) ctx.replayHistory(); // cancel: clear the overlay title note
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** /new — branch the leaf back to the session's first entry; the old turns stay in the
|
|
136
|
+
* tree. Synchronous on purpose: no overlay, nothing to await. */
|
|
137
|
+
export function cmdNew(ctx: SessionCmdCtx): void {
|
|
138
|
+
// busy gate (same as /rewind //sessions): moving the leaf mid-run would make the
|
|
139
|
+
// run's next append chain off a moved leaf while its parentId points elsewhere
|
|
140
|
+
if (ctx.busy()) { ctx.renderer.addSystemNote("finish or interrupt the run first (Esc)", "warn"); return; }
|
|
141
|
+
const store = ctx.store();
|
|
142
|
+
if (store.branch(store.messages()[0]?.id ?? "")) {
|
|
143
|
+
ctx.replayHistory(); ctx.refreshUsage(); ctx.pushStatus();
|
|
144
|
+
ctx.renderer.addSystemNote("branched to session start");
|
|
145
|
+
} else ctx.renderer.addSystemNote("nothing to branch — no turns yet");
|
|
146
|
+
}
|