@pi-archimedes/ui 2.9.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/package.json +44 -0
  4. package/src/bash/index.ts +10 -0
  5. package/src/bash/renderer.test.ts +424 -0
  6. package/src/bash/renderer.ts +301 -0
  7. package/src/bash/tool.test.ts +26 -0
  8. package/src/bash/tool.ts +17 -0
  9. package/src/codemode/index.ts +20 -0
  10. package/src/codemode/loader.test.ts +139 -0
  11. package/src/codemode/loader.ts +115 -0
  12. package/src/codemode/renderer.test.ts +717 -0
  13. package/src/codemode/renderer.ts +399 -0
  14. package/src/codemode/tool.test.ts +149 -0
  15. package/src/codemode/tool.ts +96 -0
  16. package/src/config.test.ts +199 -0
  17. package/src/config.ts +129 -0
  18. package/src/editor/index.test.ts +820 -0
  19. package/src/editor/index.ts +380 -0
  20. package/src/editor/spin-quips.test.ts +106 -0
  21. package/src/editor/spin-quips.ts +44 -0
  22. package/src/editor/spin.test.ts +763 -0
  23. package/src/editor/spin.ts +493 -0
  24. package/src/index.test.ts +402 -0
  25. package/src/index.ts +235 -0
  26. package/src/migration.test.ts +256 -0
  27. package/src/migration.ts +94 -0
  28. package/src/settings.test.ts +122 -0
  29. package/src/settings.ts +135 -0
  30. package/src/startup/capture.ts +47 -0
  31. package/src/startup/index.test.ts +175 -0
  32. package/src/startup/index.ts +348 -0
  33. package/src/startup/logo.test.ts +260 -0
  34. package/src/startup/logo.ts +85 -0
  35. package/src/startup/sections.test.ts +333 -0
  36. package/src/startup/sections.ts +306 -0
  37. package/src/startup/version.test.ts +98 -0
  38. package/src/startup/version.ts +26 -0
  39. package/src/thinking/patch.test.ts +857 -0
  40. package/src/thinking/patch.ts +407 -0
  41. package/src/thinking/theme.test.ts +216 -0
  42. package/src/thinking/theme.ts +156 -0
  43. package/src/thinking/transform.test.ts +89 -0
  44. package/src/thinking/transform.ts +21 -0
  45. package/src/thinking/unindent.test.ts +74 -0
  46. package/src/thinking/unindent.ts +74 -0
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+
3
+ // ── Mocks ────────────────────────────────────────────────────────────────────
4
+
5
+ // pi-tui — Text/Spacer/Container must be real classes for the instanceof checks
6
+ vi.mock("@earendil-works/pi-tui", () => ({
7
+ Text: class Text {
8
+ render(): string[] {
9
+ return ["rendered"];
10
+ }
11
+ },
12
+ Spacer: class Spacer {},
13
+ Container: class Container {
14
+ children: unknown[] = [];
15
+ addChild(c: unknown) {
16
+ this.children.push(c);
17
+ return this;
18
+ }
19
+ clear() {
20
+ this.children = [];
21
+ return this;
22
+ }
23
+ },
24
+ truncateToWidth: (s: string, w: number) => s.slice(0, w),
25
+ visibleWidth: (s: string) => s.length,
26
+ }));
27
+
28
+ // version — no network in tests
29
+ vi.mock("./version.js", () => ({
30
+ fetchLatestVersion: vi.fn().mockResolvedValue(undefined),
31
+ compareVersions: () => 0,
32
+ }));
33
+
34
+ // ── Imports ──────────────────────────────────────────────────────────────────
35
+
36
+ import { Text, Container } from "@earendil-works/pi-tui";
37
+ import { patchStartupListing, type ListingRef } from "./index.js";
38
+
39
+ // ── Test helpers ─────────────────────────────────────────────────────────────
40
+
41
+ function makeTui(): { tui: any; resources: any; header: any } {
42
+ // pi ≥0.84 layout: documentContainer → [headerContainer, loadedResourcesContainer, chatContainer]
43
+ const header = new Container();
44
+ const resources = new Container();
45
+ const chat = new Container();
46
+ const doc = new Container();
47
+ doc.children.push(header, resources, chat);
48
+ const tui = {
49
+ terminal: { rows: 24 },
50
+ children: [doc],
51
+ requestRender: vi.fn(),
52
+ };
53
+ return { tui, resources, header };
54
+ }
55
+
56
+ function makeRef(): ListingRef {
57
+ return {
58
+ sections: [],
59
+ frame: 0,
60
+ revealed: true, // skip the debounce timer path
61
+ revealedAt: 0,
62
+ scaffoldAt: 0,
63
+ settled: false,
64
+ };
65
+ }
66
+
67
+ /** Fake ExpandableText: render() returns whatever the current state yields. */
68
+ function makeSectionComponent(renderLines: string[], opts: { setExpanded?: ReturnType<typeof vi.fn>; getExpandedText?: () => string } = {}): any {
69
+ const comp: any = new (Text as any)();
70
+ comp.render = () => renderLines;
71
+ if (opts.setExpanded) comp.setExpanded = opts.setExpanded;
72
+ if (opts.getExpandedText) comp.getExpandedText = opts.getExpandedText;
73
+ return comp;
74
+ }
75
+
76
+ const THEME = {
77
+ fg: (_token: string, t: string) => t,
78
+ getFgAnsi: (_token: string) => "",
79
+ } as any;
80
+
81
+ let lastResources: any;
82
+ const after = () => {
83
+ // Stop the animation interval + any pending debounce timer
84
+ const ANIM_INTERVAL = Symbol.for("splashscreen:animInterval");
85
+ const DEBOUNCE_TIMER = Symbol.for("splashscreen:debounceTimer");
86
+ if (lastResources?.[ANIM_INTERVAL]) clearInterval(lastResources[ANIM_INTERVAL]);
87
+ if (lastResources?.[DEBOUNCE_TIMER]) clearTimeout(lastResources[DEBOUNCE_TIMER]);
88
+ lastResources = undefined;
89
+ };
90
+ afterEach(after);
91
+
92
+ // ── patchStartupListing: section text extraction ─────────────────────────────
93
+
94
+ describe("patchStartupListing section parsing", () => {
95
+ it("forces expansion via setExpanded when the getExpandedText method is absent (pi ≥0.99)", () => {
96
+ const { tui, resources } = makeTui();
97
+ lastResources = resources;
98
+ const ref = makeRef();
99
+ patchStartupListing(tui, THEME, ref);
100
+
101
+ // pi ≥0.99 ExpandableText: text builders live in the constructor,
102
+ // the component starts collapsed. render() after setExpanded(true)
103
+ // returns the full per-item body.
104
+ const setExpanded = vi.fn();
105
+ const comp = makeSectionComponent(
106
+ ["[Skills]", " /home/u/skills/alpha/SKILL.md", " /home/u/skills/beta/SKILL.md"],
107
+ { setExpanded },
108
+ );
109
+ resources.addChild(comp);
110
+
111
+ expect(setExpanded).toHaveBeenCalledWith(true);
112
+ expect(ref.sections).toEqual([{ name: "Skills", items: ["alpha", "beta"] }]);
113
+ // Section consumed by the splash — original component not added
114
+ expect((resources as any).children.length).toBe(0);
115
+ });
116
+
117
+ it("prefers the getExpandedText method when present (older pi)", () => {
118
+ const { tui, resources } = makeTui();
119
+ lastResources = resources;
120
+ const ref = makeRef();
121
+ patchStartupListing(tui, THEME, ref);
122
+
123
+ const setExpanded = vi.fn();
124
+ const comp = makeSectionComponent(
125
+ ["[Skills]", " collapsed-should-not-be-read"],
126
+ {
127
+ setExpanded,
128
+ getExpandedText: () => "[Skills]\n /home/u/skills/alpha/SKILL.md",
129
+ },
130
+ );
131
+ resources.addChild(comp);
132
+
133
+ expect(setExpanded).not.toHaveBeenCalled();
134
+ expect(ref.sections).toEqual([{ name: "Skills", items: ["alpha"] }]);
135
+ });
136
+
137
+ it("splits a collapsed comma-joined body when no expansion API exists (safety net)", () => {
138
+ const { tui, resources } = makeTui();
139
+ lastResources = resources;
140
+ const ref = makeRef();
141
+ patchStartupListing(tui, THEME, ref);
142
+
143
+ // No setExpanded, no getExpandedText — render() yields the collapsed body
144
+ const comp = makeSectionComponent(["[Skills]", " alpha, beta, gamma"]);
145
+ resources.addChild(comp);
146
+
147
+ expect(ref.sections).toEqual([{ name: "Skills", items: ["alpha", "beta", "gamma"] }]);
148
+ expect((resources as any).children.length).toBe(0);
149
+ });
150
+
151
+ it("passes non-section text through to the original addChild", () => {
152
+ const { tui, resources } = makeTui();
153
+ lastResources = resources;
154
+ const ref = makeRef();
155
+ patchStartupListing(tui, THEME, ref);
156
+
157
+ const comp = makeSectionComponent(["some random text"]);
158
+ resources.addChild(comp);
159
+
160
+ expect((resources as any).children).toContain(comp);
161
+ expect(ref.sections).toEqual([]);
162
+ });
163
+
164
+ it("merges items from repeated section components", () => {
165
+ const { tui, resources } = makeTui();
166
+ lastResources = resources;
167
+ const ref = makeRef();
168
+ patchStartupListing(tui, THEME, ref);
169
+
170
+ resources.addChild(makeSectionComponent(["[Skills]", " alpha, beta"]));
171
+ resources.addChild(makeSectionComponent(["[Skills]", " beta, gamma"]));
172
+
173
+ expect(ref.sections).toEqual([{ name: "Skills", items: ["alpha", "beta", "gamma"] }]);
174
+ });
175
+ });
@@ -0,0 +1,348 @@
1
+ import { VERSION, type Theme } from "@earendil-works/pi-coding-agent";
2
+ import { getShinedLogo, TRUECOLOR, LOGO_PAD, LOGO_SETTLE_FRAME } from "./logo.js";
3
+ import { loadUIConfig } from "../config.js";
4
+ import { detectSection, parseSectionText, parseModelScope, formatColumns, buildItemWrapper, type ParsedSection, SECTION_KEYS } from "./sections.js";
5
+ import { fetchLatestVersion, compareVersions } from "./version.js";
6
+ import { stripAnsi } from "@pi-archimedes/core/text";
7
+ import { Text, Spacer, Container, type TUI, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
8
+
9
+ // Symbol keys (survive hot-reload)
10
+ const LISTING_REF = Symbol.for("splashscreen:listingRef");
11
+ const ANIM_INTERVAL = Symbol.for("splashscreen:animInterval");
12
+ const DEBOUNCE_TIMER = Symbol.for("splashscreen:debounceTimer");
13
+ const PATCHED_CLEAR = Symbol.for("splashscreen:clearPatched");
14
+ const PATCHED_LISTING = Symbol.for("splashscreen:listingPatched");
15
+ const ORIG_ADD_CHILD = Symbol.for("splashscreen:origAddChild");
16
+
17
+ // Animation constants
18
+ const MAX_RENDER_WIDTH = 9999;
19
+ const MIN_HEADER_LINES = 11;
20
+ const REVEAL_DEBOUNCE_MS = 150;
21
+ const RAMP_FRAMES = 22;
22
+ const STAGGER_FRAMES = 0;
23
+ const BASE_FADE_DELAY = 3;
24
+ const MAX_STAGGER = BASE_FADE_DELAY + 5 * STAGGER_FRAMES;
25
+
26
+ export interface ListingRef {
27
+ sections: ParsedSection[];
28
+ frame: number;
29
+ revealed: boolean;
30
+ revealedAt: number;
31
+ scaffoldAt: number;
32
+ latestVersion?: string;
33
+ settled: boolean;
34
+ cachedLines?: string[];
35
+ cachedWidth?: number;
36
+ cachedHeight?: number;
37
+ maxHeaderHeight?: number;
38
+ }
39
+
40
+ // ── renderHeader() — composes logo + sections + padding ────────────────
41
+
42
+ export function renderHeader(theme: Theme, ref: ListingRef, width: number, height: number): string[] {
43
+ const dim = (t: string) => theme.fg("dim", t);
44
+ const accent = (t: string) => theme.fg("accent", t);
45
+ const logoLines = getShinedLogo(ref.frame, loadUIConfig().animationStyle);
46
+
47
+ // Use cached text lines if settled (no more animations)
48
+ let listingLines: string[];
49
+
50
+ if (ref.settled && ref.cachedLines && ref.cachedWidth === width && ref.cachedHeight === height) {
51
+ listingLines = ref.cachedLines;
52
+ } else {
53
+ const sectionsToRender: { name: "Version" | ParsedSection["name"]; items: string[] }[] = [];
54
+
55
+ if (!ref.revealed) {
56
+ // Logo only — sections appear together on reveal
57
+ } else {
58
+ const latest = ref.latestVersion ?? VERSION;
59
+ const hasUpdate = compareVersions(latest, VERSION) > 0;
60
+ const latestStr = hasUpdate ? `Latest: ${accent("v" + latest)}` : `Latest: v${latest}`;
61
+ sectionsToRender.push({ name: "Version", items: [`Local: v${VERSION}`, latestStr] });
62
+
63
+ // Display sections in SECTION_KEYS order, skip empty
64
+ const byName = new Map(ref.sections.map(s => [s.name, s]));
65
+ for (const key of SECTION_KEYS) {
66
+ const sec = byName.get(key);
67
+ if (sec && sec.items.length > 0) sectionsToRender.push(sec);
68
+ }
69
+ }
70
+
71
+ const listingMaxW = Math.floor(width * 0.7);
72
+ listingLines = formatColumns(sectionsToRender, theme, listingMaxW, ref);
73
+
74
+ // Cache once text animations are done
75
+ const textAge = ref.revealed ? ref.frame - ref.revealedAt : 0;
76
+ const textDone = ref.revealed && textAge > RAMP_FRAMES + MAX_STAGGER;
77
+ const logoDone = ref.frame >= LOGO_SETTLE_FRAME;
78
+
79
+ if (textDone && logoDone) {
80
+ ref.settled = true;
81
+ ref.cachedLines = listingLines;
82
+ ref.cachedWidth = width;
83
+ ref.cachedHeight = height;
84
+ }
85
+ }
86
+
87
+ // Build content: logo + separator + listing
88
+ const contentLines: string[] = [];
89
+
90
+ // Logo centered above listing (same center as listing text)
91
+ const listingMaxW = Math.floor(width * 0.7);
92
+ const listingLeftPad = Math.floor((width - listingMaxW) / 2);
93
+ for (const logoRow of logoLines) {
94
+ const pad = Math.floor((listingMaxW - visibleWidth(logoRow)) / 2);
95
+ contentLines.push(" ".repeat(LOGO_PAD) + " ".repeat(listingLeftPad + pad) + logoRow);
96
+ }
97
+
98
+ // Separator
99
+ contentLines.push("");
100
+
101
+ // Listing
102
+ for (const listRow of listingLines) {
103
+ contentLines.push(" ".repeat(LOGO_PAD) + " ".repeat(listingLeftPad) + listRow);
104
+ }
105
+
106
+ // Pad top and bottom to fill height (+5 top bias for visual centering)
107
+ const VERTICAL_CENTER_BIAS = 5; // Shift content slightly upward for visual balance
108
+ const contentHeight = contentLines.length;
109
+ const remaining = Math.max(0, height - contentHeight);
110
+ const topPad = Math.floor(remaining / 2) + VERTICAL_CENTER_BIAS;
111
+ const bottomPad = remaining - topPad;
112
+
113
+ const result: string[] = [];
114
+ for (let i = 0; i < topPad; i++) result.push("");
115
+ for (const line of contentLines) result.push(line);
116
+ for (let i = 0; i < bottomPad; i++) result.push("");
117
+
118
+ return result;
119
+ }
120
+
121
+ // ── Section text extraction ───────────────────────────────────────────────
122
+
123
+ /**
124
+ * Read the full text of a section component.
125
+ *
126
+ * pi ≥0.99 builds ExpandableText with constructor closures (no instance
127
+ * getExpandedText() anymore) and starts collapsed — a plain render() would
128
+ * return the lossy comma-joined body. setExpanded(true) (pi's own public
129
+ * expansion API) makes render() return the full per-item body.
130
+ */
131
+ function expandedText(component: Text): string {
132
+ const setExpanded = (component as any).setExpanded;
133
+ if (typeof setExpanded === "function") setExpanded.call(component, true);
134
+ return stripAnsi(component.render(MAX_RENDER_WIDTH).join("\n"));
135
+ }
136
+
137
+ // ── Chat container discovery ────────────────────────────────────────────
138
+
139
+ // Fragile: relies on TUI child ordering (header, chat, footer) which is an
140
+ // internal layout detail of pi's InteractiveMode. If upstream changes the
141
+ // child structure, this will need updating.
142
+ //
143
+ // Strategy (most specific → least specific):
144
+ // 1. Container with "Scrollable" in constructor name
145
+ // 2. Middle child — TUI layout is [headerContainer, chatContainer, footer]
146
+ // This is position-stable even when chatContainer is empty (e.g. after clear)
147
+ // 3. Container with most children (chat usually has the most)
148
+ // 4. First Container found
149
+ function findChatContainer(tui: TUI): Container | undefined {
150
+ // Strategy 1: Look for Scrollable container
151
+ for (const child of tui.children) {
152
+ if (child instanceof Container && child.constructor.name.includes("Scrollable")) {
153
+ return child;
154
+ }
155
+ }
156
+
157
+ // Strategy 2: Middle child — TUI layout is [headerContainer, chatContainer, footer]
158
+ if (tui.children.length >= 3) {
159
+ const middle = tui.children[1];
160
+ if (middle instanceof Container) return middle;
161
+ }
162
+
163
+ // Strategy 3: Find the Container with the most children (chat area)
164
+ const containers = tui.children.filter((c): c is Container => c instanceof Container);
165
+ if (containers.length > 1) {
166
+ const largest = containers.reduce((best, c) =>
167
+ c.children.length > best.children.length ? c : best,
168
+ );
169
+ if (largest.children.length > 0) return largest;
170
+ }
171
+
172
+ // Strategy 4: First Container
173
+ if (containers.length > 0) return containers[0];
174
+
175
+ return undefined;
176
+ }
177
+
178
+ // Pi 0.84+ moved resource sections from chatContainer to loadedResourcesContainer
179
+ // (child of documentContainer). Find it by locating documentContainer (the first TUI
180
+ // child that contains Container sub-children), then taking its second Container child.
181
+ //
182
+ // Pi 0.84+ TUI layout (regular mode, 7 top-level children):
183
+ // documentContainer → [headerContainer(0), loadedResourcesContainer(1), chatContainer(2)]
184
+ function findLoadedResourcesContainer(tui: TUI): Container | undefined {
185
+ // Only run this heuristic on Pi >= 0.84.0 where loadedResourcesContainer exists.
186
+ // On older Pi versions, return undefined so the caller falls back to findChatContainer().
187
+ if (compareVersions(VERSION, "0.84.0") < 0) return undefined;
188
+
189
+ // Find documentContainer: first tui child that has Container sub-children
190
+ const docContainer = tui.children.find(
191
+ (c) => c instanceof Container && c.children.some((child) => child instanceof Container)
192
+ ) as Container | undefined;
193
+ if (!docContainer) return undefined;
194
+
195
+ const containerChildren = docContainer.children.filter(
196
+ (c): c is Container => c instanceof Container
197
+ );
198
+ // Pi 0.84+: [headerContainer(0), loadedResourcesContainer(1), chatContainer(2)]
199
+ // The index [1] assumption is load-bearing — tied to Pi 0.84+ initialization order.
200
+ if (containerChildren.length < 2) return undefined;
201
+ return containerChildren[1]; // loadedResourcesContainer
202
+ }
203
+
204
+ export function patchStartupListing(
205
+ tui: TUI,
206
+ _theme: Theme,
207
+ ref: ListingRef,
208
+ ): void {
209
+ const resourcesContainer = findLoadedResourcesContainer(tui) ?? findChatContainer(tui);
210
+ if (!resourcesContainer) {
211
+ console.warn("[archimedes] Could not find resources container — startup listing disabled. This may indicate a pi TUI structure change.");
212
+ return;
213
+ }
214
+ const rc = resourcesContainer as any;
215
+
216
+ // Always update ref + restart animation (critical for /reload)
217
+ rc[LISTING_REF] = ref;
218
+ ref.frame = 0;
219
+ ref.revealed = false;
220
+ ref.revealedAt = 0;
221
+ ref.scaffoldAt = 0;
222
+ ref.settled = false;
223
+ delete ref.cachedLines;
224
+ delete ref.cachedWidth;
225
+ delete ref.maxHeaderHeight;
226
+
227
+ if (rc[ANIM_INTERVAL]) clearInterval(rc[ANIM_INTERVAL]);
228
+ if (rc[DEBOUNCE_TIMER]) clearTimeout(rc[DEBOUNCE_TIMER]);
229
+
230
+ const interval = setInterval(() => {
231
+ try {
232
+ const current: ListingRef = rc[LISTING_REF];
233
+ if (!current) {
234
+ clearInterval(interval);
235
+ return;
236
+ }
237
+ current.frame++;
238
+ if (current.settled && current.frame >= LOGO_SETTLE_FRAME) {
239
+ clearInterval(interval);
240
+ rc[ANIM_INTERVAL] = null;
241
+ return;
242
+ }
243
+ tui.requestRender();
244
+ } catch {
245
+ clearInterval(interval);
246
+ }
247
+ }, 16);
248
+
249
+ rc[ANIM_INTERVAL] = interval;
250
+
251
+ // Fetch latest version from npm
252
+ fetchLatestVersion().then(v => {
253
+ if (v) {
254
+ const current: ListingRef = rc[LISTING_REF];
255
+ current.latestVersion = v;
256
+ // Invalidate cache so version updates on next render
257
+ delete current.cachedLines;
258
+ current.settled = false;
259
+ }
260
+ });
261
+
262
+ // Patch clear() to intercept container rebuild
263
+ if (!rc[PATCHED_CLEAR]) {
264
+ rc[PATCHED_CLEAR] = true;
265
+ const origClear = resourcesContainer.clear.bind(resourcesContainer);
266
+ resourcesContainer.clear = () => {
267
+ return origClear();
268
+ };
269
+ }
270
+
271
+ // Only patch addChild once — the closure reads rc[LISTING_REF] dynamically
272
+ if (rc[PATCHED_LISTING]) {
273
+ resourcesContainer.clear();
274
+ return;
275
+ }
276
+ rc[PATCHED_LISTING] = true;
277
+
278
+ const origAddChild = resourcesContainer.addChild.bind(resourcesContainer);
279
+ rc[ORIG_ADD_CHILD] = origAddChild; // Store for cleanup on shutdown
280
+ resourcesContainer.clear();
281
+
282
+ resourcesContainer.addChild = (component: Component) => {
283
+ try {
284
+ const currentRef: ListingRef = rc[LISTING_REF];
285
+
286
+ if (component instanceof Text) {
287
+ // pi ≥0.67.6 wraps startup sections in ExpandableText; collapsed body
288
+ // is a lossy comma-joined base-name list. Parse the expanded text so
289
+ // Extensions keep real names instead of "index.ts/index.js/index".
290
+ // pi ≥0.99 moved the text builders into the constructor (no instance
291
+ // getExpandedText()) — expandedText() forces expansion via setExpanded().
292
+ const getExpanded = (component as any).getExpandedText;
293
+ const plain = typeof getExpanded === "function"
294
+ ? stripAnsi(getExpanded.call(component))
295
+ : expandedText(component);
296
+
297
+ const section = parseSectionText(plain) ?? parseModelScope(plain);
298
+ if (section) {
299
+ const existing = currentRef.sections.find(s => s.name === section.name);
300
+ if (existing) {
301
+ existing.items = [...new Set([...existing.items, ...section.items])];
302
+ } else {
303
+ currentRef.sections.push(section);
304
+ }
305
+
306
+ // Invalidate cache so late-arriving sections show up
307
+ currentRef.settled = false;
308
+ delete currentRef.cachedLines;
309
+
310
+ if (currentRef.revealed) {
311
+ // Already revealed — show new section immediately
312
+ tui.requestRender();
313
+ } else {
314
+ // Batch initial sections — reset debounce on each arrival
315
+ if (rc[DEBOUNCE_TIMER]) clearTimeout(rc[DEBOUNCE_TIMER]);
316
+ rc[DEBOUNCE_TIMER] = setTimeout(() => {
317
+ const ref: ListingRef = rc[LISTING_REF];
318
+ ref.revealed = true;
319
+ ref.revealedAt = ref.frame;
320
+ ref.scaffoldAt = ref.frame;
321
+ tui.requestRender();
322
+ rc[DEBOUNCE_TIMER] = null;
323
+ }, REVEAL_DEBOUNCE_MS);
324
+ }
325
+
326
+ return;
327
+ }
328
+
329
+ if (
330
+ plain.includes("Listing all available commands") ||
331
+ plain.includes("(Source: extension)") ||
332
+ plain.trim().startsWith("/skill:")
333
+ ) {
334
+ return;
335
+ }
336
+ }
337
+
338
+ if (component instanceof Spacer && !currentRef.revealed) return;
339
+ try {
340
+ origAddChild(component);
341
+ } catch (err) {
342
+ console.error("[archimedes] Error in resourcesContainer.addChild:", err);
343
+ }
344
+ } catch (err) {
345
+ console.error("[archimedes] Error in startup listing addChild handler:", err);
346
+ }
347
+ };
348
+ }