rovecode 0.4.0 → 0.4.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/package.json +62 -62
- package/src/sextant/boot.ts +74 -0
- package/src/sextant/frame.ts +39 -10
- package/src/sextant/sextant-frame-loop.ts +45 -2
- package/src/sextant/types.ts +3 -0
package/package.json
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "rovecode",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"license": "AGPL-3.0-only",
|
|
5
|
-
"author": "9Code Labs",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "https://github.com/9Code-Labs/rovecode.git"
|
|
9
|
-
},
|
|
10
|
-
"homepage": "https://github.com/9Code-Labs/rovecode",
|
|
11
|
-
"description": "Rovecode: a coding agent for the terminal. Research-derived harness on Bun/TypeScript with evidence-based ports from open-source harnesses (sextant TUI, MCP, ACP, headless server, repo-map, shadow-git checkpoints, execpolicy, model routers).",
|
|
12
|
-
"type": "module",
|
|
13
|
-
"module": "src/index.ts",
|
|
14
|
-
"exports": {
|
|
15
|
-
".": "./src/index.ts",
|
|
16
|
-
"./sdk": "./src/sdk/index.ts",
|
|
17
|
-
"./package.json": "./package.json",
|
|
18
|
-
"./*": "./*"
|
|
19
|
-
},
|
|
20
|
-
"bin": {
|
|
21
|
-
"rovecode": "bin/rovecode.ts"
|
|
22
|
-
},
|
|
23
|
-
"files": [
|
|
24
|
-
"bin",
|
|
25
|
-
"src",
|
|
26
|
-
"vendor",
|
|
27
|
-
"tsconfig.json",
|
|
28
|
-
"THIRD_PARTY_NOTICES.md"
|
|
29
|
-
],
|
|
30
|
-
"engines": {
|
|
31
|
-
"bun": ">=1.3.14"
|
|
32
|
-
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"build": "bun run scripts/build.ts",
|
|
35
|
-
"build:cli": "bun -e \"require('node:fs').rmSync('dist/cli', { recursive: true, force: true })\" && bun build --target=bun --splitting --sourcemap=external src/cli/main.ts --outdir dist/cli && bun run smoke:cli && bun run smoke:dist",
|
|
36
|
-
"smoke:cli": "bun dist/cli/main.js --version",
|
|
37
|
-
"smoke:dist": "bun dist/cli/main.js smoke-tui --sextant",
|
|
38
|
-
"test": "bun test",
|
|
39
|
-
"gauntlet": "bun src/cli/main.ts gauntlet",
|
|
40
|
-
"typecheck": "tsc --noEmit",
|
|
41
|
-
"check": "node scripts/build-model-index.mjs --check && tsc --noEmit && bun test",
|
|
42
|
-
"smoke": "bun src/cli/main.ts smoke-tui --sextant",
|
|
43
|
-
"publish:site": "bash scripts/publish-site-data.sh",
|
|
44
|
-
"build:model-index": "node scripts/build-model-index.mjs"
|
|
45
|
-
},
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"@ast-grep/napi": "^0.45.3",
|
|
48
|
-
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
|
-
"@zed-industries/agent-client-protocol": "^0.4.5",
|
|
50
|
-
"diff": "^9.0.0",
|
|
51
|
-
"get-east-asian-width": "1.6.0",
|
|
52
|
-
"gpt-tokenizer": "^4.0.0",
|
|
53
|
-
"marked": "18.0.11",
|
|
54
|
-
"tokenlens": "^1.3.1"
|
|
55
|
-
},
|
|
56
|
-
"devDependencies": {
|
|
57
|
-
"@opencode-ai/models": "^0.0.64",
|
|
58
|
-
"@types/bun": "latest",
|
|
59
|
-
"@xterm/headless": "6.0.0",
|
|
60
|
-
"typescript": "^5.9.3"
|
|
61
|
-
}
|
|
62
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "rovecode",
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"license": "AGPL-3.0-only",
|
|
5
|
+
"author": "9Code Labs",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/9Code-Labs/rovecode.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/9Code-Labs/rovecode",
|
|
11
|
+
"description": "Rovecode: a coding agent for the terminal. Research-derived harness on Bun/TypeScript with evidence-based ports from open-source harnesses (sextant TUI, MCP, ACP, headless server, repo-map, shadow-git checkpoints, execpolicy, model routers).",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"module": "src/index.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./src/index.ts",
|
|
16
|
+
"./sdk": "./src/sdk/index.ts",
|
|
17
|
+
"./package.json": "./package.json",
|
|
18
|
+
"./*": "./*"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"rovecode": "bin/rovecode.ts"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin",
|
|
25
|
+
"src",
|
|
26
|
+
"vendor",
|
|
27
|
+
"tsconfig.json",
|
|
28
|
+
"THIRD_PARTY_NOTICES.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"bun": ">=1.3.14"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "bun run scripts/build.ts",
|
|
35
|
+
"build:cli": "bun -e \"require('node:fs').rmSync('dist/cli', { recursive: true, force: true })\" && bun build --target=bun --splitting --sourcemap=external src/cli/main.ts --outdir dist/cli && bun run smoke:cli && bun run smoke:dist",
|
|
36
|
+
"smoke:cli": "bun dist/cli/main.js --version",
|
|
37
|
+
"smoke:dist": "bun dist/cli/main.js smoke-tui --sextant",
|
|
38
|
+
"test": "bun test",
|
|
39
|
+
"gauntlet": "bun src/cli/main.ts gauntlet",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"check": "node scripts/build-model-index.mjs --check && tsc --noEmit && bun test",
|
|
42
|
+
"smoke": "bun src/cli/main.ts smoke-tui --sextant",
|
|
43
|
+
"publish:site": "bash scripts/publish-site-data.sh",
|
|
44
|
+
"build:model-index": "node scripts/build-model-index.mjs"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@ast-grep/napi": "^0.45.3",
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
|
+
"@zed-industries/agent-client-protocol": "^0.4.5",
|
|
50
|
+
"diff": "^9.0.0",
|
|
51
|
+
"get-east-asian-width": "1.6.0",
|
|
52
|
+
"gpt-tokenizer": "^4.0.0",
|
|
53
|
+
"marked": "18.0.11",
|
|
54
|
+
"tokenlens": "^1.3.1"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@opencode-ai/models": "^0.0.64",
|
|
58
|
+
"@types/bun": "latest",
|
|
59
|
+
"@xterm/headless": "6.0.0",
|
|
60
|
+
"typescript": "^5.9.3"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** The boot gate (#46): the cockpit stays in its reveal animation until every panel the layout asked
|
|
2
|
+
* for has painted CLEAN at least once — then `ready` flips and the surface accepts submits. A panel
|
|
3
|
+
* that keeps faulting can never lock the UI: the cap forces readiness and the toast already names the
|
|
4
|
+
* fault (frame.ts safe()). Future surfaces/versions get the same discipline by constructing one of
|
|
5
|
+
* these and asking it, instead of re-rolling boot timing by hand.
|
|
6
|
+
*
|
|
7
|
+
* Wiring: the frame loop owns the gate → renderFrame's per-panel guard reports clean/fault into it →
|
|
8
|
+
* the loop holds FRAME_MS pacing and swallows submits until `ready`. */
|
|
9
|
+
|
|
10
|
+
import { REVEAL_STEP_MS } from "./frame.ts";
|
|
11
|
+
|
|
12
|
+
/** panels the gate knows about; the loop re-declares the live set every frame (layout-dependent) */
|
|
13
|
+
export const BOOT_PANELS = ["files", "code", "messages", "plan", "usage", "pet"] as const;
|
|
14
|
+
|
|
15
|
+
/** the animation itself: six reveal steps at 90 ms + a beat to settle */
|
|
16
|
+
export const BOOT_ANIM_MS = REVEAL_STEP_MS * 6;
|
|
17
|
+
|
|
18
|
+
/** a faulting panel may delay readiness at most this long — then the gate opens anyway */
|
|
19
|
+
export const BOOT_CAP_MS = 3000;
|
|
20
|
+
|
|
21
|
+
export interface BootGate {
|
|
22
|
+
/** true once every live panel painted clean (and the animation finished) or the cap elapsed */
|
|
23
|
+
readonly ready: boolean;
|
|
24
|
+
/** 0..1 — animation progress for loaders; faults do not stall it (the cap guarantees 1) */
|
|
25
|
+
readonly progress: number;
|
|
26
|
+
/** names of live panels that have not painted clean yet */
|
|
27
|
+
readonly pending: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Boot {
|
|
31
|
+
readonly gate: BootGate;
|
|
32
|
+
/** frame loop → gate: the clock, and which panels this layout actually shows */
|
|
33
|
+
tick(now: number, livePanels: readonly string[]): void;
|
|
34
|
+
/** renderFrame safe() → gate: a panel painted clean / threw */
|
|
35
|
+
clean(panel: string): void;
|
|
36
|
+
fault(panel: string): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createBoot(bootAt: number): Boot {
|
|
40
|
+
const cleanPanels = new Set<string>();
|
|
41
|
+
const faulted = new Set<string>();
|
|
42
|
+
let live: readonly string[] = [...BOOT_PANELS];
|
|
43
|
+
let now = bootAt;
|
|
44
|
+
|
|
45
|
+
const gate: BootGate = {
|
|
46
|
+
get ready(): boolean {
|
|
47
|
+
const animDone = now - bootAt >= BOOT_ANIM_MS;
|
|
48
|
+
if (now - bootAt >= BOOT_CAP_MS) return true;
|
|
49
|
+
if (!animDone) return false;
|
|
50
|
+
return live.every((p) => cleanPanels.has(p) || faulted.has(p));
|
|
51
|
+
},
|
|
52
|
+
get progress(): number {
|
|
53
|
+
return Math.min(1, (now - bootAt) / BOOT_ANIM_MS);
|
|
54
|
+
},
|
|
55
|
+
get pending(): readonly string[] {
|
|
56
|
+
return live.filter((p) => !cleanPanels.has(p) && !faulted.has(p));
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
gate,
|
|
62
|
+
tick(n: number, livePanels: readonly string[]): void {
|
|
63
|
+
now = n;
|
|
64
|
+
live = livePanels;
|
|
65
|
+
},
|
|
66
|
+
clean(panel: string): void {
|
|
67
|
+
cleanPanels.add(panel);
|
|
68
|
+
faulted.delete(panel);
|
|
69
|
+
},
|
|
70
|
+
fault(panel: string): void {
|
|
71
|
+
faulted.add(panel); // faults count toward readiness — the cap + toast carry the story
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
package/src/sextant/frame.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { GridScreen } from "./grid.ts";
|
|
|
9
9
|
import { drawFiles, drawFrame, drawToasts } from "./draw-frame.ts";
|
|
10
10
|
import { drawPlan, drawUsage } from "./draw-plan.ts";
|
|
11
11
|
import { panel } from "./layout.ts";
|
|
12
|
-
import { pruneToasts } from "./model.ts";
|
|
12
|
+
import { pruneToasts, pushToast } from "./model.ts";
|
|
13
13
|
import type { DumpFrame, Layout, LayoutOptions, Rect, ScreenLike, SextantState, Theme } from "./types.ts";
|
|
14
14
|
import { mainPage } from "./draw-tabs.ts";
|
|
15
15
|
|
|
@@ -22,6 +22,9 @@ export interface FrameDeps {
|
|
|
22
22
|
/** overrides per panel; a missing painter draws an empty titled box */
|
|
23
23
|
painters?: Partial<Painters>;
|
|
24
24
|
layoutOpts?: LayoutOptions;
|
|
25
|
+
/** #46 boot gate: the per-panel guard reports clean/fault into it so the surface can hold its
|
|
26
|
+
* reveal until every live panel has painted once. Omit = no gating (tests, dumps). */
|
|
27
|
+
boot?: import("./boot.ts").Boot;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
/** boot reveal (app.js render()): panels appear in 90 ms steps after bootAt */
|
|
@@ -73,20 +76,46 @@ export function renderFrame(scr: ScreenLike, s: SextantState, theme: Theme, now:
|
|
|
73
76
|
const L = (deps.layout ?? layoutFallback)(scr.w, scr.h, deps.layoutOpts ?? { pet: true });
|
|
74
77
|
pruneToasts(s, now);
|
|
75
78
|
const age = now - s.bootAt;
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
// A painter that throws must never take the rest of the frame down with it — a blank panel with a
|
|
80
|
+
// toast beats a half-drawn cockpit. The first failure per panel is recorded on the state so the
|
|
81
|
+
// loop can surface it once instead of spamming every frame.
|
|
82
|
+
// the panels this layout actually shows — the gate waits on exactly these, no more
|
|
83
|
+
deps.boot?.tick(now, [
|
|
84
|
+
...(L.files ? ["files"] : []),
|
|
85
|
+
"code",
|
|
86
|
+
"messages",
|
|
87
|
+
...(L.plan ? ["plan"] : []),
|
|
88
|
+
...(L.usage ? ["usage"] : []),
|
|
89
|
+
...(L.pet ? ["pet"] : []),
|
|
90
|
+
]);
|
|
91
|
+
const safe = (name: keyof Painters, fn: () => void): void => {
|
|
92
|
+
try {
|
|
93
|
+
fn();
|
|
94
|
+
if (s.painterError === name || s.painterError?.startsWith(name + ":")) s.painterError = undefined;
|
|
95
|
+
deps.boot?.clean(name);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
const msg = `${name}: ${e instanceof Error ? e.message : String(e)}`;
|
|
98
|
+
deps.boot?.fault(name);
|
|
99
|
+
if (s.painterError !== msg) {
|
|
100
|
+
s.painterError = msg;
|
|
101
|
+
pushToast(s, `panel fault — ${msg}`, now, "error");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
safe("frame", () => P.frame(scr, L.frame, s, theme, now));
|
|
106
|
+
if (L.files && age >= 0) safe("files", () => P.files(scr, L.files!, s, theme, now));
|
|
78
107
|
// paging (draw-tabs.ts): on a narrow terminal the main slot shows the panel `s.page` names when
|
|
79
108
|
// the layout hid it; the tab strip itself is painted by the frame loop after the panels
|
|
80
109
|
const main = mainPage(L, s);
|
|
81
110
|
if (age >= REVEAL_STEP_MS) {
|
|
82
|
-
if (main === "files") P.files(scr, L.code, s, theme, now);
|
|
83
|
-
else if (main === "plan") P.plan(scr, L.code, s, theme, now);
|
|
84
|
-
else P.code(scr, L.code, s, theme, now);
|
|
111
|
+
if (main === "files") safe("files", () => P.files(scr, L.code, s, theme, now));
|
|
112
|
+
else if (main === "plan") safe("plan", () => P.plan(scr, L.code, s, theme, now));
|
|
113
|
+
else safe("code", () => P.code(scr, L.code, s, theme, now));
|
|
85
114
|
}
|
|
86
|
-
if (age >= REVEAL_STEP_MS * 2) P.messages(scr, L.messages, s, theme, now);
|
|
87
|
-
if (L.plan && age >= REVEAL_STEP_MS * 3) P.plan(scr, L.plan
|
|
88
|
-
if (L.usage && age >= REVEAL_STEP_MS * 4) P.usage(scr, L.usage
|
|
89
|
-
if (L.pet && age >= REVEAL_STEP_MS * 5) P.pet(scr, L.pet
|
|
115
|
+
if (age >= REVEAL_STEP_MS * 2) safe("messages", () => P.messages(scr, L.messages, s, theme, now));
|
|
116
|
+
if (L.plan && age >= REVEAL_STEP_MS * 3) safe("plan", () => P.plan(scr, L.plan!, s, theme, now));
|
|
117
|
+
if (L.usage && age >= REVEAL_STEP_MS * 4) safe("usage", () => P.usage(scr, L.usage!, s, theme, now));
|
|
118
|
+
if (L.pet && age >= REVEAL_STEP_MS * 5) safe("pet", () => P.pet(scr, L.pet!, s, theme, now));
|
|
90
119
|
drawToasts(scr, s, theme, now);
|
|
91
120
|
return L;
|
|
92
121
|
}
|
|
@@ -12,10 +12,11 @@ import { drawMessages, messagesScroll, promptCursor } from "./draw-messages.ts";
|
|
|
12
12
|
import { drawPet, SWAY_MS } from "./draw-pet.ts";
|
|
13
13
|
import { fuzzy, tokenize } from "./engine.ts";
|
|
14
14
|
import { renderFrame } from "./frame.ts";
|
|
15
|
+
import { createBoot, type Boot } from "./boot.ts";
|
|
15
16
|
import { parseInput } from "./input.ts";
|
|
16
17
|
import { handleInput, type KeyCtx } from "./keys.ts";
|
|
17
18
|
import { layout as layoutFn } from "./layout.ts";
|
|
18
|
-
import { treeRows } from "./model.ts";
|
|
19
|
+
import { treeRows, pushToast } from "./model.ts";
|
|
19
20
|
import { drawHelp, drawPalette, drawSuggest, suggestions } from "./overlays.ts";
|
|
20
21
|
import { drawMarket } from "./draw-market.ts";
|
|
21
22
|
import { drawContext } from "./draw-context.ts";
|
|
@@ -95,6 +96,11 @@ export class FrameLoop {
|
|
|
95
96
|
private rows: TreeRow[] = [];
|
|
96
97
|
/** frames painted (tests: "a render happened") */
|
|
97
98
|
frames = 0;
|
|
99
|
+
/** #46: the cockpit opens for submits once every live panel painted clean (boot.ts) */
|
|
100
|
+
private boot: Boot | null = null;
|
|
101
|
+
private bootToastAt = 0;
|
|
102
|
+
/** Enter presses caught while the boot gate was closed; replayed in order when it opens */
|
|
103
|
+
private bootQueue: InputEvent[] = [];
|
|
98
104
|
|
|
99
105
|
constructor(private readonly d: FrameLoopDeps) {
|
|
100
106
|
const { cols, rows } = d.io.size();
|
|
@@ -191,6 +197,16 @@ export class FrameLoop {
|
|
|
191
197
|
dispatch(ev: InputEvent): void {
|
|
192
198
|
const now = this.d.clock();
|
|
193
199
|
this.markDirty(); // and wake: the tick that follows this key (onTick: file reload) must come in one frame, not one sleep
|
|
200
|
+
// #46: while the boot reveal runs, Enter would submit into a half-loaded surface — QUEUE it and
|
|
201
|
+
// replay once the gate opens (typing still buffers into the editor live). Nothing is ever lost.
|
|
202
|
+
if (this.boot && !this.boot.gate.ready && ev.type === "key" && ev.name === "enter") {
|
|
203
|
+
this.bootQueue.push(ev);
|
|
204
|
+
if (now - this.bootToastAt > 2000) {
|
|
205
|
+
this.bootToastAt = now;
|
|
206
|
+
pushToast(this.d.state, "cockpit is loading — your input is queued", now, "info");
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
194
210
|
if (this.d.beforeInput?.(ev, now)) return;
|
|
195
211
|
const kc = this.d.keyCtx();
|
|
196
212
|
const ctx: KeyCtx = { layout: this.L, hooks: kc.hooks, local: kc.local, hits: this.hits, rows: this.rows, fuzzy, drag: this.drag };
|
|
@@ -202,6 +218,8 @@ export class FrameLoop {
|
|
|
202
218
|
* storms and effects, the boot reveal. These hold the loop at FRAME_MS. */
|
|
203
219
|
moving(now: number): boolean {
|
|
204
220
|
const s = this.d.state, P = this.d.pet.state;
|
|
221
|
+
// #46: the gate holds the loop hot until the surface is truly ready (not just REVEAL_MS elapsed)
|
|
222
|
+
if (this.boot && !this.boot.gate.ready) return true;
|
|
205
223
|
return s.running || s.card !== null || s.files.touched.size > 0 || now - s.bootAt < REVEAL_MS
|
|
206
224
|
|| now < P.stormUntil || P.fx.some((f) => f.until > now);
|
|
207
225
|
}
|
|
@@ -239,17 +257,36 @@ export class FrameLoop {
|
|
|
239
257
|
return Math.min(IDLE_TICK_MS, Math.max(FRAME_MS, this.ambientDue - now));
|
|
240
258
|
}
|
|
241
259
|
|
|
242
|
-
/** paint one frame: panels (renderFrame) → suggestion box → palette → help → scroll write-backs → cursor → flush
|
|
260
|
+
/** paint one frame: panels (renderFrame) → suggestion box → palette → help → scroll write-backs → cursor → flush.
|
|
261
|
+
* The whole body is guarded: a fault anywhere (huge home-dir file tree, odd git state, a painter
|
|
262
|
+
* edge case) must surface as a toast and leave the LAST good frame on screen — never a blank cockpit. */
|
|
243
263
|
render(now: number): void {
|
|
264
|
+
try {
|
|
265
|
+
this.renderInner(now);
|
|
266
|
+
this.lastFrameError = undefined;
|
|
267
|
+
} catch (e) {
|
|
268
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
269
|
+
if (this.lastFrameError !== msg) {
|
|
270
|
+
this.lastFrameError = msg;
|
|
271
|
+
try { pushToast(this.d.state, `frame fault — ${msg}`, now, "error"); } catch { /* state itself is sick */ }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private lastFrameError: string | undefined;
|
|
277
|
+
|
|
278
|
+
private renderInner(now: number): void {
|
|
244
279
|
const scr = this.screen;
|
|
245
280
|
if (!scr) return;
|
|
246
281
|
const s = this.d.state, theme = this.d.theme(), pet = this.d.pet;
|
|
247
282
|
scr.begin(theme.bg);
|
|
248
283
|
this.rows = treeRows(s, now); // the same (version, now) key drawFiles uses → one build per frame (model.ts cache)
|
|
249
284
|
const hits: HitZone[] = [];
|
|
285
|
+
if (!this.boot) this.boot = createBoot(s.bootAt);
|
|
250
286
|
const L = renderFrame(scr, s, theme, now, {
|
|
251
287
|
layout: layoutFn,
|
|
252
288
|
layoutOpts: this.layoutOpts(),
|
|
289
|
+
boot: this.boot,
|
|
253
290
|
painters: {
|
|
254
291
|
code: (g, r, st, th, t) => drawCode(g, r, st, th, t, { tokenize }),
|
|
255
292
|
messages: drawMessages,
|
|
@@ -257,6 +294,12 @@ export class FrameLoop {
|
|
|
257
294
|
},
|
|
258
295
|
});
|
|
259
296
|
this.L = L;
|
|
297
|
+
// the gate just opened → flush queued submits through the normal path
|
|
298
|
+
if (this.boot.gate.ready && this.bootQueue.length > 0) {
|
|
299
|
+
const queued = this.bootQueue;
|
|
300
|
+
this.bootQueue = [];
|
|
301
|
+
for (const ev of queued) this.dispatch(ev);
|
|
302
|
+
}
|
|
260
303
|
if (L.pet) hits.push({ rect: L.pet, onClick: () => pet.poke(this.d.clock()) });
|
|
261
304
|
// the frame's border rows (frame-hits.ts): unread badge → notices, theme name → next theme, effort → /effort
|
|
262
305
|
for (const z of frameHits(L.frame, s, theme, now)) hits.push(z);
|
package/src/sextant/types.ts
CHANGED
|
@@ -388,6 +388,9 @@ export interface SextantState {
|
|
|
388
388
|
context: ContextState | null;
|
|
389
389
|
help: boolean;
|
|
390
390
|
toasts: Toast[];
|
|
391
|
+
/** last painter failure seen by renderFrame's per-panel guard (frame.ts); surfaced as a toast by
|
|
392
|
+
* the frame loop, cleared on the next clean frame of that panel. Undefined = all panels clean. */
|
|
393
|
+
painterError?: string;
|
|
391
394
|
/** the notification history behind the toasts (Notice); newest last */
|
|
392
395
|
notices: Notice[];
|
|
393
396
|
/** images staged for the next user message (names), shown as chips above the prompt; synced from
|