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
|
@@ -0,0 +1,377 @@
|
|
|
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
|
+
import { ModelCatalog } from "../providers/catalog.ts"; // port #60: enrich existing picker rows
|
|
23
|
+
import { describeModel } from "../providers/model-list.ts"; // port #60
|
|
24
|
+
|
|
25
|
+
export interface ProviderCmdCtx {
|
|
26
|
+
rt: Runtime;
|
|
27
|
+
modes: ModeManager;
|
|
28
|
+
state: { provider: string; model: string };
|
|
29
|
+
renderer: Renderer;
|
|
30
|
+
pushStatus: () => void;
|
|
31
|
+
catalog?: Pick<ModelCatalog, "lookup">; // port #60: reuse the app's /cost catalog (offline fallback for other callers)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type NoteLevel = Parameters<Renderer["addSystemNote"]>[1];
|
|
35
|
+
|
|
36
|
+
export const MODEL_COMMAND = { name: "model", description: "Switch the model I use: /model <provider/model | model> [--save]", group: "model & provider" };
|
|
37
|
+
|
|
38
|
+
/** every `provider/model` id the configured providers list — the `/model` suggestions in the sextant. A
|
|
39
|
+
* provider that errors or lists nothing drops out silently here; `/models` is where the reasons show. */
|
|
40
|
+
export async function listModelIds(reg: Pick<Runtime["providers"], "list" | "models">): Promise<string[]> {
|
|
41
|
+
const ids = reg.list().filter(isConfigured).map((p) => p.id);
|
|
42
|
+
const results = await Promise.all(ids.map(async (id) => ({ id, r: await reg.models(id) })));
|
|
43
|
+
return results.flatMap(({ id, r }) => (r.ok ? r.models.map((m) => `${id}/${m}`) : []));
|
|
44
|
+
}
|
|
45
|
+
export const SETUP_COMMAND = { name: "setup", description: "Connect a model step by step: provider, model, key, one test call", group: "start here" };
|
|
46
|
+
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" };
|
|
47
|
+
export const PROVIDER_COMMANDS = [
|
|
48
|
+
{ name: "models", description: "Pick a model from a list: /models [provider] [--save]", group: "model & provider" },
|
|
49
|
+
{ 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" },
|
|
50
|
+
];
|
|
51
|
+
/** the last line of a finished /setup */
|
|
52
|
+
export const SETUP_DONE_TUI = "Done. Tell me what you want done — try: explain this repo";
|
|
53
|
+
|
|
54
|
+
/** `/model …` / `/provider use` write the default themselves and report it — the watcher must not
|
|
55
|
+
* announce the same change a second time. Set around the write; refresh() fires synchronously. */
|
|
56
|
+
let localWrite = false;
|
|
57
|
+
|
|
58
|
+
const note = (ctx: ProviderCmdCtx, text: string, level?: NoteLevel): void => { ctx.renderer.addSystemNote(text, level); };
|
|
59
|
+
|
|
60
|
+
/** the session's model = the current mode's slot (port #20; mirrored to both when planActSeparateModels is off) */
|
|
61
|
+
function applyRef(ctx: ProviderCmdCtx, ref: ModelRef): void {
|
|
62
|
+
ctx.modes.setModel({ provider: ref.provider, model: ref.model });
|
|
63
|
+
const cur = ctx.modes.modelFor();
|
|
64
|
+
ctx.state.model = cur.model;
|
|
65
|
+
ctx.state.provider = cur.provider;
|
|
66
|
+
ctx.pushStatus();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** /model <provider/model | model> [--save] — switch this session; --save also persists it as the default. */
|
|
70
|
+
export function cmdModel(ctx: ProviderCmdCtx, arg: string): void | Promise<void> {
|
|
71
|
+
const words = arg.split(/\s+/).filter((w) => w.length > 0);
|
|
72
|
+
const save = words.includes("--save");
|
|
73
|
+
const sel = words.filter((w) => w !== "--save").join(" ");
|
|
74
|
+
if (sel.length === 0) return cmdModels(ctx, save ? "--save" : ""); // port #60: bare /model uses the existing picker
|
|
75
|
+
const ref = ctx.rt.providers.resolveSelector(sel, ctx.state.provider);
|
|
76
|
+
if ("error" in ref) { note(ctx, `${ref.error} — add one with /provider add <id> <baseUrl>`, "warn"); return; }
|
|
77
|
+
const switchedProvider = ref.provider !== ctx.state.provider;
|
|
78
|
+
applyRef(ctx, ref);
|
|
79
|
+
let saved = "";
|
|
80
|
+
if (save) {
|
|
81
|
+
localWrite = true;
|
|
82
|
+
try {
|
|
83
|
+
const r = ctx.rt.providers.setDefault(`${ref.provider}/${ref.model}`);
|
|
84
|
+
saved = "error" in r ? ` (not saved: ${r.error})` : " (saved as default)";
|
|
85
|
+
} finally { localWrite = false; }
|
|
86
|
+
}
|
|
87
|
+
const p = ctx.rt.providers.get(ref.provider);
|
|
88
|
+
const keyNote = p !== undefined && !isConfigured(p) ? `\n${ctx.rt.providers.keyHint(p)}` : "";
|
|
89
|
+
// same provider → the bare model id (the historical `/model <id>` note); a provider switch names both
|
|
90
|
+
note(ctx, `model → ${switchedProvider ? `${ref.provider}/` : ""}${ref.model}${saved}${ctx.modes.separate ? ` (${ctx.modes.mode} mode)` : ""}${keyNote}`, keyNote.length > 0 ? "warn" : undefined);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** /models [provider] — ids from providers.json `models` or the endpoint's /models. */
|
|
94
|
+
/** /models [provider] [--all] [--save] — PICK a model, do not read a wall of ids.
|
|
95
|
+
*
|
|
96
|
+
* It used to print up to 60 rows as a note and leave you to retype one into `/model <id>`. The ids
|
|
97
|
+
* are long and often differ by a date suffix, so retyping is where the mistakes were. Now the same
|
|
98
|
+
* list opens in the picker the setup flow already uses: arrow, Enter, done — and Enter applies it
|
|
99
|
+
* to this session immediately.
|
|
100
|
+
*
|
|
101
|
+
* Bare, it lists every CONFIGURED provider's models, so switching vendor is the same gesture as
|
|
102
|
+
* switching model; naming a provider narrows it to that one (and reaches an unconfigured one, which
|
|
103
|
+
* is how you look before you commit). `--save` also persists the pick as the default.
|
|
104
|
+
*
|
|
105
|
+
* Fetching is per provider and concurrent — a provider whose endpoint is down or keyless reports
|
|
106
|
+
* itself as one row instead of failing the whole list. */
|
|
107
|
+
export async function cmdModels(ctx: ProviderCmdCtx, arg: string): Promise<void> {
|
|
108
|
+
const words = arg.split(/\s+/).filter((w) => w.length > 0);
|
|
109
|
+
const save = words.includes("--save");
|
|
110
|
+
const named = words.filter((w) => !w.startsWith("-"))[0];
|
|
111
|
+
const reg = ctx.rt.providers;
|
|
112
|
+
|
|
113
|
+
const ids = named !== undefined ? [named] : reg.list().filter(isConfigured).map((p) => p.id);
|
|
114
|
+
if (ids.length === 0) { note(ctx, `no provider is configured yet. ${next("/connect")}`, "warn"); return; }
|
|
115
|
+
|
|
116
|
+
note(ctx, ids.length === 1 ? `fetching ${ids[0]} models…` : `fetching models from ${ids.length} providers…`);
|
|
117
|
+
const results = await Promise.all(ids.map(async (id) => ({ id, r: await reg.models(id) })));
|
|
118
|
+
|
|
119
|
+
const items: PickItem[] = [];
|
|
120
|
+
const problems: string[] = [];
|
|
121
|
+
const catalog = ctx.catalog ?? new ModelCatalog(); // port #60: lookup only, never refresh/fetch metadata
|
|
122
|
+
const itemFor = (id: string, model: string): PickItem => {
|
|
123
|
+
const current = ctx.state.provider === id && ctx.state.model === model;
|
|
124
|
+
const description = [current ? "current" : "", describeModel(catalog.lookup(id, model))].filter(Boolean).join(" · ");
|
|
125
|
+
return { value: `${id}/${model}`, label: `${current ? "* " : " "}${id}/${model}`, ...(description ? { description } : {}) };
|
|
126
|
+
}; // port #60
|
|
127
|
+
for (const { id, r } of results) {
|
|
128
|
+
if (!r.ok) { problems.push(`${id}: ${r.error}`); continue; }
|
|
129
|
+
if (r.models.length === 0) { problems.push(`${id}: the endpoint listed no models (no /models route?) — /model ${id}/<model> still works`); continue; }
|
|
130
|
+
for (const m of r.models) {
|
|
131
|
+
items.push(itemFor(id, m)); // port #60: same live ids, optional context/pricing/capabilities
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const p of problems) note(ctx, p, "warn");
|
|
135
|
+
// port #60: never hide the running model just because a provider's list does not know it.
|
|
136
|
+
const currentRef = `${ctx.state.provider}/${ctx.state.model}`;
|
|
137
|
+
if (ctx.state.model && ids.includes(ctx.state.provider) && !items.some((item) => item.value === currentRef)) items.unshift(itemFor(ctx.state.provider, ctx.state.model));
|
|
138
|
+
if (items.length === 0) return;
|
|
139
|
+
|
|
140
|
+
// the current model first: the list is long and the answer to "what am I on?" should not need scrolling
|
|
141
|
+
items.sort((a, b) => Number(b.label.startsWith("*")) - Number(a.label.startsWith("*")));
|
|
142
|
+
// short on purpose: the box clips its title, and it already draws an `esc` affordance of its own
|
|
143
|
+
const picked = await ctx.renderer.pickOne(items, `pick a model · ${items.length} from ${ids.length} provider${ids.length === 1 ? "" : "s"}`);
|
|
144
|
+
if (picked === null || (!save && picked === `${ctx.state.provider}/${ctx.state.model}`)) return; // port #60: cancel/re-pick changes no slot; --save still persists
|
|
145
|
+
cmdModel(ctx, save ? `${picked} --save` : picked); // ONE path for applying a model, flags and all
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** /provider list|add|remove|use|test|key */
|
|
149
|
+
export async function cmdProvider(ctx: ProviderCmdCtx, arg: string): Promise<void> {
|
|
150
|
+
const words = arg.split(/\s+/).filter((w) => w.length > 0);
|
|
151
|
+
const [action, ...rest] = words;
|
|
152
|
+
const reg = ctx.rt.providers;
|
|
153
|
+
switch (action) {
|
|
154
|
+
case undefined:
|
|
155
|
+
case "list":
|
|
156
|
+
note(ctx, formatProviderList(reg, { all: rest.includes("--all") }));
|
|
157
|
+
return;
|
|
158
|
+
case "add": {
|
|
159
|
+
const parsed = parseAddArgs(rest);
|
|
160
|
+
if ("error" in parsed) { note(ctx, parsed.error, "warn"); return; }
|
|
161
|
+
const r = reg.add(parsed.spec, parsed.scope);
|
|
162
|
+
if ("error" in r) { note(ctx, r.error, "warn"); return; }
|
|
163
|
+
const key = isConfigured(r) ? "" : `\nkey: /provider key ${r.id} <secret> · or set ${r.keyEnv} (live, no restart)`;
|
|
164
|
+
note(ctx, `added ${r.id} (${r.protocol}, ${r.baseUrl}) [${parsed.scope}]${key}\nuse it: /model ${r.id}/${r.defaultModel ?? "<model>"}`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
case "remove": {
|
|
168
|
+
if (rest[0] === undefined) break;
|
|
169
|
+
const r = reg.remove(rest[0]);
|
|
170
|
+
if ("error" in r) { note(ctx, r.error, "warn"); return; }
|
|
171
|
+
note(ctx, r.removed.length > 0 ? `removed ${rest[0]} (${r.removed.join(", ")} providers.json)` : `${rest[0]} was not in any providers.json`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
case "use": {
|
|
175
|
+
if (rest[0] === undefined) break;
|
|
176
|
+
localWrite = true;
|
|
177
|
+
let r: ReturnType<typeof reg.setDefault>;
|
|
178
|
+
try { r = reg.setDefault(rest[0]); } finally { localWrite = false; }
|
|
179
|
+
if ("error" in r) { note(ctx, r.error, "warn"); return; }
|
|
180
|
+
applyRef(ctx, r);
|
|
181
|
+
const p = reg.get(r.provider);
|
|
182
|
+
const keyNote = p !== undefined && !isConfigured(p) ? `\n${reg.keyHint(p)}` : "";
|
|
183
|
+
note(ctx, `default → ${r.provider}/${r.model} (saved; this session switched)${keyNote}`, keyNote.length > 0 ? "warn" : undefined);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
case "test": {
|
|
187
|
+
if (rest[0] === undefined) break;
|
|
188
|
+
note(ctx, `testing ${rest[0]}${rest[1] !== undefined ? `/${rest[1]}` : ""}…`);
|
|
189
|
+
const r = await reg.probe(rest[0], rest[1]);
|
|
190
|
+
note(ctx, `${rest[0]}/${r.model}: ${r.detail}`, r.ok ? undefined : "warn");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
case "key": {
|
|
194
|
+
const [id, secret] = rest;
|
|
195
|
+
if (id === undefined || secret === undefined) break;
|
|
196
|
+
const p = reg.get(id);
|
|
197
|
+
if (p === undefined) { note(ctx, `unknown provider "${id}" — /provider add ${id} <baseUrl> first`, "warn"); return; }
|
|
198
|
+
saveCredential(id, secret, p.keyEnv);
|
|
199
|
+
reg.refresh();
|
|
200
|
+
note(ctx, `stored ${p.keyEnv} for ${id} (${redactSecret(secret)}) — live now`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
default:
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
note(ctx, `usage: /provider list [--all] · /provider ${ADD_USAGE} · remove <id> · use <provider/model> · test <id> [model] · key <id> <secret>`, "warn");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------- /setup ----------
|
|
210
|
+
|
|
211
|
+
/** the watcher + poll an unfinished /setup left armed (one at a time; a new /setup replaces it) */
|
|
212
|
+
let pendingSetup: (() => void) | null = null;
|
|
213
|
+
|
|
214
|
+
/** /setup — the TUI's guided connect: provider from the picker, model from a question card, then the
|
|
215
|
+
* key. The cards have no masked input, so the key is a HAND-OFF: `rovecode auth set <id>` in another
|
|
216
|
+
* terminal (masked) or `/provider key` here. The live registry notices the key landing (a 1 s poll keeps
|
|
217
|
+
* its mtime check warm while a setup is pending) and this finishes the job — one test call, default,
|
|
218
|
+
* session switch — without another command. Wording: core/voice.ts. */
|
|
219
|
+
export async function cmdSetup(ctx: ProviderCmdCtx): Promise<void> {
|
|
220
|
+
const reg = ctx.rt.providers;
|
|
221
|
+
pendingSetup?.(); pendingSetup = null;
|
|
222
|
+
const items = SETUP_PICKS.map((p) => ({ value: p.key, label: p.label }));
|
|
223
|
+
const key = await ctx.renderer.pickOne(items, "◆ connect a model — which provider? (Esc = cancel)");
|
|
224
|
+
if (key === null) { note(ctx, "setup cancelled — nothing changed. /setup whenever you like"); return; }
|
|
225
|
+
const pick = SETUP_PICKS.find((p) => p.key === key);
|
|
226
|
+
if (pick === undefined) return;
|
|
227
|
+
const askText = async (question: string): Promise<string | null> => {
|
|
228
|
+
const a = await ctx.renderer.askQuestion({ question, options: [], allowFreeText: true }).catch(() => null);
|
|
229
|
+
return a?.text?.trim() ?? null;
|
|
230
|
+
};
|
|
231
|
+
let id: string;
|
|
232
|
+
if (pick.url !== undefined) {
|
|
233
|
+
const idText = ((await askText("Short name for it? (letters, digits, - _ .)")) ?? "").toLowerCase();
|
|
234
|
+
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; }
|
|
235
|
+
const baseUrl = (await askText(`Base URL for ${idText}? (e.g. https://host/v1)`)) ?? "";
|
|
236
|
+
if (!/^https?:\/\//.test(baseUrl)) { note(ctx, `setup stopped — "${baseUrl}" is not an http(s) URL. ${next("/setup")}`, "warn"); return; }
|
|
237
|
+
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);
|
|
238
|
+
const noKey = keyA?.choice === 1;
|
|
239
|
+
const r = reg.add({ id: idText, baseUrl, protocol: pick.url, ...(noKey ? { noKey: true } : {}) }, "user");
|
|
240
|
+
if ("error" in r) { note(ctx, `setup stopped — ${r.error}`, "warn"); return; }
|
|
241
|
+
id = idText;
|
|
242
|
+
note(ctx, `◆ ${id} registered (${pick.url} protocol, ${baseUrl}).`);
|
|
243
|
+
} else {
|
|
244
|
+
id = pick.id ?? "";
|
|
245
|
+
const known = reg.get(id);
|
|
246
|
+
if (known === undefined) { note(ctx, `setup stopped — ${id} is missing from the built-in table`, "warn"); return; }
|
|
247
|
+
if (pick.local === true && known.noKey !== true) {
|
|
248
|
+
// the built-in row expects a key env; a local server needs none — the user entry says so
|
|
249
|
+
const r = reg.add({ id, baseUrl: known.baseUrl, protocol: known.protocol, noKey: true, ...(known.defaultModel !== undefined ? { defaultModel: known.defaultModel } : {}) }, "user");
|
|
250
|
+
if ("error" in r) { note(ctx, `setup stopped — ${r.error}`, "warn"); return; }
|
|
251
|
+
note(ctx, `◆ ${id} marked as a local server (${known.baseUrl}, no key).`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const spec = reg.get(id);
|
|
255
|
+
if (spec === undefined) return;
|
|
256
|
+
// A provider that ships a default model does not need a question: taking it is right nearly always,
|
|
257
|
+
// and `/model <id>/<other> --save` changes it in one line afterwards. Only an endpoint with no
|
|
258
|
+
// default (a bare proxy, a self-hosted server) still has to be asked — there is nothing to assume.
|
|
259
|
+
let model = spec.defaultModel;
|
|
260
|
+
if (model === undefined) {
|
|
261
|
+
const mA = await ctx.renderer.askQuestion({ question: `Which model id on ${id}?`, options: [], allowFreeText: true }).catch(() => null);
|
|
262
|
+
model = mA?.text?.trim() || mA?.label;
|
|
263
|
+
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; }
|
|
264
|
+
} else {
|
|
265
|
+
note(ctx, `◆ model → ${id}/${model} (change it any time: /model ${id}/<other> --save)`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const finish = async (): Promise<void> => {
|
|
269
|
+
note(ctx, `◆ testing ${id}/${model} …`);
|
|
270
|
+
const r = await reg.probe(id, model);
|
|
271
|
+
if (r.ok) note(ctx, ` ${r.detail}`);
|
|
272
|
+
else note(ctx, ` that didn't work: ${r.detail}\n ${next(`check the key and the URL, then /provider test ${id} ${model}`)}`, "warn");
|
|
273
|
+
localWrite = true;
|
|
274
|
+
let d: ReturnType<typeof reg.setDefault>;
|
|
275
|
+
try { d = reg.setDefault(`${id}/${model}`, "user"); } finally { localWrite = false; }
|
|
276
|
+
if ("error" in d) { note(ctx, ` ${d.error}`, "warn"); return; }
|
|
277
|
+
applyRef(ctx, d);
|
|
278
|
+
note(ctx, `◆ default → ${d.provider}/${d.model} — this session switched too.\n${SETUP_DONE_TUI}`);
|
|
279
|
+
};
|
|
280
|
+
if (isConfigured(spec)) { await finish(); return; }
|
|
281
|
+
note(ctx, [
|
|
282
|
+
`◆ I need the key for ${id}. Paste it hidden in another terminal:`,
|
|
283
|
+
` rovecode auth set ${id}`,
|
|
284
|
+
` or here (this line only, redacted in the note): /provider key ${id} <secret>`,
|
|
285
|
+
` or set ${spec.keyEnv} in your environment.`,
|
|
286
|
+
" I'll notice as soon as it lands and finish the setup.",
|
|
287
|
+
].join("\n"));
|
|
288
|
+
armKeyWatch(reg, id, finish);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Park until the key for `id` lands, then run `finish` once. The registry's mtime check only runs on
|
|
292
|
+
* access, so a 1 s poll keeps it warm while we wait; one watch at a time — a new /setup or /connect
|
|
293
|
+
* replaces the one before it. */
|
|
294
|
+
function armKeyWatch(reg: ProviderCmdCtx["rt"]["providers"], id: string, finish: () => Promise<void>): void {
|
|
295
|
+
const timer = setInterval(() => { reg.snapshot(); }, 1000);
|
|
296
|
+
timer.unref?.();
|
|
297
|
+
const off = reg.onChange(() => {
|
|
298
|
+
const p = reg.get(id);
|
|
299
|
+
if (p === undefined || !isConfigured(p)) return;
|
|
300
|
+
stop();
|
|
301
|
+
void finish();
|
|
302
|
+
});
|
|
303
|
+
const stop = (): void => { clearInterval(timer); off(); if (pendingSetup === stop) pendingSetup = null; };
|
|
304
|
+
pendingSetup = stop;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** /connect — the same job as /setup, but with the answers on the line instead of in cards, so it is one
|
|
308
|
+
* command to paste. Bare, it IS /setup. It runs cli/connect.ts against this session's LIVE registry and
|
|
309
|
+
* applies the new default to the session, like /setup does.
|
|
310
|
+
*
|
|
311
|
+
* There is no masked input in here, so `--key` and `--key-stdin` are refused rather than quietly
|
|
312
|
+
* ignored: a key typed on this line would sit in the transcript. When a key is simply missing,
|
|
313
|
+
* `deferKey` hands over the same way /setup does and the watcher finishes the job when it lands. */
|
|
314
|
+
export async function cmdConnect(ctx: ProviderCmdCtx, arg: string): Promise<void> {
|
|
315
|
+
const words = arg.split(/\s+/).filter((w) => w.length > 0);
|
|
316
|
+
if (words.length === 0) { await cmdSetup(ctx); return; }
|
|
317
|
+
const parsed = parseConnectArgs(words);
|
|
318
|
+
if ("error" in parsed) { note(ctx, `${parsed.error.replace("usage: rovecode connect", "usage: /connect")}`, "warn"); return; }
|
|
319
|
+
if (parsed.key === "prompt" || parsed.key === "stdin") {
|
|
320
|
+
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");
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const reg = ctx.rt.providers;
|
|
324
|
+
pendingSetup?.(); pendingSetup = null;
|
|
325
|
+
|
|
326
|
+
/** the default is written by the time connect returns (even on a failed test call) — bring the
|
|
327
|
+
* session with it, the way /setup does. Also runs on the deferred pass, after the key lands. */
|
|
328
|
+
const follow = (code: number): void => {
|
|
329
|
+
if (code === CONNECT_WAITING || code === 2) return;
|
|
330
|
+
const d = reg.defaultRef();
|
|
331
|
+
if (d !== null && d.provider === parsed.id && d.model.length > 0) applyRef(ctx, d);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const run = async (): Promise<number> => {
|
|
335
|
+
localWrite = true; // the watcher must not announce a default this command already reported
|
|
336
|
+
try {
|
|
337
|
+
return await runConnect(parsed, {
|
|
338
|
+
registry: reg,
|
|
339
|
+
tty: false, // cards, not a terminal: never let runConnect reach for a prompt or for stdin
|
|
340
|
+
out: (l) => note(ctx, l),
|
|
341
|
+
done: SETUP_DONE_TUI,
|
|
342
|
+
where: "tui", // the "→ next:" steps are slash commands in here, not shell lines
|
|
343
|
+
deferKey: (spec) => {
|
|
344
|
+
note(ctx, [
|
|
345
|
+
`◆ I need the key for ${parsed.id}. Paste it hidden in another terminal:`,
|
|
346
|
+
` rovecode auth set ${parsed.id}`,
|
|
347
|
+
` or here (this line only, redacted in the note): /provider key ${parsed.id} <secret>`,
|
|
348
|
+
` or set ${spec.keyEnv} in your environment.`,
|
|
349
|
+
" I'll notice as soon as it lands and finish the job.",
|
|
350
|
+
].join("\n"));
|
|
351
|
+
armKeyWatch(reg, parsed.id, async () => { follow(await run()); });
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
} finally { localWrite = false; }
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
follow(await run());
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Boot-time subscription: when the persisted default changes underneath this session (another
|
|
361
|
+
* terminal's `rovecode model use`, the agent's provider_edit use) the session follows it; when the
|
|
362
|
+
* first provider appears, say so. In-process slash writes report themselves (localWrite). */
|
|
363
|
+
export function watchProviders(ctx: ProviderCmdCtx): () => void {
|
|
364
|
+
let lastSelector = ctx.rt.providers.defaultSelector();
|
|
365
|
+
let hadProvider = ctx.rt.noProviderReason() === null;
|
|
366
|
+
return ctx.rt.providers.onChange(() => {
|
|
367
|
+
const sel = ctx.rt.providers.defaultSelector();
|
|
368
|
+
if (sel !== lastSelector) {
|
|
369
|
+
lastSelector = sel;
|
|
370
|
+
const d = ctx.rt.providers.defaultRef();
|
|
371
|
+
if (d !== null && d.model.length > 0 && !localWrite) { applyRef(ctx, d); note(ctx, `default model changed → ${d.provider}/${d.model}`); }
|
|
372
|
+
}
|
|
373
|
+
const has = ctx.rt.noProviderReason() === null;
|
|
374
|
+
if (has && !hadProvider) note(ctx, "provider configured — ready for the next prompt");
|
|
375
|
+
hadProvider = has;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Reasoning blocks on the interactive surfaces (port #60): the app's per-message bookkeeping between
|
|
2
|
+
* `reasoning_update` events and a Renderer — ONE collapsed block per assistant message, opened on the
|
|
3
|
+
* turn's first reasoning event, settled when the message's text starts, the turn ends or the run ends.
|
|
4
|
+
* A renderer with `beginReasoning` (the classic card) gets the settle; one without gets a single system
|
|
5
|
+
* note carrying the collapsed label once the block settles.
|
|
6
|
+
*
|
|
7
|
+
* THE DELIBERATE DIFFERENCE FROM AION: rovecode's reasoning_update carries `tokens`, never text —
|
|
8
|
+
* the loop drops the thinking text on purpose ("only its estimated size leaves the loop", loop.ts:272),
|
|
9
|
+
* so there is nothing to append and this module counts tokens instead of characters. The property that
|
|
10
|
+
* matters survives the translation: the thinking text is never rendered and never kept here.
|
|
11
|
+
* `reasoningLabel` is the ONE wording every surface prints (the classic card, the fallback note).
|
|
12
|
+
* Pure: no clock, no timers, no process. */
|
|
13
|
+
|
|
14
|
+
import type { RunEvent } from "../core/types.ts";
|
|
15
|
+
import type { AssistantView, Renderer } from "./renderer.ts";
|
|
16
|
+
|
|
17
|
+
/** `reasoning · 1234 tokens …` while streaming, `reasoning · 1234 tokens · collapsed` once settled */
|
|
18
|
+
export function reasoningLabel(tokens: number, streaming: boolean): string {
|
|
19
|
+
return `reasoning · ${tokens} tokens${streaming ? " …" : " · collapsed"}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** the `--plain` REPL's one line per block (printed when the message's text starts or the turn ends) */
|
|
23
|
+
export function collapsedReasoningLine(tokens: number): string {
|
|
24
|
+
return ` ∴ ${reasoningLabel(tokens, false)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface OpenBlock { id: string; view: AssistantView | null; tokens: number }
|
|
28
|
+
|
|
29
|
+
export class ReasoningViews {
|
|
30
|
+
private open: OpenBlock | null = null;
|
|
31
|
+
constructor(private readonly renderer: Pick<Renderer, "addSystemNote" | "beginReasoning">) {}
|
|
32
|
+
|
|
33
|
+
/** feed every RunEvent of the live run (app.ts: right after renderer.onEvent, before the per-event calls).
|
|
34
|
+
* `reasoning_update.tokens` is CUMULATIVE for the message (a dropped event costs nothing), so the block
|
|
35
|
+
* takes the event's value rather than summing. */
|
|
36
|
+
onEvent(ev: RunEvent): void {
|
|
37
|
+
if (ev.type === "reasoning_update") {
|
|
38
|
+
if (this.open && this.open.id !== ev.messageId) this.settle();
|
|
39
|
+
if (!this.open) this.open = { id: ev.messageId, view: this.renderer.beginReasoning?.() ?? null, tokens: 0 };
|
|
40
|
+
this.open.tokens = Math.max(this.open.tokens, ev.tokens);
|
|
41
|
+
this.open.view?.set?.(`∴ ${reasoningLabel(this.open.tokens, true)}`);
|
|
42
|
+
} else if (ev.type === "message_update" || ev.type === "turn_end" || ev.type === "run_end") this.settle();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** the open block (if any) is done: the card settles, or the fallback note lands */
|
|
46
|
+
settle(): void {
|
|
47
|
+
const o = this.open;
|
|
48
|
+
if (!o) return;
|
|
49
|
+
this.open = null;
|
|
50
|
+
if (o.view) o.view.done();
|
|
51
|
+
else this.renderer.addSystemNote(`∴ ${reasoningLabel(o.tokens, false)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** tokens of the block still open (tests) */
|
|
55
|
+
get openTokens(): number { return this.open?.tokens ?? 0; }
|
|
56
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
/** port #60 (OPTIONAL): replace the view's content — the reasoning card uses it because a reasoning_update
|
|
48
|
+
* carries the cumulative TOKEN count, not deltas, so the card repaints one line per event. A view without
|
|
49
|
+
* it (plain transcript appends) is left alone; ReasoningViews skips the update instead of appending counts. */
|
|
50
|
+
set?(content: string): void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SlashCommand {
|
|
54
|
+
name: string;
|
|
55
|
+
description: string;
|
|
56
|
+
/** /help topic the command is listed under (info-cmd.ts); the palette ignores it */
|
|
57
|
+
group?: string;
|
|
58
|
+
/** the argument values (`/effort` levels; `/model` ids): the sextant lists them as suggestions once
|
|
59
|
+
* the command is typed and completes the one picked. A function is read at suggestion time, for a
|
|
60
|
+
* set that changes while the TUI runs (the models of the providers that have a key). */
|
|
61
|
+
choices?: readonly string[] | (() => readonly string[]);
|
|
62
|
+
/** what Enter does on a picked choice: "submit" (default — `/effort high` runs) or "complete" — the
|
|
63
|
+
* choice is a subcommand that wants more words (`/mcp add ` waits for a name) */
|
|
64
|
+
choicesThen?: "submit" | "complete";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface PickItem { value: string; label: string; description?: string }
|
|
68
|
+
|
|
69
|
+
export interface RendererStartOptions {
|
|
70
|
+
/** false after a loading intro: reveal the complete layout, not a second panel animation. */
|
|
71
|
+
animate?: boolean;
|
|
72
|
+
/** Called once the scene exists but before its first paint, to seed transcript and status. */
|
|
73
|
+
beforeFirstRender?: () => void;
|
|
74
|
+
/** Keep the prepared frame offscreen while the loading intro completes. Never reads stdin. */
|
|
75
|
+
beforeReveal?: () => Promise<void>;
|
|
76
|
+
/** Last handoff: the frame is laid out, immediately before terminal takeover/output. */
|
|
77
|
+
onReveal?: () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface Renderer {
|
|
81
|
+
start(hooks: RendererHooks, options?: RendererStartOptions): void | Promise<void>;
|
|
82
|
+
stop(): void;
|
|
83
|
+
/** Put the terminal back and nothing else — no settling, no child processes, no async. `stop()` is the
|
|
84
|
+
* orderly shutdown and calls this as its last act; this is what the CRASH path calls, because there
|
|
85
|
+
* the only thing that still matters is that the person gets their shell back. Separated because
|
|
86
|
+
* `stop()` does work that can itself throw, and a throw before the restore is how a crash turns into
|
|
87
|
+
* "my terminal is broken": the alt screen never closes and mouse reporting keeps typing escape
|
|
88
|
+
* sequences at the prompt. Must be idempotent and must never throw. Optional: a renderer that never
|
|
89
|
+
* touched the terminal (the test fake) has nothing to restore. */
|
|
90
|
+
restoreTerminal?(): void;
|
|
91
|
+
/** slash commands offered by editor autocomplete; call before start() */
|
|
92
|
+
setCommands(cmds: SlashCommand[]): void;
|
|
93
|
+
addUser(text: string): void;
|
|
94
|
+
addSystemNote(text: string, tone?: "info" | "warn" | "error"): void;
|
|
95
|
+
beginAssistant(): AssistantView;
|
|
96
|
+
/** port #60 (OPTIONAL): a collapsed reasoning block for the assistant message being streamed — its view
|
|
97
|
+
* settles on done(); surfaces render a SIZE (tokens), never the text. A renderer without it gets one
|
|
98
|
+
* system note per block from tui/reasoning-view.ts instead. */
|
|
99
|
+
beginReasoning?(): AssistantView;
|
|
100
|
+
toolStart(callId: string, tool: string, argsPreview: string): void;
|
|
101
|
+
toolUpdate(callId: string, note: string): void;
|
|
102
|
+
toolEnd(callId: string, ok: boolean, outputPreview: string, durationMs: number): void;
|
|
103
|
+
/** modal approval; resolves deny on cancel/escape. `detail` (port #24): pre-rendered
|
|
104
|
+
* unified diff of a pending edit/write, shown inside the overlay above the verdicts */
|
|
105
|
+
askApproval(tool: string, argsPreview: string, detail?: string): Promise<ApprovalAnswer>;
|
|
106
|
+
/** modal picker (session/turn navigators); resolves null on cancel/escape/stop */
|
|
107
|
+
pickOne(items: PickItem[], title?: string): Promise<string | null>;
|
|
108
|
+
/** modal question for the ask_user tool (port #33): options + a free-text entry when allowed.
|
|
109
|
+
* Resolves the pick/typed text; null when the user declines, `signal` aborts (the run's
|
|
110
|
+
* controller — the card must dismiss, never leak), or the UI stops. Implementations may
|
|
111
|
+
* reject a SECOND concurrent ask with a clear error (the tool turns it into a failed result). */
|
|
112
|
+
askQuestion(q: QuestionPrompt, signal?: AbortSignal): Promise<QuestionAnswer | null>;
|
|
113
|
+
/** remove all transcript items (history replay after rewind/resume) */
|
|
114
|
+
clearTranscript(): void;
|
|
115
|
+
/** place text in the editor for edit-and-resubmit (pi sessions.md:106-116) */
|
|
116
|
+
prefillEditor(text: string): void;
|
|
117
|
+
setBusy(busy: boolean, label?: string): void;
|
|
118
|
+
setStatus(info: StatusInfo): void;
|
|
119
|
+
/** port #44 (OPTIONAL — FakeRenderer/PiTuiRenderer omit it): every RunEvent of the live run, delivered
|
|
120
|
+
* as the FIRST statement of the app's event loop, BEFORE the toolStart/beginAssistant/addSystemNote
|
|
121
|
+
* calls the same event triggers. A renderer that implements it (sextant) treats the event stream as
|
|
122
|
+
* its source of truth for transcript rows and ignores the duplicate per-event calls while busy. */
|
|
123
|
+
onEvent?(ev: RunEvent): void;
|
|
124
|
+
/** port #44 (OPTIONAL): the runtime handles the sextant panels read — cwd, sessions dir, the ACTIVE
|
|
125
|
+
* store, tasks, model + context window, usage math. Called exactly once by runTui, after runtime
|
|
126
|
+
* construction and before start(). */
|
|
127
|
+
attach?(ctx: SextantAttach): void;
|
|
128
|
+
}
|
|
@@ -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
|
+
}
|