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,187 @@
|
|
|
1
|
+
/** Sextant plan + usage painters (port #41). Ported from the user's sextant v0.4.0 app.js:675-744
|
|
2
|
+
* (drawPlan steps/crew/next, drawUsage tokens/context bar/cost); the scenario phases are gone — the
|
|
3
|
+
* steps ARE the session's todos (tools/todo.ts loadTodos), the crew rows are TaskManager TaskInfo,
|
|
4
|
+
* usage comes from the run's real token/cost accounting. Pure: `now` only drives the crew spinner. */
|
|
5
|
+
|
|
6
|
+
import { ATTR } from "./types.ts";
|
|
7
|
+
import type { Rect, ScreenLike, Seg, SextantState, Theme } from "./types.ts";
|
|
8
|
+
import type { TaskInfo } from "../core/tasks.ts";
|
|
9
|
+
import { EMPTY } from "../core/voice.ts";
|
|
10
|
+
import type { TodoItem } from "../tools/todo.ts";
|
|
11
|
+
import { fmtK, planCounts } from "./model.ts";
|
|
12
|
+
import { crewSummary, laneGlyph, laneStatus, laneTone } from "./draw-agents.ts";
|
|
13
|
+
import { crewCardBlock, crewCardHeight, groupCrew, paintCrewCards, type CrewTask } from "./crew-cards.ts";
|
|
14
|
+
import { panel } from "./layout.ts";
|
|
15
|
+
import { st } from "./theme.ts";
|
|
16
|
+
import { driftRow } from "./draw-context.ts";
|
|
17
|
+
|
|
18
|
+
/** the crew section's row ceiling: two full cards and the separator between them (the card is 3 rows —
|
|
19
|
+
* crew-cards.ts). Beyond that the plan's own steps lose their room, and the `+N more` footer says what is
|
|
20
|
+
* not shown; the full board is `∷` (draw-agents.ts). */
|
|
21
|
+
const CREW_ROWS_MAX = 7;
|
|
22
|
+
export const STEP_GLYPH: Record<TodoItem["status"], string> = { completed: "◆", in_progress: "◈", pending: "◇" };
|
|
23
|
+
|
|
24
|
+
const oneLine = (text: string): string => text.replace(/\s+/g, " ").trim();
|
|
25
|
+
/** greedy word wrap (app.js wrap) — long words are split at the width */
|
|
26
|
+
export function wrapText(text: string, width: number): string[] {
|
|
27
|
+
const out: string[] = [];
|
|
28
|
+
if (width <= 0) return out;
|
|
29
|
+
for (const para of oneLine(text).split("\n")) {
|
|
30
|
+
let line = "";
|
|
31
|
+
for (const w of para.split(" ")) {
|
|
32
|
+
if (!line) { line = w; continue; }
|
|
33
|
+
if ([...line].length + 1 + [...w].length <= width) line += " " + w; else { out.push(line); line = w; }
|
|
34
|
+
}
|
|
35
|
+
while ([...line].length > width) { const cps = [...line]; out.push(cps.slice(0, width).join("")); line = cps.slice(width).join(""); }
|
|
36
|
+
out.push(line);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `next` = the in_progress item, then the first pending one */
|
|
42
|
+
export function nextSteps(todos: readonly TodoItem[]): TodoItem[] {
|
|
43
|
+
const out: TodoItem[] = [];
|
|
44
|
+
const cur = todos.find((t) => t.status === "in_progress"); if (cur) out.push(cur);
|
|
45
|
+
const pend = todos.find((t) => t.status === "pending"); if (pend) out.push(pend);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/** non-terminal tasks = "working" (queued + running) — crewSummary(crew).working agrees */
|
|
49
|
+
export const crewWorking = (crew: readonly TaskInfo[]): number => crewSummary(crew).working;
|
|
50
|
+
/** the crew board's glyph + tone (#46 draw-agents): ◇ queued · spinner running · ◆ done · × failed · ▪ cancelled */
|
|
51
|
+
export function crewGlyph(t: TaskInfo, theme: Theme, now: number): [string, number] {
|
|
52
|
+
return [laneGlyph(t.status, now), laneTone(t.status, theme)];
|
|
53
|
+
}
|
|
54
|
+
/** the crew board's status word (#46 laneStatus): `working mm:ss` while running, else the status */
|
|
55
|
+
export const crewStatus = (t: TaskInfo, now = 0): string => laneStatus(t, now);
|
|
56
|
+
|
|
57
|
+
/** Plan panel: `plan done/total` title, todo steps (◆ done · ◈ in_progress · ◇ pending, priority hint
|
|
58
|
+
* at the right), then — anchored to the bottom — `crew k working` (when any) and `next`. */
|
|
59
|
+
export function drawPlan(scr: ScreenLike, R: Rect, s: SextantState, theme: Theme, now = 0): void {
|
|
60
|
+
const todos = s.plan.todos, c = planCounts(s);
|
|
61
|
+
const B = panel(scr, R, "plan", false, todos.length ? [[`${c.completed}/${c.total}`, st(theme.muted)]] : [], theme);
|
|
62
|
+
const yEnd = B.y + B.h;
|
|
63
|
+
let y = B.y;
|
|
64
|
+
if (s.plan.note) for (const line of wrapText(s.plan.note, B.w).slice(0, 2)) if (y < yEnd) scr.put(B.x, y++, line, st(theme.warn), B.w);
|
|
65
|
+
const next = nextSteps(todos);
|
|
66
|
+
// the crew section is CARDS now (Berkay's pick): one card per workflow — the tasks of one run — never one row
|
|
67
|
+
// per task. The height is what the cards actually need, capped; the block itself drops spacing, then names,
|
|
68
|
+
// then whole cards behind a footer (crew-cards.ts states the order).
|
|
69
|
+
const groups = groupCrew(s.crew as readonly CrewTask[]);
|
|
70
|
+
const crewNeed = groups.length ? Math.min(CREW_ROWS_MAX, crewCardHeight(groups, B.w, now)) : 0;
|
|
71
|
+
let nextRows = next.length ? next.length + 1 : 0, crewRows = crewNeed ? crewNeed + 1 : 0;
|
|
72
|
+
let nextY = yEnd - nextRows;
|
|
73
|
+
let crewY = (nextRows ? nextY - 1 : yEnd) - crewRows;
|
|
74
|
+
const minY = y + 2; // keep the steps header + one row above the anchored blocks
|
|
75
|
+
if (crewRows && crewY < minY) { crewRows = 0; crewY = yEnd; }
|
|
76
|
+
if (nextRows && nextY < minY) { nextRows = 0; nextY = yEnd; }
|
|
77
|
+
const stepsEnd = crewRows ? crewY - 1 : nextRows ? nextY - 1 : yEnd;
|
|
78
|
+
scr.text(B.x, y++, [["steps", st(theme.muted)], [todos.length ? ` ${c.completed}/${c.total}` : "", st(theme.dim)]], B.w);
|
|
79
|
+
if (!todos.length && y < stepsEnd) { // empty plan: the hint's second line only when there is room
|
|
80
|
+
scr.put(B.x, y++, EMPTY.plan[0], st(theme.dim), B.w);
|
|
81
|
+
if (y < stepsEnd) scr.put(B.x, y++, EMPTY.plan[1], st(theme.dim), B.w);
|
|
82
|
+
}
|
|
83
|
+
// a long plan windows around the step in progress instead of always showing its head: the person
|
|
84
|
+
// wants to see what is being done now, not steps 1-8 of 30. One row above says how many are earlier.
|
|
85
|
+
const room = Math.max(0, stepsEnd - y), curIdx = todos.findIndex((t) => t.status === "in_progress");
|
|
86
|
+
let start = 0;
|
|
87
|
+
if (todos.length > room && curIdx >= room - 1) { // the current step would be hidden (or be the marker row)
|
|
88
|
+
const visible = Math.max(1, room - 2); // minus the "earlier" row and the "+N more" row
|
|
89
|
+
start = Math.max(0, Math.min(curIdx - Math.floor(visible / 2), todos.length - visible));
|
|
90
|
+
}
|
|
91
|
+
if (start > 0 && y < stepsEnd) scr.put(B.x, y++, ` …${start} earlier`, st(theme.dim), B.w);
|
|
92
|
+
let shown = start;
|
|
93
|
+
for (const t of todos.slice(start)) {
|
|
94
|
+
if (y >= stepsEnd) break;
|
|
95
|
+
const cur = t.status === "in_progress", done = t.status === "completed";
|
|
96
|
+
const pr = t.priority === "high" ? "high" : t.priority === "low" ? "low" : "";
|
|
97
|
+
scr.put(B.x, y, STEP_GLYPH[t.status], st(done ? theme.okDim : cur ? theme.accent : theme.dim));
|
|
98
|
+
scr.clip(B.x + 3, y, oneLine(t.content), st(done ? theme.fg2 : cur ? theme.fg : theme.muted, -1, cur ? ATTR.BOLD : 0), B.w - 3 - (pr ? pr.length + 1 : 0));
|
|
99
|
+
if (pr) scr.put(B.x + B.w - pr.length, y, pr, st(t.priority === "high" ? theme.warn : theme.dim));
|
|
100
|
+
y++; shown++;
|
|
101
|
+
}
|
|
102
|
+
if (shown < todos.length && shown > 0) { y--; scr.fill(B.x, y, B.w, 1, " "); scr.put(B.x, y, ` +${todos.length - shown + 1} more`, st(theme.dim), B.w); }
|
|
103
|
+
if (crewRows) {
|
|
104
|
+
let cy = crewY;
|
|
105
|
+
scr.text(B.x, cy++, [["crew", st(theme.muted)], [` ${crewSummary(s.crew).text}`, st(theme.dim)]], B.w);
|
|
106
|
+
const block = crewCardBlock(groups, B.w, crewRows - 1, now);
|
|
107
|
+
paintCrewCards(scr, B.x, cy, B.w, block.lines, theme);
|
|
108
|
+
}
|
|
109
|
+
if (nextRows) {
|
|
110
|
+
let ny = nextY;
|
|
111
|
+
scr.put(B.x, ny++, "next", st(theme.muted));
|
|
112
|
+
for (const t of next) {
|
|
113
|
+
const cur = t.status === "in_progress";
|
|
114
|
+
scr.text(B.x, ny++, [[STEP_GLYPH[t.status] + " ", st(cur ? theme.accent : theme.dim)], [oneLine(t.content), st(cur ? theme.fg : theme.muted)]], B.w);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** filled cells of a `width`-wide context bar at `pct` (0..100) */
|
|
120
|
+
export const barFilled = (pct: number, width: number): number => Math.round((Math.max(0, Math.min(100, pct)) / 100) * width);
|
|
121
|
+
/** the first candidate that fits `width` cells, else the last one clipped with an ellipsis */
|
|
122
|
+
export function fitText(candidates: readonly string[], width: number): string {
|
|
123
|
+
for (const c of candidates) if ([...c].length <= width) return c;
|
|
124
|
+
const last = candidates[candidates.length - 1];
|
|
125
|
+
if (last === undefined || width <= 0) return "";
|
|
126
|
+
return width === 1 ? "…" : [...last].slice(0, width - 1).join("") + "…";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Usage panel (3 inner rows): `tokens 4.2k` (+ in/out split when it fits), `context ━━──── 12%`
|
|
130
|
+
* (or `context ?` when the window is unknown), `cost $0.030` (or `cost —` when unpriced) +
|
|
131
|
+
* provider/model (model alone, then clipped, when narrow). */
|
|
132
|
+
/** Token counts for the context row, as short as they can be and still be read: `842`, `4.2k`, `24k`,
|
|
133
|
+
* `200k`, `1M`. fmtK keeps a decimal at every size, which is right where a number stands alone and
|
|
134
|
+
* wrong in a pair — `24.0k/200.0k` costs six characters that say nothing and eats the bar beside it. */
|
|
135
|
+
function fmtCtx(n: number): string {
|
|
136
|
+
if (n < 1000) return String(Math.round(n));
|
|
137
|
+
if (n < 10_000) return `${(n / 1000).toFixed(1)}k`;
|
|
138
|
+
if (n < 1_000_000) return `${Math.round(n / 1000)}k`;
|
|
139
|
+
const m = n / 1_000_000;
|
|
140
|
+
return m < 10 && m !== Math.round(m) ? `${m.toFixed(1)}M` : `${Math.round(m)}M`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function drawUsage(scr: ScreenLike, R: Rect, s: SextantState, theme: Theme): void {
|
|
144
|
+
const B = panel(scr, R, "usage", false, [], theme);
|
|
145
|
+
const u = s.usage;
|
|
146
|
+
const label = (t: string): Seg => [t.padEnd(10), st(theme.muted)];
|
|
147
|
+
const total = fmtK(u.tokensIn + u.tokensOut);
|
|
148
|
+
const split = fitText([` ${fmtK(u.tokensIn)} in · ${fmtK(u.tokensOut)} out`, ` ${fmtK(u.tokensIn)}/${fmtK(u.tokensOut)}`, ""], B.w - 10 - total.length);
|
|
149
|
+
if (u.tokensIn + u.tokensOut === 0) scr.text(B.x, B.y, [label("tokens"), [EMPTY.usage, st(theme.dim)]], B.w); // nothing spent yet
|
|
150
|
+
else scr.text(B.x, B.y, [label("tokens"), [total, st(theme.fg)], [split, st(theme.dim)]], B.w);
|
|
151
|
+
scr.text(B.x, B.y + 1, [label("context")]);
|
|
152
|
+
if (u.contextPct === null) scr.put(B.x + 10, B.y + 1, "?", st(theme.dim));
|
|
153
|
+
else {
|
|
154
|
+
// A percentage alone goes blind on a large window. claude-opus-5 carries 1,000,000 tokens, so a
|
|
155
|
+
// real working session sits at 0% for hours: you need 10k tokens to move the first digit, and
|
|
156
|
+
// Berkay's report was exactly this — "context shows 0%, we cannot see how much context there is".
|
|
157
|
+
// The numbers are what a person acts on ("42k of 1M" is a decision; "0%" is not), so they get the
|
|
158
|
+
// room and the percent keeps only what is left. The bar stays: it is the at-a-glance channel, and
|
|
159
|
+
// it is the only part that reads correctly when the window is small.
|
|
160
|
+
const pct = `${Math.round(u.contextPct)}%`;
|
|
161
|
+
const nums = u.contextWindow !== undefined ? `${fmtCtx(u.contextTokens ?? 0)}/${fmtCtx(u.contextWindow)}` : "";
|
|
162
|
+
const tail = nums === "" ? pct : `${nums} ${pct}`;
|
|
163
|
+
const barW = Math.max(4, B.w - 10 - tail.length - 2);
|
|
164
|
+
const filled = barFilled(u.contextPct, barW);
|
|
165
|
+
for (let i = 0; i < barW; i++) scr.put(B.x + 10 + i, B.y + 1, i < filled ? "━" : "─", st(i < filled ? theme.accent : theme.rule2));
|
|
166
|
+
const tailX = B.x + 10 + barW + 2;
|
|
167
|
+
if (nums !== "") scr.put(tailX, B.y + 1, nums, st(theme.dim));
|
|
168
|
+
scr.put(tailX + (nums === "" ? 0 : nums.length + 1), B.y + 1, pct, st(u.contextPct > 80 ? theme.warn : theme.fg));
|
|
169
|
+
}
|
|
170
|
+
const cost = u.costUsd === null ? "—" : `$${u.costUsd.toFixed(3)}`;
|
|
171
|
+
const model = u.model ? fitText([u.provider ? ` ${u.provider}/${u.model}` : ` ${u.model}`, ` ${u.model}`], B.w - 10 - cost.length) : "";
|
|
172
|
+
scr.text(B.x, B.y + 2, [label("cost"), [cost, st(theme.fg)], [model, st(theme.dim)]], B.w);
|
|
173
|
+
|
|
174
|
+
// How far the bar above is from what the provider actually counted. It matters here rather than only
|
|
175
|
+
// inside /context because compaction fires on OUR estimate: a model whose real prompt is a fifth bigger
|
|
176
|
+
// than the bar says gets compacted late, and the bar is the thing the human is looking at when they
|
|
177
|
+
// decide whether one more turn fits.
|
|
178
|
+
//
|
|
179
|
+
// Drawn only once /context has been opened, and silent otherwise. The number comes from comparing our
|
|
180
|
+
// count with a turn that reported usage, which the cockpit does not do on its own — and inventing a
|
|
181
|
+
// placeholder ("drift —") would take a row to say nothing. `driftRow` returns undefined when there is
|
|
182
|
+
// nothing honest to say, so this is one condition rather than a special case.
|
|
183
|
+
const d = s.context ? driftRow(s.context.drift, s.context.tolerance) : undefined;
|
|
184
|
+
if (d && B.h > 3) {
|
|
185
|
+
scr.text(B.x, B.y + 3, [label("drift"), [fitText([d.text, ""], B.w - 10), st(d.warn ? theme.warn : theme.dim)]], B.w);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** The tab strip: paging between panels the terminal is too narrow to show at once.
|
|
2
|
+
*
|
|
3
|
+
* layout.ts hides the files panel under 140 columns and the plan/usage column under 110, and until
|
|
4
|
+
* now a hidden panel was simply gone — ⌃e answered "the files panel needs ≥ 140 columns" and that was
|
|
5
|
+
* that. Berkay asked for the panels to behave like tabs instead: on a narrow terminal the MAIN slot
|
|
6
|
+
* (the code panel's rect) shows whichever panel `s.page` names, and a strip on that slot's top border
|
|
7
|
+
* says which panels exist and which one is showing. Clicking a tab pages to it; ⌃e pages to files,
|
|
8
|
+
* ⌃o cycles.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately absent on a wide terminal: when every panel is on screen there is nothing to page,
|
|
11
|
+
* and a strip would be one more thing to read. tabsFor() returns [] and nothing is drawn.
|
|
12
|
+
*
|
|
13
|
+
* Geometry and painting live together here so the hit zones the frame loop registers are the cells
|
|
14
|
+
* that were actually painted — the same rule card-hits.ts and panel-hits.ts follow. The strip
|
|
15
|
+
* overpaints the panel title on the top border (panel() draws the title at x+2); the active tab IS
|
|
16
|
+
* the title. */
|
|
17
|
+
|
|
18
|
+
import { st } from "./draw-util.ts";
|
|
19
|
+
import { strWidth } from "./screen.ts";
|
|
20
|
+
import { ATTR, PAGES, type Layout, type Page, type Rect, type ScreenLike, type SextantState, type Theme } from "./types.ts";
|
|
21
|
+
|
|
22
|
+
export interface Tab {
|
|
23
|
+
page: Page;
|
|
24
|
+
/** the cells the label occupies on the border row */
|
|
25
|
+
rect: Rect;
|
|
26
|
+
active: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const LABEL: Record<Page, string> = { code: "code", files: "files", plan: "plan" };
|
|
30
|
+
const SEP = " · ";
|
|
31
|
+
|
|
32
|
+
/** is this page's own panel hidden by the layout, so it needs a tab to be reachable? */
|
|
33
|
+
export function hiddenByLayout(L: Layout, page: Page): boolean {
|
|
34
|
+
return page === "files" ? L.files === null : page === "plan" ? L.plan === null : false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** the page actually showing in the main slot: `s.page` if that panel is hidden by the layout (so
|
|
38
|
+
* paging is what makes it visible), else code — a page pointing at a panel that is already on
|
|
39
|
+
* screen is inert, never a duplicate */
|
|
40
|
+
export function mainPage(L: Layout, s: SextantState): Page {
|
|
41
|
+
return hiddenByLayout(L, s.page) ? s.page : "code";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The tabs to show, positioned on the main slot's top border; [] on a wide terminal. */
|
|
45
|
+
export function tabsFor(L: Layout, s: SextantState): Tab[] {
|
|
46
|
+
const hidden = PAGES.filter((p) => hiddenByLayout(L, p));
|
|
47
|
+
if (hidden.length === 0) return [];
|
|
48
|
+
const pages: Page[] = ["code", ...hidden];
|
|
49
|
+
const active = mainPage(L, s);
|
|
50
|
+
const y = L.code.y;
|
|
51
|
+
const right = L.code.x + L.code.w - 2; // the border's last usable cell before the corner
|
|
52
|
+
let x = L.code.x + 2;
|
|
53
|
+
const out: Tab[] = [];
|
|
54
|
+
for (const [i, page] of pages.entries()) {
|
|
55
|
+
const w = strWidth(LABEL[page]) + 2; // ` label `
|
|
56
|
+
if (x + w > right) break; // a strip the border cannot hold stops, never wraps
|
|
57
|
+
out.push({ page, rect: { x, y, w, h: 1 }, active: page === active });
|
|
58
|
+
x += w + (i < pages.length - 1 ? strWidth(SEP) : 0);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Paint the strip over the main slot's top border. Pure; returns what it painted. */
|
|
64
|
+
export function drawTabs(scr: ScreenLike, L: Layout, s: SextantState, theme: Theme): Tab[] {
|
|
65
|
+
const tabs = tabsFor(L, s);
|
|
66
|
+
if (tabs.length === 0) return tabs;
|
|
67
|
+
const focused = s.focus === "code" || s.focus === "files";
|
|
68
|
+
for (const [i, t] of tabs.entries()) {
|
|
69
|
+
const style = t.active
|
|
70
|
+
? st(theme.bg, focused ? theme.accent : theme.fg2, ATTR.BOLD)
|
|
71
|
+
: st(theme.muted);
|
|
72
|
+
scr.text(t.rect.x, t.rect.y, [[` ${LABEL[t.page]} `, style]], t.rect.w);
|
|
73
|
+
const next = tabs[i + 1];
|
|
74
|
+
if (next) scr.text(t.rect.x + t.rect.w, t.rect.y, [[SEP, st(theme.frame)]], next.rect.x - (t.rect.x + t.rect.w));
|
|
75
|
+
}
|
|
76
|
+
return tabs;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** the page after `page` among the tabs on screen (⌃o); `page` itself when there is nothing to cycle */
|
|
80
|
+
export function nextPage(L: Layout, s: SextantState): Page {
|
|
81
|
+
const tabs = tabsFor(L, s);
|
|
82
|
+
if (tabs.length < 2) return s.page;
|
|
83
|
+
const i = tabs.findIndex((t) => t.active);
|
|
84
|
+
return tabs[(i + 1) % tabs.length]!.page;
|
|
85
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Sextant surface (port #42) — small pure drawing helpers shared by the code and messages panels.
|
|
2
|
+
* Ported from the user's own sextant v0.4.0 prototype: src/app.js:37 (st), :66-78 (wrap), :210-220
|
|
3
|
+
* (panel). Pure: no clock, no timers, no process access — `now` arrives as a parameter.
|
|
4
|
+
* #44 dedupe: `st` and `panel` are the shared theme.ts / layout.ts ones, re-exported here so the
|
|
5
|
+
* panels' imports stay put (layout.ts panel() = this file's former one: interior cleared to the
|
|
6
|
+
* theme background, extras clipped with an ellipsis before they are dropped). */
|
|
7
|
+
|
|
8
|
+
import { SPIN, type Rect } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export { st } from "./theme.ts";
|
|
11
|
+
export { panel } from "./layout.ts";
|
|
12
|
+
|
|
13
|
+
/** the four-glyph diamond spinner, phase from the frame clock (140 ms per step) */
|
|
14
|
+
export const spinner = (now: number): string => SPIN[Math.floor(now / 140) % SPIN.length] ?? "◇";
|
|
15
|
+
|
|
16
|
+
/** split file content into lines; a trailing newline does not produce a phantom empty line */
|
|
17
|
+
export function splitLines(s: string): string[] {
|
|
18
|
+
const l = s.split("\n");
|
|
19
|
+
if (l.length && l[l.length - 1] === "") l.pop();
|
|
20
|
+
return l;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 1-slot cache: splitLines is called twice per frame (drawFileView + codeScrollTop/rowCount).
|
|
24
|
+
* File content is a stable string reference within a frame; reference equality is the cheapest guard. */
|
|
25
|
+
let _splitCache: { src: string; lines: string[] } | null = null;
|
|
26
|
+
export function splitLinesCached(s: string): string[] {
|
|
27
|
+
if (_splitCache && _splitCache.src === s) return _splitCache.lines;
|
|
28
|
+
const lines = splitLines(s);
|
|
29
|
+
_splitCache = { src: s, lines };
|
|
30
|
+
return lines;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Word-wrap to `width` cells — one cell per code point (types.ts), so an emoji counts once and a
|
|
34
|
+
* surrogate pair never straddles two rows — keeping explicit newlines (a blank paragraph stays a
|
|
35
|
+
* blank row); a word longer than the width is hard-split wherever it starts. Never returns []. */
|
|
36
|
+
export function wrap(text: string, width: number): string[] {
|
|
37
|
+
const w = Math.max(1, width);
|
|
38
|
+
const out: string[] = [];
|
|
39
|
+
for (const para of String(text).split("\n")) {
|
|
40
|
+
let line = "", len = 0;
|
|
41
|
+
for (const word of para.split(" ")) {
|
|
42
|
+
const cps = [...word];
|
|
43
|
+
if (line && len + 1 + cps.length <= w) { line += " " + word; len += 1 + cps.length; continue; }
|
|
44
|
+
if (line) out.push(line);
|
|
45
|
+
while (cps.length > w) out.push(cps.splice(0, w).join(""));
|
|
46
|
+
line = cps.join(""); len = cps.length;
|
|
47
|
+
}
|
|
48
|
+
out.push(line);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** hard-split a line into `width`-cell chunks (terminal-style, no word boundaries; one cell per
|
|
54
|
+
* code point — an astral glyph moves to the next row whole); "" → [""] */
|
|
55
|
+
export function hardWrap(line: string, width: number): string[] {
|
|
56
|
+
const w = Math.max(1, width);
|
|
57
|
+
const cps = [...line];
|
|
58
|
+
if (cps.length <= w) return [line];
|
|
59
|
+
const out: string[] = [];
|
|
60
|
+
for (let i = 0; i < cps.length; i += w) out.push(cps.slice(i, i + w).join(""));
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** inner rect of a panel drawn at `r` (2 cells of horizontal padding, 1 row for each border) */
|
|
65
|
+
export const inner = (r: Rect): Rect => ({ x: r.x + 2, y: r.y + 1, w: r.w - 4, h: r.h - 2 });
|