@pi-archimedes/ui 2.10.3 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/package.json +8 -8
- package/src/bash/index.ts +0 -2
- package/src/bash/renderer.test.ts +50 -5
- package/src/bash/renderer.ts +28 -30
- package/src/codemode/index.ts +0 -9
- package/src/codemode/renderer.test.ts +32 -31
- package/src/codemode/renderer.ts +29 -61
- package/src/config.test.ts +62 -1
- package/src/config.ts +45 -1
- package/src/editor/index.test.ts +118 -2
- package/src/editor/index.ts +34 -3
- package/src/editor/spin-quips.ts +1 -1
- package/src/index.screen-mode.test.ts +411 -0
- package/src/index.test.ts +506 -101
- package/src/index.ts +252 -104
- package/src/settings.test.ts +37 -2
- package/src/settings.ts +16 -3
- package/src/startup/index.test.ts +239 -129
- package/src/startup/index.ts +179 -272
- package/src/startup/pi-listing.test.ts +290 -0
- package/src/startup/sections.test.ts +38 -208
- package/src/startup/sections.ts +83 -236
- package/src/startup-splash-gate.test.ts +546 -0
- package/src/thinking/patch.test.ts +81 -1
- package/src/thinking/patch.ts +22 -6
- package/src/bash/tool.test.ts +0 -26
- package/src/bash/tool.ts +0 -17
- package/src/codemode/loader.test.ts +0 -139
- package/src/codemode/loader.ts +0 -115
- package/src/codemode/tool.test.ts +0 -149
- package/src/codemode/tool.ts +0 -96
- package/src/startup/capture.ts +0 -47
- package/src/startup/version.test.ts +0 -98
- package/src/startup/version.ts +0 -26
package/src/startup/index.ts
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
|
-
import { VERSION, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { getShinedLogo,
|
|
1
|
+
import { VERSION, type ScopedModel, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { getShinedLogo, LOGO_SETTLE_FRAME } from "./logo.js";
|
|
3
3
|
import { loadUIConfig } from "../config.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
4
|
+
import {
|
|
5
|
+
formatColumns,
|
|
6
|
+
RAMP_FRAMES,
|
|
7
|
+
MAX_STAGGER,
|
|
8
|
+
type ParsedSection,
|
|
9
|
+
} from "./sections.js";
|
|
10
|
+
import { visibleWidth, type TUI } from "@earendil-works/pi-tui";
|
|
11
|
+
|
|
12
|
+
// ── What this module owns ───────────────────────────────────────────────────
|
|
13
|
+
//
|
|
14
|
+
// The splash: the logo animation, the version row, and the text reveal that
|
|
15
|
+
// carries them in. That is all.
|
|
16
|
+
//
|
|
17
|
+
// It used to own pi's startup listing too — finding pi's resource container by
|
|
18
|
+
// child-order heuristics, intercepting `addChild`, swallowing pi's section
|
|
19
|
+
// components and re-rendering them here, plus a `console.log` patch that ate
|
|
20
|
+
// pi's `Model scope:` line. All of that is gone (pi 1.x alignment, Task 8): pi
|
|
21
|
+
// renders its own listing, `quietStartup` decides whether it shows, and
|
|
22
|
+
// `ctx.scopedModels` hands us the model scope directly, so there is nothing
|
|
23
|
+
// left to scrape and no reason to touch the console or pi's component tree.
|
|
24
|
+
// `ui/src/index.ts` decides *whether* to install this splash (quietStartup) and
|
|
25
|
+
// what sections to reveal (the model scope); this module animates them.
|
|
25
26
|
|
|
26
27
|
export interface ListingRef {
|
|
27
28
|
sections: ParsedSection[];
|
|
@@ -29,48 +30,65 @@ export interface ListingRef {
|
|
|
29
30
|
revealed: boolean;
|
|
30
31
|
revealedAt: number;
|
|
31
32
|
scaffoldAt: number;
|
|
32
|
-
latestVersion?: string;
|
|
33
33
|
settled: boolean;
|
|
34
34
|
cachedLines?: string[];
|
|
35
35
|
cachedWidth?: number;
|
|
36
|
-
cachedHeight?: number;
|
|
37
|
-
maxHeaderHeight?: number;
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
// ── renderHeader() — composes logo +
|
|
38
|
+
// ── renderHeader() — composes logo + listing ───────────────────────────
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The splash's own text rows, drawn in pi's shape (`formatColumns`), under the
|
|
42
|
+
* animated logo. Everything the header is, and nothing else.
|
|
43
|
+
*
|
|
44
|
+
* The block is exactly its content on both screens, and that is deliberate
|
|
45
|
+
* twice over:
|
|
46
|
+
*
|
|
47
|
+
* - a frame-bounded (fullscreen) screen allocates the height itself (ADR 0027),
|
|
48
|
+
* so a header padded to the terminal would be the whole viewport and the
|
|
49
|
+
* editor would get nothing — the reason #97/#102 sized it to content;
|
|
50
|
+
* - on regular screen the padding was the defect the maintainer pointed at.
|
|
51
|
+
* pi's header container puts a `Spacer(1)` above and below whatever header is
|
|
52
|
+
* installed (`interactive-mode.js:764-766`) and its resource listing opens
|
|
53
|
+
* with one more, so a header padded to `rows - 3` pushed pi's own listing a
|
|
54
|
+
* screen down and left the two halves of the intro reading as two documents.
|
|
55
|
+
*
|
|
56
|
+
* Nothing here is sized by the screen any more, which is also why the header
|
|
57
|
+
* factory no longer asks the renderer which screen it is on: there is no
|
|
58
|
+
* per-screen measurement left to take and nothing per-screen left to go stale.
|
|
59
|
+
*/
|
|
60
|
+
export function renderHeader(theme: Theme, ref: ListingRef, width: number): string[] {
|
|
45
61
|
const cfg = loadUIConfig();
|
|
46
62
|
const logoLines = getShinedLogo(ref.frame, cfg.animationStyle, cfg.colorfulLogo);
|
|
47
63
|
|
|
48
64
|
// Use cached text lines if settled (no more animations)
|
|
49
65
|
let listingLines: string[];
|
|
50
66
|
|
|
51
|
-
if (ref.settled && ref.cachedLines && ref.cachedWidth === width
|
|
67
|
+
if (ref.settled && ref.cachedLines && ref.cachedWidth === width) {
|
|
52
68
|
listingLines = ref.cachedLines;
|
|
53
69
|
} else {
|
|
54
|
-
const sectionsToRender:
|
|
70
|
+
const sectionsToRender: ParsedSection[] = [];
|
|
55
71
|
|
|
56
72
|
if (!ref.revealed) {
|
|
57
73
|
// Logo only — sections appear together on reveal
|
|
58
74
|
} else {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
// The version row is ours to state; pi owns its own update check
|
|
76
|
+
// (`--update`, `/changelog`, `PI_SKIP_VERSION_CHECK`), so there is no
|
|
77
|
+
// second `Latest:` here — that used to arrive from a registry fetch on
|
|
78
|
+
// every start.
|
|
79
|
+
sectionsToRender.push({ name: "Version", items: [`v${VERSION}`] });
|
|
80
|
+
|
|
81
|
+
// Then whatever the caller revealed in. pi decides which of *its*
|
|
82
|
+
// sections exist; we only ever carry the model scope. Empty sections are
|
|
83
|
+
// "nothing loaded", skip them.
|
|
84
|
+
for (const sec of ref.sections) {
|
|
85
|
+
if (sec.items.length > 0) sectionsToRender.push(sec);
|
|
69
86
|
}
|
|
70
87
|
}
|
|
71
88
|
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
// pi writes its listing rows across the frame, so the rows break at the
|
|
90
|
+
// frame edge rather than at some narrower column of ours.
|
|
91
|
+
listingLines = formatColumns(sectionsToRender, theme, width, ref);
|
|
74
92
|
|
|
75
93
|
// Cache once text animations are done
|
|
76
94
|
const textAge = ref.revealed ? ref.frame - ref.revealedAt : 0;
|
|
@@ -81,141 +99,121 @@ export function renderHeader(theme: Theme, ref: ListingRef, width: number, heigh
|
|
|
81
99
|
ref.settled = true;
|
|
82
100
|
ref.cachedLines = listingLines;
|
|
83
101
|
ref.cachedWidth = width;
|
|
84
|
-
ref.cachedHeight = height;
|
|
85
102
|
}
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
// Build content: logo + separator + listing
|
|
89
106
|
const contentLines: string[] = [];
|
|
90
107
|
|
|
91
|
-
// Logo
|
|
92
|
-
const listingMaxW = Math.floor(width * 0.7);
|
|
93
|
-
const listingLeftPad = Math.floor((width - listingMaxW) / 2);
|
|
108
|
+
// Logo centred across the frame — the top half of the intro, unchanged.
|
|
94
109
|
for (const logoRow of logoLines) {
|
|
95
|
-
const pad = Math.floor((
|
|
96
|
-
contentLines.push(" ".repeat(
|
|
110
|
+
const pad = Math.max(0, Math.floor((width - visibleWidth(logoRow)) / 2));
|
|
111
|
+
contentLines.push(" ".repeat(pad) + logoRow);
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
// Separator
|
|
100
115
|
contentLines.push("");
|
|
101
116
|
|
|
102
|
-
// Listing
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Pad top and bottom to fill height (+5 top bias for visual centering)
|
|
108
|
-
const VERTICAL_CENTER_BIAS = 5; // Shift content slightly upward for visual balance
|
|
109
|
-
const contentHeight = contentLines.length;
|
|
110
|
-
const remaining = Math.max(0, height - contentHeight);
|
|
111
|
-
const topPad = Math.floor(remaining / 2) + VERTICAL_CENTER_BIAS;
|
|
112
|
-
const bottomPad = remaining - topPad;
|
|
113
|
-
|
|
114
|
-
const result: string[] = [];
|
|
115
|
-
for (let i = 0; i < topPad; i++) result.push("");
|
|
116
|
-
for (const line of contentLines) result.push(line);
|
|
117
|
-
for (let i = 0; i < bottomPad; i++) result.push("");
|
|
117
|
+
// Listing, at column 1: pi's rows start there, and anything indenting ours
|
|
118
|
+
// past it is what made the two halves look unrelated.
|
|
119
|
+
for (const listRow of listingLines) contentLines.push(listRow);
|
|
118
120
|
|
|
119
|
-
return
|
|
121
|
+
return contentLines;
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
// ──
|
|
124
|
+
// ── The section the splash is allowed to know about ───────────────────────
|
|
123
125
|
|
|
124
126
|
/**
|
|
125
|
-
*
|
|
127
|
+
* The session's model scope, in the shape the splash renders — the same set
|
|
128
|
+
* `ctx.scopedModels` reports and `/scoped-models` lists, formatted the way pi's
|
|
129
|
+
* own `Model scope:` line does (`id`, plus `:thinkingLevel` when the pattern
|
|
130
|
+
* carried one).
|
|
126
131
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
132
|
+
* The alternative it replaces was a `console.log` patch watching for pi to
|
|
133
|
+
* print that line, which both hid the line (the patch consumed it) and had no
|
|
134
|
+
* answer for a session where pi prints nothing.
|
|
135
|
+
*
|
|
136
|
+
* Returns `undefined` for no scope, so the section is simply absent: an empty
|
|
137
|
+
* `[Models]` header is pi's "nothing scoped" case, and there pi offers every
|
|
138
|
+
* model. A host without the property answers the same way.
|
|
131
139
|
*/
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
export function modelScopeSection(
|
|
141
|
+
scopedModels: readonly ScopedModel[] | undefined,
|
|
142
|
+
): ParsedSection | undefined {
|
|
143
|
+
if (!Array.isArray(scopedModels)) return undefined;
|
|
144
|
+
|
|
145
|
+
const items: string[] = [];
|
|
146
|
+
for (const scoped of scopedModels) {
|
|
147
|
+
const id = scoped?.model?.id;
|
|
148
|
+
if (typeof id !== "string" || id === "") continue;
|
|
149
|
+
items.push(scoped.thinkingLevel ? `${id}:${scoped.thinkingLevel}` : id);
|
|
150
|
+
}
|
|
151
|
+
return items.length > 0 ? { name: "Models", items } : undefined;
|
|
136
152
|
}
|
|
137
153
|
|
|
138
|
-
// ──
|
|
154
|
+
// ── The animation ───────────────────────────────────────────────────────────
|
|
139
155
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Sections wait this long after the header is built before they appear, so the
|
|
158
|
+
* logo gets its reveal frames first. The old value, kept: it is the interval
|
|
159
|
+
* pi's own section components used to arrive inside, and the splash looks the
|
|
160
|
+
* same either way.
|
|
161
|
+
*/
|
|
162
|
+
const REVEAL_DEBOUNCE_MS = 150;
|
|
163
|
+
const FRAME_INTERVAL_MS = 16;
|
|
164
|
+
|
|
165
|
+
// One splash is on screen at a time. pi disposes the previous header before it
|
|
166
|
+
// builds the next one (`setExtensionHeader()` — a new session, `/reload`, or any
|
|
167
|
+
// `setHeader()` call), so a module pair of timers cannot leak a second
|
|
168
|
+
// animation — and holds no renderer: each timer closes over the `tui` it was
|
|
169
|
+
// started with, and the one running is the one the current header factory was
|
|
170
|
+
// handed (ADR 0027).
|
|
171
|
+
let frameTimer: ReturnType<typeof setInterval> | undefined;
|
|
172
|
+
let revealTimer: ReturnType<typeof setTimeout> | undefined;
|
|
173
|
+
|
|
174
|
+
/** Stop both halves of the animation (header disposed, session ended). */
|
|
175
|
+
export function stopStartupListing(): void {
|
|
176
|
+
if (frameTimer) {
|
|
177
|
+
clearInterval(frameTimer);
|
|
178
|
+
frameTimer = undefined;
|
|
156
179
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const middle = tui.children[1];
|
|
161
|
-
if (middle instanceof Container) return middle;
|
|
180
|
+
if (revealTimer) {
|
|
181
|
+
clearTimeout(revealTimer);
|
|
182
|
+
revealTimer = undefined;
|
|
162
183
|
}
|
|
163
|
-
|
|
164
|
-
// Strategy 3: Find the Container with the most children (chat area)
|
|
165
|
-
const containers = tui.children.filter((c): c is Container => c instanceof Container);
|
|
166
|
-
if (containers.length > 1) {
|
|
167
|
-
const largest = containers.reduce((best, c) =>
|
|
168
|
-
c.children.length > best.children.length ? c : best,
|
|
169
|
-
);
|
|
170
|
-
if (largest.children.length > 0) return largest;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Strategy 4: First Container
|
|
174
|
-
if (containers.length > 0) return containers[0];
|
|
175
|
-
|
|
176
|
-
return undefined;
|
|
177
184
|
}
|
|
178
185
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const containerChildren = docContainer.children.filter(
|
|
197
|
-
(c): c is Container => c instanceof Container
|
|
198
|
-
);
|
|
199
|
-
// Pi 0.84+: [headerContainer(0), loadedResourcesContainer(1), chatContainer(2)]
|
|
200
|
-
// The index [1] assumption is load-bearing — tied to Pi 0.84+ initialization order.
|
|
201
|
-
if (containerChildren.length < 2) return undefined;
|
|
202
|
-
return containerChildren[1]; // loadedResourcesContainer
|
|
186
|
+
function startFrameTimer(tui: TUI, ref: ListingRef): void {
|
|
187
|
+
if (frameTimer) clearInterval(frameTimer);
|
|
188
|
+
frameTimer = setInterval(() => {
|
|
189
|
+
ref.frame++;
|
|
190
|
+
// Logo done and either the listing settled, or nothing is revealed and
|
|
191
|
+
// none is on its way — the state a quiet start leaves behind. Idle frames
|
|
192
|
+
// cost a full viewport redraw on fullscreen screen, so stop here rather
|
|
193
|
+
// than repaint forever (#92).
|
|
194
|
+
const nothingToAnimate = ref.settled || (!ref.revealed && !revealTimer);
|
|
195
|
+
if (nothingToAnimate && ref.frame >= LOGO_SETTLE_FRAME) {
|
|
196
|
+
clearInterval(frameTimer);
|
|
197
|
+
frameTimer = undefined;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
tui.requestRender();
|
|
201
|
+
}, FRAME_INTERVAL_MS);
|
|
203
202
|
}
|
|
204
203
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
rc[LISTING_REF] = ref;
|
|
204
|
+
/**
|
|
205
|
+
* Install the splash for the header we were just handed: reset the animation
|
|
206
|
+
* ref and start the frame timer. Re-installing (a new session, `/reload`, or
|
|
207
|
+
* any other `setHeader()` replacement, each of which pi disposes the old header
|
|
208
|
+
* before it calls the factory) restarts the animation on the renderer now in
|
|
209
|
+
* hand and stops the previous one — a stale renderer must not keep being asked
|
|
210
|
+
* for frames.
|
|
211
|
+
*
|
|
212
|
+
* Sections are not passed here: call {@link revealStartupListing} with them
|
|
213
|
+
* (or not at all, which leaves the splash as logo + version).
|
|
214
|
+
*/
|
|
215
|
+
export function patchStartupListing(tui: TUI, ref: ListingRef): void {
|
|
216
|
+
stopStartupListing();
|
|
219
217
|
ref.frame = 0;
|
|
220
218
|
ref.revealed = false;
|
|
221
219
|
ref.revealedAt = 0;
|
|
@@ -223,127 +221,36 @@ export function patchStartupListing(
|
|
|
223
221
|
ref.settled = false;
|
|
224
222
|
delete ref.cachedLines;
|
|
225
223
|
delete ref.cachedWidth;
|
|
226
|
-
delete ref.maxHeaderHeight;
|
|
227
|
-
|
|
228
|
-
if (rc[ANIM_INTERVAL]) clearInterval(rc[ANIM_INTERVAL]);
|
|
229
|
-
if (rc[DEBOUNCE_TIMER]) clearTimeout(rc[DEBOUNCE_TIMER]);
|
|
230
|
-
|
|
231
|
-
const interval = setInterval(() => {
|
|
232
|
-
try {
|
|
233
|
-
const current: ListingRef = rc[LISTING_REF];
|
|
234
|
-
if (!current) {
|
|
235
|
-
clearInterval(interval);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
current.frame++;
|
|
239
|
-
if (current.settled && current.frame >= LOGO_SETTLE_FRAME) {
|
|
240
|
-
clearInterval(interval);
|
|
241
|
-
rc[ANIM_INTERVAL] = null;
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
tui.requestRender();
|
|
245
|
-
} catch {
|
|
246
|
-
clearInterval(interval);
|
|
247
|
-
}
|
|
248
|
-
}, 16);
|
|
249
|
-
|
|
250
|
-
rc[ANIM_INTERVAL] = interval;
|
|
251
|
-
|
|
252
|
-
// Fetch latest version from npm
|
|
253
|
-
fetchLatestVersion().then(v => {
|
|
254
|
-
if (v) {
|
|
255
|
-
const current: ListingRef = rc[LISTING_REF];
|
|
256
|
-
current.latestVersion = v;
|
|
257
|
-
// Invalidate cache so version updates on next render
|
|
258
|
-
delete current.cachedLines;
|
|
259
|
-
current.settled = false;
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Patch clear() to intercept container rebuild
|
|
264
|
-
if (!rc[PATCHED_CLEAR]) {
|
|
265
|
-
rc[PATCHED_CLEAR] = true;
|
|
266
|
-
const origClear = resourcesContainer.clear.bind(resourcesContainer);
|
|
267
|
-
resourcesContainer.clear = () => {
|
|
268
|
-
return origClear();
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Only patch addChild once — the closure reads rc[LISTING_REF] dynamically
|
|
273
|
-
if (rc[PATCHED_LISTING]) {
|
|
274
|
-
resourcesContainer.clear();
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
rc[PATCHED_LISTING] = true;
|
|
278
|
-
|
|
279
|
-
const origAddChild = resourcesContainer.addChild.bind(resourcesContainer);
|
|
280
|
-
rc[ORIG_ADD_CHILD] = origAddChild; // Store for cleanup on shutdown
|
|
281
|
-
resourcesContainer.clear();
|
|
282
|
-
|
|
283
|
-
resourcesContainer.addChild = (component: Component) => {
|
|
284
|
-
try {
|
|
285
|
-
const currentRef: ListingRef = rc[LISTING_REF];
|
|
286
224
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
// is a lossy comma-joined base-name list. Parse the expanded text so
|
|
290
|
-
// Extensions keep real names instead of "index.ts/index.js/index".
|
|
291
|
-
// pi ≥0.99 moved the text builders into the constructor (no instance
|
|
292
|
-
// getExpandedText()) — expandedText() forces expansion via setExpanded().
|
|
293
|
-
const getExpanded = (component as any).getExpandedText;
|
|
294
|
-
const plain = typeof getExpanded === "function"
|
|
295
|
-
? stripAnsi(getExpanded.call(component))
|
|
296
|
-
: expandedText(component);
|
|
297
|
-
|
|
298
|
-
const section = parseSectionText(plain) ?? parseModelScope(plain);
|
|
299
|
-
if (section) {
|
|
300
|
-
const existing = currentRef.sections.find(s => s.name === section.name);
|
|
301
|
-
if (existing) {
|
|
302
|
-
existing.items = [...new Set([...existing.items, ...section.items])];
|
|
303
|
-
} else {
|
|
304
|
-
currentRef.sections.push(section);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Invalidate cache so late-arriving sections show up
|
|
308
|
-
currentRef.settled = false;
|
|
309
|
-
delete currentRef.cachedLines;
|
|
310
|
-
|
|
311
|
-
if (currentRef.revealed) {
|
|
312
|
-
// Already revealed — show new section immediately
|
|
313
|
-
tui.requestRender();
|
|
314
|
-
} else {
|
|
315
|
-
// Batch initial sections — reset debounce on each arrival
|
|
316
|
-
if (rc[DEBOUNCE_TIMER]) clearTimeout(rc[DEBOUNCE_TIMER]);
|
|
317
|
-
rc[DEBOUNCE_TIMER] = setTimeout(() => {
|
|
318
|
-
const ref: ListingRef = rc[LISTING_REF];
|
|
319
|
-
ref.revealed = true;
|
|
320
|
-
ref.revealedAt = ref.frame;
|
|
321
|
-
ref.scaffoldAt = ref.frame;
|
|
322
|
-
tui.requestRender();
|
|
323
|
-
rc[DEBOUNCE_TIMER] = null;
|
|
324
|
-
}, REVEAL_DEBOUNCE_MS);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (
|
|
331
|
-
plain.includes("Listing all available commands") ||
|
|
332
|
-
plain.includes("(Source: extension)") ||
|
|
333
|
-
plain.trim().startsWith("/skill:")
|
|
334
|
-
) {
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
225
|
+
startFrameTimer(tui, ref);
|
|
226
|
+
}
|
|
338
227
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Reveal the splash's text half — the version row always comes with the
|
|
230
|
+
* header, `sections` is whatever the caller decided to show next (see
|
|
231
|
+
* {@link modelScopeSection}).
|
|
232
|
+
*
|
|
233
|
+
* Debounced so the logo keeps its frames, and it restarts the frame timer when
|
|
234
|
+
* the logo already settled before anything arrived: without the restart the
|
|
235
|
+
* text would be revealed into a stopped animation and fade in over frames
|
|
236
|
+
* nobody draws.
|
|
237
|
+
*/
|
|
238
|
+
export function revealStartupListing(
|
|
239
|
+
tui: TUI,
|
|
240
|
+
ref: ListingRef,
|
|
241
|
+
sections: ParsedSection[],
|
|
242
|
+
): void {
|
|
243
|
+
if (revealTimer) clearTimeout(revealTimer);
|
|
244
|
+
revealTimer = setTimeout(() => {
|
|
245
|
+
revealTimer = undefined;
|
|
246
|
+
ref.sections = sections;
|
|
247
|
+
ref.revealed = true;
|
|
248
|
+
ref.revealedAt = ref.frame;
|
|
249
|
+
ref.scaffoldAt = ref.frame;
|
|
250
|
+
ref.settled = false;
|
|
251
|
+
delete ref.cachedLines;
|
|
252
|
+
delete ref.cachedWidth;
|
|
253
|
+
tui.requestRender();
|
|
254
|
+
if (!frameTimer) startFrameTimer(tui, ref);
|
|
255
|
+
}, REVEAL_DEBOUNCE_MS);
|
|
349
256
|
}
|