rovecode 0.4.1 → 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 +1 -1
- package/src/sextant/boot.ts +74 -0
- package/src/sextant/frame.ts +18 -1
- package/src/sextant/sextant-frame-loop.ts +26 -0
package/package.json
CHANGED
|
@@ -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
|
@@ -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 */
|
|
@@ -76,9 +79,23 @@ export function renderFrame(scr: ScreenLike, s: SextantState, theme: Theme, now:
|
|
|
76
79
|
// A painter that throws must never take the rest of the frame down with it — a blank panel with a
|
|
77
80
|
// toast beats a half-drawn cockpit. The first failure per panel is recorded on the state so the
|
|
78
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
|
+
]);
|
|
79
91
|
const safe = (name: keyof Painters, fn: () => void): void => {
|
|
80
|
-
try {
|
|
92
|
+
try {
|
|
93
|
+
fn();
|
|
94
|
+
if (s.painterError === name || s.painterError?.startsWith(name + ":")) s.painterError = undefined;
|
|
95
|
+
deps.boot?.clean(name);
|
|
96
|
+
} catch (e) {
|
|
81
97
|
const msg = `${name}: ${e instanceof Error ? e.message : String(e)}`;
|
|
98
|
+
deps.boot?.fault(name);
|
|
82
99
|
if (s.painterError !== msg) {
|
|
83
100
|
s.painterError = msg;
|
|
84
101
|
pushToast(s, `panel fault — ${msg}`, now, "error");
|
|
@@ -12,6 +12,7 @@ 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";
|
|
@@ -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
|
}
|
|
@@ -264,9 +282,11 @@ export class FrameLoop {
|
|
|
264
282
|
scr.begin(theme.bg);
|
|
265
283
|
this.rows = treeRows(s, now); // the same (version, now) key drawFiles uses → one build per frame (model.ts cache)
|
|
266
284
|
const hits: HitZone[] = [];
|
|
285
|
+
if (!this.boot) this.boot = createBoot(s.bootAt);
|
|
267
286
|
const L = renderFrame(scr, s, theme, now, {
|
|
268
287
|
layout: layoutFn,
|
|
269
288
|
layoutOpts: this.layoutOpts(),
|
|
289
|
+
boot: this.boot,
|
|
270
290
|
painters: {
|
|
271
291
|
code: (g, r, st, th, t) => drawCode(g, r, st, th, t, { tokenize }),
|
|
272
292
|
messages: drawMessages,
|
|
@@ -274,6 +294,12 @@ export class FrameLoop {
|
|
|
274
294
|
},
|
|
275
295
|
});
|
|
276
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
|
+
}
|
|
277
303
|
if (L.pet) hits.push({ rect: L.pet, onClick: () => pet.poke(this.d.clock()) });
|
|
278
304
|
// the frame's border rows (frame-hits.ts): unread badge → notices, theme name → next theme, effort → /effort
|
|
279
305
|
for (const z of frameHits(L.frame, s, theme, now)) hits.push(z);
|