@pi-archimedes/ui 2.10.4 → 2.11.1
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 +57 -5
- package/src/bash/renderer.ts +33 -32
- 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/index.ts
CHANGED
|
@@ -11,13 +11,21 @@ import type {
|
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
12
|
|
|
13
13
|
import { HephaestusEditor } from "./editor/index.js";
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
renderHeader,
|
|
16
|
+
patchStartupListing,
|
|
17
|
+
revealStartupListing,
|
|
18
|
+
stopStartupListing,
|
|
19
|
+
modelScopeSection,
|
|
20
|
+
type ListingRef,
|
|
21
|
+
} from "./startup/index.js";
|
|
22
|
+
import type { ParsedSection } from "./startup/sections.js";
|
|
16
23
|
import { patchThinkingRenderer } from "./thinking/patch.js";
|
|
17
24
|
import { transformThinkingContent } from "./thinking/transform.js";
|
|
18
25
|
import {
|
|
19
26
|
loadUIConfig,
|
|
20
27
|
saveUIConfig,
|
|
28
|
+
readQuietStartup,
|
|
21
29
|
DEFAULT_UI_CONFIG,
|
|
22
30
|
ANIMATION_STYLES,
|
|
23
31
|
normalizeThinkingStyle,
|
|
@@ -25,13 +33,13 @@ import {
|
|
|
25
33
|
type UIConfig,
|
|
26
34
|
type CoreConfig,
|
|
27
35
|
} from "./config.js";
|
|
36
|
+
import { reportPiSupport } from "@pi-archimedes/core/pi-support";
|
|
28
37
|
import { getUISettingsItems } from "./settings.js";
|
|
29
38
|
import { migrateCoreToUIConfig, migrateCompactThinkingToStyle, migrateRemovedToolPatch } from "./migration.js";
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
39
|
+
import { renderBashCall, renderBashResult } from "./bash/index.js";
|
|
40
|
+
import { renderCodemodeCall, renderCodemodeResult } from "./codemode/index.js";
|
|
32
41
|
|
|
33
42
|
// Re-exports
|
|
34
|
-
export { unpatchConsoleLog } from "./startup/capture.js";
|
|
35
43
|
export { getUISettingsItems } from "./settings.js";
|
|
36
44
|
export {
|
|
37
45
|
loadUIConfig,
|
|
@@ -51,11 +59,20 @@ export {
|
|
|
51
59
|
// Module-level state for session lifecycle
|
|
52
60
|
let uiRef: ListingRef | undefined;
|
|
53
61
|
let uiCtx: ExtensionContext | undefined;
|
|
54
|
-
let uiTui: TUI | undefined;
|
|
55
62
|
|
|
56
63
|
let spinFlag = false;
|
|
57
64
|
let spinInterval: ReturnType<typeof setInterval> | undefined;
|
|
58
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Which Archimedes tool rows the current session asks for, refreshed at every
|
|
68
|
+
* `session_start` and read by the renderer resolver. The resolver itself is
|
|
69
|
+
* registered once per extension load — pi keeps a list of resolvers per
|
|
70
|
+
* extension, so registering it from `session_start` would stack a duplicate on
|
|
71
|
+
* every `/reload` — which means the settings have to be read here rather than
|
|
72
|
+
* inside the resolver: `renderCall`/`renderResult` run on every frame.
|
|
73
|
+
*/
|
|
74
|
+
let styledTools = { bash: false, codemode: false };
|
|
75
|
+
|
|
59
76
|
function clearSpinInterval(): void {
|
|
60
77
|
if (spinInterval) {
|
|
61
78
|
clearInterval(spinInterval);
|
|
@@ -63,7 +80,48 @@ function clearSpinInterval(): void {
|
|
|
63
80
|
}
|
|
64
81
|
}
|
|
65
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Draw the bash row through pi's renderer resolution (ADR 0029).
|
|
85
|
+
*
|
|
86
|
+
* `registerToolRenderer()` was added in pi 1.0.1 precisely for presentation
|
|
87
|
+
* overrides: it chooses how calls to a tool are drawn without registering the
|
|
88
|
+
* tool, and `next()` hands the tool to the remaining resolvers and then to
|
|
89
|
+
* whatever is registered. Off settings therefore give back pi's own row rather
|
|
90
|
+
* than the previous arrangement — re-registering `bash` from
|
|
91
|
+
* `createBashToolDefinition()` with our renderers bolted on — which also means
|
|
92
|
+
* anything pi later adds to that tool cannot be silently dropped by us.
|
|
93
|
+
*/
|
|
94
|
+
function registerToolRenderers(pi: ExtensionAPI): void {
|
|
95
|
+
// Capability check, not a version wish: on a host without the API there is
|
|
96
|
+
// nothing to resolve through, and pi's own rows stand in.
|
|
97
|
+
if (typeof pi.registerToolRenderer !== "function") return;
|
|
98
|
+
|
|
99
|
+
pi.registerToolRenderer((toolName, next) => {
|
|
100
|
+
// `next()` is spread first so that whatever the built-in definition
|
|
101
|
+
// declares besides the two renderers — `renderShell` is the one that matters
|
|
102
|
+
// today — survives resolution instead of being silently dropped by us.
|
|
103
|
+
const ours =
|
|
104
|
+
toolName === "bash" && styledTools.bash
|
|
105
|
+
? { renderCall: renderBashCall, renderResult: renderBashResult }
|
|
106
|
+
: toolName === "codemode" && styledTools.codemode
|
|
107
|
+
? { renderCall: renderCodemodeCall, renderResult: renderCodemodeResult }
|
|
108
|
+
: undefined;
|
|
109
|
+
return ours ? { ...next(), ...ours } : next();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
66
113
|
export function registerUI(pi: ExtensionAPI): void {
|
|
114
|
+
// Floor check before anything that can fail (ADR 0026): pi never installs or
|
|
115
|
+
// solves peers, so the peer range below cannot warn anyone — the runtime
|
|
116
|
+
// comparison against the host's own VERSION is the only thing that can. It
|
|
117
|
+
// records a warning for meta to print once, aggregated; nothing here is
|
|
118
|
+
// withheld on a below-floor host, because a `ui` that registered nothing
|
|
119
|
+
// would make /plugins report an enabled package that did nothing.
|
|
120
|
+
reportPiSupport("ui", "1.2.0");
|
|
121
|
+
|
|
122
|
+
// Registered once per extension load, before any session exists (ADR 0029).
|
|
123
|
+
registerToolRenderers(pi);
|
|
124
|
+
|
|
67
125
|
// One-time migration of any legacy core config to UI config
|
|
68
126
|
migrateCoreToUIConfig();
|
|
69
127
|
migrateCompactThinkingToStyle();
|
|
@@ -71,9 +129,6 @@ export function registerUI(pi: ExtensionAPI): void {
|
|
|
71
129
|
// previous version left on the shared prototype (upgrade + /reload case).
|
|
72
130
|
migrateRemovedToolPatch();
|
|
73
131
|
|
|
74
|
-
// Patch console.log for model scope capture
|
|
75
|
-
patchConsoleLog();
|
|
76
|
-
|
|
77
132
|
pi.on("message_end", (event, _ctx) => {
|
|
78
133
|
if (loadUIConfig().codeUnindent) {
|
|
79
134
|
transformThinkingContent(event.message as any);
|
|
@@ -82,67 +137,29 @@ export function registerUI(pi: ExtensionAPI): void {
|
|
|
82
137
|
|
|
83
138
|
// session_shutdown handler (top-level to prevent accumulation on /reload)
|
|
84
139
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
85
|
-
unpatchConsoleLog();
|
|
86
|
-
clearActiveBashIntervals();
|
|
87
|
-
clearActiveCodemodeIntervals();
|
|
88
|
-
|
|
89
140
|
const targetCtx = ctx?.hasUI ? ctx : uiCtx;
|
|
90
141
|
if (targetCtx?.hasUI) {
|
|
142
|
+
// Give the working row back whichever way we left it: a session that
|
|
143
|
+
// started with the border spinner on had hidden it (`setWorkingVisible(false)`).
|
|
91
144
|
targetCtx.ui.setWorkingVisible(true);
|
|
145
|
+
// Give back everything the session borrowed: pi's animated indicator and
|
|
146
|
+
// its own hidden-thinking wording, so a session that starts after this
|
|
147
|
+
// one (or after `/reload` with the plugin off) sees pi's defaults.
|
|
148
|
+
targetCtx.ui.setWorkingIndicator(undefined);
|
|
149
|
+
targetCtx.ui.setHiddenThinkingLabel(undefined);
|
|
92
150
|
targetCtx.ui.setEditorComponent(undefined);
|
|
93
151
|
}
|
|
94
152
|
clearSpinInterval();
|
|
95
153
|
spinFlag = false;
|
|
96
154
|
|
|
155
|
+
// The splash: stop the frame/reveal timers, then freeze the ref so a late
|
|
156
|
+
// render of a header still on screen cannot restart the animation. Nothing
|
|
157
|
+
// else to undo — the startup listing interception and the console.log patch
|
|
158
|
+
// this used to un-install are gone (pi 1.x alignment, Task 8).
|
|
159
|
+
stopStartupListing();
|
|
97
160
|
if (uiRef) {
|
|
98
161
|
uiRef.settled = true;
|
|
99
162
|
}
|
|
100
|
-
|
|
101
|
-
const PATCHED_LISTING = Symbol.for("splashscreen:listingPatched");
|
|
102
|
-
const ORIG_ADD_CHILD = Symbol.for("splashscreen:origAddChild");
|
|
103
|
-
if (uiTui) {
|
|
104
|
-
try {
|
|
105
|
-
for (const child of uiTui.children) {
|
|
106
|
-
const cc = child as any;
|
|
107
|
-
if (cc[PATCHED_LISTING] && cc[ORIG_ADD_CHILD]) {
|
|
108
|
-
cc.addChild = cc[ORIG_ADD_CHILD];
|
|
109
|
-
cc[PATCHED_LISTING] = false;
|
|
110
|
-
cc[ORIG_ADD_CHILD] = undefined;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
for (const topChild of uiTui.children) {
|
|
114
|
-
const tc = topChild as any;
|
|
115
|
-
if (!tc.children) continue;
|
|
116
|
-
for (const child of tc.children) {
|
|
117
|
-
const cc = child as any;
|
|
118
|
-
if (cc[PATCHED_LISTING] && cc[ORIG_ADD_CHILD]) {
|
|
119
|
-
cc.addChild = cc[ORIG_ADD_CHILD];
|
|
120
|
-
cc[PATCHED_LISTING] = false;
|
|
121
|
-
cc[ORIG_ADD_CHILD] = undefined;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const ANIM_INTERVAL = Symbol.for("splashscreen:animInterval");
|
|
127
|
-
const DEBOUNCE_TIMER = Symbol.for("splashscreen:debounceTimer");
|
|
128
|
-
for (const child of uiTui.children) {
|
|
129
|
-
const cc = child as any;
|
|
130
|
-
if (cc[ANIM_INTERVAL]) clearInterval(cc[ANIM_INTERVAL]);
|
|
131
|
-
if (cc[DEBOUNCE_TIMER]) clearTimeout(cc[DEBOUNCE_TIMER]);
|
|
132
|
-
}
|
|
133
|
-
for (const topChild of uiTui.children) {
|
|
134
|
-
const tc = topChild as any;
|
|
135
|
-
if (!tc.children) continue;
|
|
136
|
-
for (const child of tc.children) {
|
|
137
|
-
const cc = child as any;
|
|
138
|
-
if (cc[ANIM_INTERVAL]) clearInterval(cc[ANIM_INTERVAL]);
|
|
139
|
-
if (cc[DEBOUNCE_TIMER]) clearTimeout(cc[DEBOUNCE_TIMER]);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
} catch {
|
|
143
|
-
/* TUI structure may have changed — ignore */
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
163
|
});
|
|
147
164
|
|
|
148
165
|
// session_start handler
|
|
@@ -151,56 +168,180 @@ export function registerUI(pi: ExtensionAPI): void {
|
|
|
151
168
|
|
|
152
169
|
const config = loadUIConfig();
|
|
153
170
|
const toolStyle = normalizeToolStyle(config.toolStyle);
|
|
171
|
+
const minimal = toolStyle === "Minimal";
|
|
154
172
|
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// The codemode override replaces pi's built-in codemode extension
|
|
165
|
-
// (the same take-over the bash override does for the core bash
|
|
166
|
-
// tool). It loads the native definition from the running CLI and
|
|
167
|
-
// swaps in the Archimedes renderers; it is a no-op on pi versions
|
|
168
|
-
// without the codemode extension.
|
|
169
|
-
if (config.codemodeToolStyling !== false) {
|
|
170
|
-
void registerCodemodeToolOverride(pi);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
+
// What the resolver answers with for this session (see `styledTools`).
|
|
174
|
+
// `Minimal` = archimedes tool styling (the per-tool toggles fine-tune it);
|
|
175
|
+
// `Native` = none of it, so pi's own rows stand everywhere.
|
|
176
|
+
styledTools = {
|
|
177
|
+
bash: minimal && config.bashToolStyling !== false,
|
|
178
|
+
codemode: minimal && config.codemodeToolStyling !== false,
|
|
179
|
+
};
|
|
173
180
|
|
|
174
181
|
if (ctx.hasUI) {
|
|
175
182
|
spinFlag = config.editorSpinBorder;
|
|
176
183
|
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
184
|
+
// pi 1.x owns the startup screen, and the suite's splash is opt-in
|
|
185
|
+
// (`archimedes.ui.startupSplash`, default off — the maintainer's call: keep
|
|
186
|
+
// the code, flip the default). `ctx.scopedModels` is the model scope pi
|
|
187
|
+
// would otherwise have printed, so nothing here scrapes pi's stdout or its
|
|
188
|
+
// component tree any more (that was `startup/capture.ts` plus the
|
|
189
|
+
// `addChild` interception in `patchStartupListing`, both removed here).
|
|
190
|
+
//
|
|
191
|
+
// The two settings are independent knobs, so their combination is stated
|
|
192
|
+
// in full. `quietStartup` is pi's own (`false` = header + details,
|
|
193
|
+
// `"header"` = header alone, `true` = nothing);
|
|
194
|
+
// `startupSplash` is ours (off = we never install a header).
|
|
195
|
+
//
|
|
196
|
+
// startupSplash quietStartup what the header slot gets timers
|
|
197
|
+
// ------------- ------------ ---------------------------- ------
|
|
198
|
+
// false (default) false pi's own header none
|
|
199
|
+
// false (default) "header" pi's own header none
|
|
200
|
+
// false (default) true pi's own (empty) header none
|
|
201
|
+
// true false our splash (+ `[Models]` if armed
|
|
202
|
+
// any model is scoped)
|
|
203
|
+
// true "header" our splash, no scope armed
|
|
204
|
+
// true true pi's own (empty) header none
|
|
205
|
+
//
|
|
206
|
+
// Off wins outright: with the splash off we hand the header back to pi
|
|
207
|
+
// whatever `quietStartup` says, and pi's own setting then decides what pi
|
|
208
|
+
// draws — its header and resource listing, its header alone, or its empty
|
|
209
|
+
// `Text("", 0, 0)` — exactly as it does with no archimedes loaded. Handing
|
|
210
|
+
// it back through `setHeader(undefined)` rather than simply doing nothing
|
|
211
|
+
// is what makes a splash left installed by an earlier session in the same
|
|
212
|
+
// process (`/reload` after turning it off) give way: pi puts its
|
|
213
|
+
// `builtInHeader` back in the slot (`setExtensionHeader()`,
|
|
214
|
+
// interactive-mode.js:2006-2011).
|
|
215
|
+
//
|
|
216
|
+
// On means today's behaviour, unchanged. `startupSplash: true` with
|
|
217
|
+
// `quietStartup: true` is the one row worth naming: pi asked for no header
|
|
218
|
+
// at all, so the splash stands down and pi's silence stands — opting in to
|
|
219
|
+
// the splash is not an instruction to overrule pi's own quiet startup, and
|
|
220
|
+
// `quietStartup` keeps deciding whether the resource listing (and so our
|
|
221
|
+
// `[Models]` row) shows. Pinned cell by cell in
|
|
222
|
+
// `startup-splash-gate.test.ts`.
|
|
223
|
+
//
|
|
224
|
+
// `startupSplash` is ours, and read strictly: anything that is not the
|
|
225
|
+
// literal `true` — a hand-edited `"true"`, a `1` — means off, the same way
|
|
226
|
+
// core's `isConfigEnabled` refuses to guess at a boolean.
|
|
227
|
+
const startupSplash = config.startupSplash === true;
|
|
228
|
+
// pi's own answer, asked only when the splash is on: with it off we install
|
|
229
|
+
// no header, so there is nothing for `quietStartup` to decide.
|
|
230
|
+
const quietStartup = startupSplash ? readQuietStartup(pi) : undefined;
|
|
231
|
+
|
|
232
|
+
if (!startupSplash) {
|
|
233
|
+
ctx.ui.setHeader(undefined);
|
|
234
|
+
uiRef = undefined;
|
|
235
|
+
} else if (quietStartup === true) {
|
|
236
|
+
// pi's own header is an empty component in this mode. Handing ours back
|
|
237
|
+
// is what keeps that silence: a splash left installed from an earlier
|
|
238
|
+
// session in the same process (`/reload` after a settings change) would
|
|
239
|
+
// otherwise animate over it.
|
|
240
|
+
ctx.ui.setHeader(undefined);
|
|
241
|
+
uiRef = undefined;
|
|
242
|
+
} else {
|
|
243
|
+
// What the splash reveals after the logo. The version row belongs to
|
|
244
|
+
// the header itself; the model scope is a startup *detail*, so it
|
|
245
|
+
// follows pi's own rule for the `Model scope:` line and the resource
|
|
246
|
+
// listing: visible only when `quietStartup` is `false`.
|
|
247
|
+
const sections: ParsedSection[] = [];
|
|
248
|
+
const models = quietStartup === false ? modelScopeSection(ctx.scopedModels) : undefined;
|
|
249
|
+
if (models) sections.push(models);
|
|
250
|
+
|
|
251
|
+
// Set animated header
|
|
252
|
+
uiRef = {
|
|
253
|
+
sections: [],
|
|
254
|
+
frame: 0,
|
|
255
|
+
revealed: false,
|
|
256
|
+
revealedAt: 0,
|
|
257
|
+
scaffoldAt: 0,
|
|
258
|
+
settled: false,
|
|
259
|
+
};
|
|
260
|
+
const ref = uiRef;
|
|
261
|
+
const headerFactory = (tui: TUI, theme: Theme): Component & { dispose(): void } => {
|
|
262
|
+
// Nothing here is sized by the screen: pi calls this factory again
|
|
263
|
+
// whenever the header is replaced — a new session, `/reload` (which
|
|
264
|
+
// goes through `resetExtensionUI()`), or any `setHeader()` — and never
|
|
265
|
+
// for a `tuiMode` switch (`switchTuiMode()` re-mounts the components it
|
|
266
|
+
// already has), so a per-screen measurement taken here would be the
|
|
267
|
+
// measurement the header kept until the next replacement. The header
|
|
268
|
+
// does not need one: it renders its own content on either screen
|
|
269
|
+
// (`renderHeader`), which is both what keeps a frame-bounded viewport
|
|
270
|
+
// usable (ADR 0027) and what leaves pi's listing directly below it.
|
|
271
|
+
const comp: Component & { dispose(): void } = {
|
|
272
|
+
// pi invalidates the component tree on every theme change
|
|
273
|
+
// (`onThemeChange()` → `ui.invalidate()`) and from the theme file
|
|
274
|
+
// watcher, and the splash's cached lines carry the theme's colours
|
|
275
|
+
// — so this is the hook that keeps a finished splash from rendering
|
|
276
|
+
// yesterday's palette for the rest of the session. The cache key is
|
|
277
|
+
// the width it was drawn at, which a theme switch leaves alone.
|
|
278
|
+
// Only the memo goes: this is a repaint, not an animation restart.
|
|
279
|
+
invalidate(): void {
|
|
280
|
+
delete ref.cachedLines;
|
|
281
|
+
delete ref.cachedWidth;
|
|
282
|
+
},
|
|
283
|
+
render(width: number): string[] {
|
|
284
|
+
return renderHeader(theme, ref, width);
|
|
285
|
+
},
|
|
286
|
+
// pi disposes the header it is replacing before it builds the next
|
|
287
|
+
// one (`setExtensionHeader()` disposes, then calls the factory), so
|
|
288
|
+
// this is where the splash's timers die on a `setHeader()`
|
|
289
|
+
// replacement or a session restart — no stored renderer needed. A
|
|
290
|
+
// `tuiMode` switch does not dispose: it re-mounts this same
|
|
291
|
+
// component, whose timers then run until they settle themselves.
|
|
292
|
+
dispose(): void {
|
|
293
|
+
stopStartupListing();
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
patchStartupListing(tui, ref);
|
|
297
|
+
revealStartupListing(tui, ref, sections);
|
|
298
|
+
return comp;
|
|
194
299
|
};
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
};
|
|
198
|
-
ctx.ui.setHeader(headerFactory);
|
|
300
|
+
ctx.ui.setHeader(headerFactory);
|
|
301
|
+
}
|
|
199
302
|
|
|
200
303
|
clearSpinInterval();
|
|
304
|
+
// One announcement, in one place. pi's working row writes "Working" above
|
|
305
|
+
// the editor and our border spinner writes its label — `editorSpinLabel`
|
|
306
|
+
// defaults to "Working" as well — into the editor's own top border, so with
|
|
307
|
+
// both on screen the word appeared twice. The border is ours, so the row is
|
|
308
|
+
// handed pi's own switch: hidden while the spinner runs, given back when it
|
|
309
|
+
// does not.
|
|
310
|
+
//
|
|
311
|
+
// `setWorkingVisible()` is official API (`types.d.ts:88`), and hiding the
|
|
312
|
+
// row is what makes the indicator configuration `#106` needed redundant:
|
|
313
|
+
// pi's `setWorkingVisible(false)` clears the working status indicator
|
|
314
|
+
// outright and its `turn_start` handler honours the flag
|
|
315
|
+
// (`interactive-mode.js:1819-1828`, `:2762-2769`), so there is no row left
|
|
316
|
+
// for a `frames: []` to silence. The indicator is still handed back to pi
|
|
317
|
+
// unconditionally, so the row a user *does* see is pi's default down to its
|
|
318
|
+
// spinner.
|
|
319
|
+
//
|
|
320
|
+
// Read per session rather than remembered: `editorSpinBorder` can change
|
|
321
|
+
// between sessions, and pi puts the row back on whenever it replaces the
|
|
322
|
+
// session (`resetExtensionUI()` → `workingVisible = true`, `:1921`). The
|
|
323
|
+
// toggle needs no keying on the `pi` handle the way a registration does —
|
|
324
|
+
// it is one idempotent setter call inside the `session_start` handler, and
|
|
325
|
+
// every handle gets its own handler.
|
|
201
326
|
ctx.ui.setWorkingVisible(!spinFlag);
|
|
327
|
+
ctx.ui.setWorkingIndicator(undefined);
|
|
328
|
+
|
|
329
|
+
// The word a collapsed thinking block shows is pi's own row, so the
|
|
330
|
+
// setting is handed to pi rather than rebuilt here. An empty setting
|
|
331
|
+
// hands the label back to pi's default.
|
|
332
|
+
ctx.ui.setHiddenThinkingLabel(config.labelText.trim() || undefined);
|
|
202
333
|
|
|
203
334
|
if (spinFlag) {
|
|
335
|
+
// The border spinner lives in the editor frame, not in scrollback, so
|
|
336
|
+
// it runs on either screen. The renderer is handed straight to the
|
|
337
|
+
// editor and never kept by us, and idleness is read from `ctx` rather
|
|
338
|
+
// than from the renderer, so a screen-mode switch cannot strand it.
|
|
339
|
+
//
|
|
340
|
+
// `ctx.isIdle()` is false from `before_agent_start` onward — i.e. from
|
|
341
|
+
// the moment a run begins, not from the first token — which is exactly
|
|
342
|
+
// the *Busy episode* boundary in `CONTEXT.md` (idle → working → idle),
|
|
343
|
+
// so the spinner starting on the run's first tick is the behaviour the
|
|
344
|
+
// term defines, not an early start.
|
|
204
345
|
ctx.ui.setEditorComponent(
|
|
205
346
|
(tui: TUI, editorTheme: EditorTheme, keybindings: KeybindingsManager) => {
|
|
206
347
|
const theme = ctx.ui.theme;
|
|
@@ -220,12 +361,19 @@ export function registerUI(pi: ExtensionAPI): void {
|
|
|
220
361
|
);
|
|
221
362
|
}
|
|
222
363
|
|
|
223
|
-
patchThinkingRenderer(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
364
|
+
patchThinkingRenderer(
|
|
365
|
+
() => ctx.ui.theme,
|
|
366
|
+
{
|
|
367
|
+
labelText: config.labelText,
|
|
368
|
+
labelColor: config.labelColor,
|
|
369
|
+
autoCollapseThinking: config.autoCollapseThinking,
|
|
370
|
+
thinkingStyle: normalizeThinkingStyle(config.thinkingStyle),
|
|
371
|
+
},
|
|
372
|
+
// The patch's one user-visible diagnostic (a skipped patch) goes to the
|
|
373
|
+
// notification surface — the console is not a surface on either screen
|
|
374
|
+
// mode, and a subagent child discards it.
|
|
375
|
+
(message) => ctx.ui.notify(message, "warning"),
|
|
376
|
+
);
|
|
229
377
|
}
|
|
230
378
|
});
|
|
231
379
|
}
|
package/src/settings.test.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { getUISettingsItems } from "./settings.js";
|
|
|
3
3
|
import { DEFAULT_UI_CONFIG, ANIMATION_STYLES, type UIConfig } from "./config.js";
|
|
4
4
|
|
|
5
5
|
describe("getUISettingsItems", () => {
|
|
6
|
-
it("exposes all
|
|
6
|
+
it("exposes all 16 UI setting items with correct defaults", () => {
|
|
7
7
|
const items = getUISettingsItems(DEFAULT_UI_CONFIG);
|
|
8
|
-
expect(items).toHaveLength(
|
|
8
|
+
expect(items).toHaveLength(16);
|
|
9
9
|
|
|
10
10
|
const ids = items.map((i) => i.id);
|
|
11
11
|
expect(ids).toEqual([
|
|
@@ -18,6 +18,7 @@ describe("getUISettingsItems", () => {
|
|
|
18
18
|
"codeUnindent",
|
|
19
19
|
"labelText",
|
|
20
20
|
"labelColor",
|
|
21
|
+
"startupSplash",
|
|
21
22
|
"animationStyle",
|
|
22
23
|
"colorfulLogo",
|
|
23
24
|
"editorSpinBorder",
|
|
@@ -35,6 +36,7 @@ describe("getUISettingsItems", () => {
|
|
|
35
36
|
expect(items.find((i) => i.id === "codeUnindent")?.currentValue).toBe("On");
|
|
36
37
|
expect(items.find((i) => i.id === "labelText")?.currentValue).toBe("Thinking...");
|
|
37
38
|
expect(items.find((i) => i.id === "labelColor")?.currentValue).toBe("255,215,0");
|
|
39
|
+
expect(items.find((i) => i.id === "startupSplash")?.currentValue).toBe("Off");
|
|
38
40
|
expect(items.find((i) => i.id === "animationStyle")?.currentValue).toBe("vertical-up");
|
|
39
41
|
expect(items.find((i) => i.id === "editorSpinBorder")?.currentValue).toBe("On");
|
|
40
42
|
expect(items.find((i) => i.id === "editorSpinSpeed")?.currentValue).toBe("Normal");
|
|
@@ -87,6 +89,22 @@ describe("getUISettingsItems", () => {
|
|
|
87
89
|
expect(item?.values).toEqual([...ANIMATION_STYLES]);
|
|
88
90
|
});
|
|
89
91
|
|
|
92
|
+
// The splash is opt-in, so the row has to read as an opt-in to a human: a
|
|
93
|
+
// label that names the splash, and an `Off` default matching the config.
|
|
94
|
+
it("offers the startup splash as a human-readable On/Off row, off by default", () => {
|
|
95
|
+
const off = getUISettingsItems(DEFAULT_UI_CONFIG).find((i) => i.id === "startupSplash");
|
|
96
|
+
expect(off).toBeDefined();
|
|
97
|
+
expect(off!.label).toBe("Startup Splash");
|
|
98
|
+
expect(off!.description).toMatch(/splash/i);
|
|
99
|
+
expect(off!.values).toEqual(["On", "Off"]);
|
|
100
|
+
expect(off!.currentValue).toBe("Off");
|
|
101
|
+
|
|
102
|
+
const on = getUISettingsItems({ ...DEFAULT_UI_CONFIG, startupSplash: true }).find(
|
|
103
|
+
(i) => i.id === "startupSplash",
|
|
104
|
+
);
|
|
105
|
+
expect(on!.currentValue).toBe("On");
|
|
106
|
+
});
|
|
107
|
+
|
|
90
108
|
describe("robustness with hand-edited (corrupt) values", () => {
|
|
91
109
|
it("an empty-string editorSpinSpeed projects to 'Normal'", () => {
|
|
92
110
|
const items = getUISettingsItems({
|
|
@@ -119,5 +137,22 @@ describe("getUISettingsItems", () => {
|
|
|
119
137
|
});
|
|
120
138
|
expect(items.find((i) => i.id === "editorSpinStyle")?.currentValue).toBe("Typing");
|
|
121
139
|
});
|
|
140
|
+
|
|
141
|
+
// `startupSplash` is read strictly by the gate (`config.startupSplash === true`,
|
|
142
|
+
// `index.ts`), so a hand-edited `"true"` or `1` means OFF on screen. The row
|
|
143
|
+
// has to answer with the same reading — a row saying `On` over a session that
|
|
144
|
+
// draws pi's own startup is the panel promising something that never arrives.
|
|
145
|
+
it("a startupSplash that is not the literal true projects to 'Off', like the gate reads it", () => {
|
|
146
|
+
for (const value of ["true", 1, "yes", {}, null]) {
|
|
147
|
+
const items = getUISettingsItems({
|
|
148
|
+
...DEFAULT_UI_CONFIG,
|
|
149
|
+
startupSplash: value as never,
|
|
150
|
+
});
|
|
151
|
+
expect(
|
|
152
|
+
items.find((i) => i.id === "startupSplash")?.currentValue,
|
|
153
|
+
`startupSplash=${String(value)}`,
|
|
154
|
+
).toBe("Off");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
122
157
|
});
|
|
123
158
|
});
|
package/src/settings.ts
CHANGED
|
@@ -60,7 +60,7 @@ export function getUISettingsItems(config: UIConfig): SettingItem[] {
|
|
|
60
60
|
{
|
|
61
61
|
id: "labelText",
|
|
62
62
|
label: "Label Text",
|
|
63
|
-
description: "Text shown before thinking blocks",
|
|
63
|
+
description: "Text shown before thinking blocks, and on collapsed ones",
|
|
64
64
|
currentValue: config.labelText,
|
|
65
65
|
},
|
|
66
66
|
{
|
|
@@ -69,17 +69,30 @@ export function getUISettingsItems(config: UIConfig): SettingItem[] {
|
|
|
69
69
|
description: "RGB color for thinking label (e.g. 255,215,0)",
|
|
70
70
|
currentValue: config.labelColor,
|
|
71
71
|
},
|
|
72
|
+
{
|
|
73
|
+
id: "startupSplash",
|
|
74
|
+
label: "Startup Splash",
|
|
75
|
+
description:
|
|
76
|
+
"Archimedes startup splash animation (Off = pi's own startup screen, which is the default)",
|
|
77
|
+
// Strictly `=== true`, because that is how the gate reads it (`index.ts`):
|
|
78
|
+
// a hand-edited `"true"` or `1` is off on screen, so it must read as off
|
|
79
|
+
// here too. Every other boolean row can afford truthiness because every
|
|
80
|
+
// other boolean is *consumed* by truthiness.
|
|
81
|
+
currentValue: config.startupSplash === true ? "On" : "Off",
|
|
82
|
+
values: ["On", "Off"],
|
|
83
|
+
},
|
|
72
84
|
{
|
|
73
85
|
id: "animationStyle",
|
|
74
86
|
label: "Logo Animation",
|
|
75
|
-
description: "Splashscreen logo reveal style",
|
|
87
|
+
description: "Splashscreen logo reveal style (applies when Startup Splash is On)",
|
|
76
88
|
currentValue: config.animationStyle,
|
|
77
89
|
values: [...ANIMATION_STYLES],
|
|
78
90
|
},
|
|
79
91
|
{
|
|
80
92
|
id: "colorfulLogo",
|
|
81
93
|
label: "Colorful Logo",
|
|
82
|
-
description:
|
|
94
|
+
description:
|
|
95
|
+
"Use pi's brand colors for the splash logo (applies when Startup Splash is On)",
|
|
83
96
|
currentValue: config.colorfulLogo ? "On" : "Off",
|
|
84
97
|
values: ["On", "Off"],
|
|
85
98
|
},
|