@pi-archimedes/ui 2.10.4 → 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/config.test.ts
CHANGED
|
@@ -3,11 +3,13 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
3
3
|
vi.mock("@pi-archimedes/core/settings-io", () => ({
|
|
4
4
|
loadConfig: vi.fn(),
|
|
5
5
|
saveConfig: vi.fn(),
|
|
6
|
+
readEffectiveSettings: vi.fn(),
|
|
6
7
|
}));
|
|
7
8
|
|
|
8
9
|
import {
|
|
9
10
|
loadUIConfig,
|
|
10
11
|
saveUIConfig,
|
|
12
|
+
readQuietStartup,
|
|
11
13
|
DEFAULT_UI_CONFIG,
|
|
12
14
|
ANIMATION_STYLES,
|
|
13
15
|
THINKING_STYLE_VALUES,
|
|
@@ -17,7 +19,7 @@ import {
|
|
|
17
19
|
normalizeToolStyle,
|
|
18
20
|
SPIN_SPEED_MULT,
|
|
19
21
|
} from "./config.js";
|
|
20
|
-
import { loadConfig, saveConfig } from "@pi-archimedes/core/settings-io";
|
|
22
|
+
import { loadConfig, saveConfig, readEffectiveSettings } from "@pi-archimedes/core/settings-io";
|
|
21
23
|
|
|
22
24
|
describe("loadUIConfig", () => {
|
|
23
25
|
beforeEach(() => {
|
|
@@ -43,6 +45,7 @@ describe("loadUIConfig", () => {
|
|
|
43
45
|
codeUnindent: true,
|
|
44
46
|
labelText: "Thinking...",
|
|
45
47
|
labelColor: "255,215,0",
|
|
48
|
+
startupSplash: false,
|
|
46
49
|
animationStyle: "vertical-up",
|
|
47
50
|
colorfulLogo: false,
|
|
48
51
|
editorSpinBorder: true,
|
|
@@ -75,6 +78,18 @@ describe("saveUIConfig", () => {
|
|
|
75
78
|
saveUIConfig(config);
|
|
76
79
|
expect(saveConfig).toHaveBeenCalledWith("archimedes.ui", config);
|
|
77
80
|
});
|
|
81
|
+
|
|
82
|
+
// The splash toggle is saved by the same whole-object write as every other UI
|
|
83
|
+
// key, so `/archimedes` turning it on reaches settings.json as
|
|
84
|
+
// `archimedes.ui.startupSplash` — nothing strips an unknown key on the way.
|
|
85
|
+
it("persists startupSplash under archimedes.ui", () => {
|
|
86
|
+
const config = { ...DEFAULT_UI_CONFIG, startupSplash: true };
|
|
87
|
+
saveUIConfig(config);
|
|
88
|
+
expect(saveConfig).toHaveBeenCalledWith(
|
|
89
|
+
"archimedes.ui",
|
|
90
|
+
expect.objectContaining({ startupSplash: true }),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
78
93
|
});
|
|
79
94
|
|
|
80
95
|
describe("DEFAULT_UI_CONFIG", () => {
|
|
@@ -89,6 +104,7 @@ describe("DEFAULT_UI_CONFIG", () => {
|
|
|
89
104
|
codeUnindent: true,
|
|
90
105
|
labelText: "Thinking...",
|
|
91
106
|
labelColor: "255,215,0",
|
|
107
|
+
startupSplash: false,
|
|
92
108
|
animationStyle: "vertical-up",
|
|
93
109
|
colorfulLogo: false,
|
|
94
110
|
editorSpinBorder: true,
|
|
@@ -98,6 +114,12 @@ describe("DEFAULT_UI_CONFIG", () => {
|
|
|
98
114
|
});
|
|
99
115
|
});
|
|
100
116
|
|
|
117
|
+
// The maintainer's call on the startup splash: keep the code, flip the
|
|
118
|
+
// default. pi's own startup is what a fresh install sees.
|
|
119
|
+
it("leaves the startup splash off, so pi's own startup is the default", () => {
|
|
120
|
+
expect(DEFAULT_UI_CONFIG.startupSplash).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
101
123
|
it("exposes a speed→multiplier map (slow/normal/fast = 1.5/1/0.6)", () => {
|
|
102
124
|
expect(SPIN_SPEED_MULT).toEqual({ slow: 1.5, normal: 1, fast: 0.6 });
|
|
103
125
|
});
|
|
@@ -199,3 +221,42 @@ describe("normalizeToolStyle", () => {
|
|
|
199
221
|
expect(normalizeToolStyle(true)).toBe("Minimal");
|
|
200
222
|
});
|
|
201
223
|
});
|
|
224
|
+
|
|
225
|
+
// ── readQuietStartup ─────────────────────────────────────────────────
|
|
226
|
+
//
|
|
227
|
+
// pi 1.x's `quietStartup` is what decides how much of the startup screen the
|
|
228
|
+
// splash may draw: `false` = header + details, `"header"` = header alone,
|
|
229
|
+
// `true` = nothing. These are pi's own semantics, mirrored — a value that is
|
|
230
|
+
// not one of the three (including an absent key, whose pi default is `false`)
|
|
231
|
+
// reads as `false`.
|
|
232
|
+
|
|
233
|
+
describe("readQuietStartup", () => {
|
|
234
|
+
beforeEach(() => {
|
|
235
|
+
vi.clearAllMocks();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const answering = (settings: unknown) => {
|
|
239
|
+
vi.mocked(readEffectiveSettings).mockReturnValue(settings as Record<string, unknown>);
|
|
240
|
+
return readQuietStartup({});
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
it("reads each of the three values pi accepts", () => {
|
|
244
|
+
expect(answering({ quietStartup: false })).toBe(false);
|
|
245
|
+
expect(answering({ quietStartup: true })).toBe(true);
|
|
246
|
+
expect(answering({ quietStartup: "header" })).toBe("header");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("reads false when the key is absent or unusable", () => {
|
|
250
|
+
expect(answering({})).toBe(false);
|
|
251
|
+
expect(answering({ quietStartup: "false" })).toBe(false);
|
|
252
|
+
expect(answering({ quietStartup: 0 })).toBe(false);
|
|
253
|
+
expect(answering({ quietStartup: null })).toBe(false);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("passes the pi handle through — getSettings() lives on ExtensionAPI, not the context", () => {
|
|
257
|
+
const pi = {};
|
|
258
|
+
vi.mocked(readEffectiveSettings).mockReturnValue({});
|
|
259
|
+
readQuietStartup(pi);
|
|
260
|
+
expect(readEffectiveSettings).toHaveBeenCalledWith(pi);
|
|
261
|
+
});
|
|
262
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadConfig, saveConfig } from "@pi-archimedes/core/settings-io";
|
|
1
|
+
import { loadConfig, saveConfig, readEffectiveSettings } from "@pi-archimedes/core/settings-io";
|
|
2
2
|
|
|
3
3
|
export const ANIMATION_STYLES = [
|
|
4
4
|
"diagonal",
|
|
@@ -86,6 +86,12 @@ export interface UIConfig {
|
|
|
86
86
|
codeUnindent: boolean;
|
|
87
87
|
labelText: string;
|
|
88
88
|
labelColor: string;
|
|
89
|
+
/**
|
|
90
|
+
* Opt in to the Archimedes startup splash (logo reveal + `[Version]`/`[Models]`
|
|
91
|
+
* rows). Default **off**: pi owns the startup screen unless the user asks.
|
|
92
|
+
* See the truth table in `index.ts` for how this and `quietStartup` interact.
|
|
93
|
+
*/
|
|
94
|
+
startupSplash: boolean;
|
|
89
95
|
animationStyle: AnimationStyle;
|
|
90
96
|
colorfulLogo: boolean;
|
|
91
97
|
editorSpinBorder: boolean;
|
|
@@ -106,6 +112,9 @@ export const DEFAULT_UI_CONFIG: UIConfig = {
|
|
|
106
112
|
codeUnindent: true,
|
|
107
113
|
labelText: "Thinking...",
|
|
108
114
|
labelColor: "255,215,0",
|
|
115
|
+
// Off by default: the splash stays in the codebase, pi's own startup is what
|
|
116
|
+
// a user sees unless they turn it on (`/archimedes` → Startup Splash).
|
|
117
|
+
startupSplash: false,
|
|
109
118
|
animationStyle: "vertical-up",
|
|
110
119
|
colorfulLogo: false,
|
|
111
120
|
editorSpinBorder: true,
|
|
@@ -126,6 +135,41 @@ export function loadUIConfig(): UIConfig {
|
|
|
126
135
|
return loadConfig(NAMESPACE, DEFAULT_UI_CONFIG);
|
|
127
136
|
}
|
|
128
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Write the whole `archimedes.ui` namespace (core's `saveConfig` replaces the
|
|
140
|
+
* section, it does not merge into it), so every key the caller holds lands —
|
|
141
|
+
* including any this package has not heard of. A new field therefore needs no
|
|
142
|
+
* handling here: it arrives on the `UIConfig` the `/archimedes` panel hands
|
|
143
|
+
* back, and `loadUIConfig()` reads it back merged over `DEFAULT_UI_CONFIG` (an
|
|
144
|
+
* absent key means the default, which is how `startupSplash: false` works for a
|
|
145
|
+
* settings file written before the key existed).
|
|
146
|
+
*/
|
|
129
147
|
export function saveUIConfig(config: UIConfig): void {
|
|
130
148
|
saveConfig(NAMESPACE, config);
|
|
131
149
|
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* pi's own `quietStartup`, normalised to the three answers pi acts on.
|
|
153
|
+
*
|
|
154
|
+
* pi's semantics (`shouldShowStartupHeader()` / `shouldShowStartupDetails()` in
|
|
155
|
+
* its interactive mode): the startup header is hidden only by `true`; the
|
|
156
|
+
* startup *details* — the `Model scope:` line and the loaded-resource listing —
|
|
157
|
+
* are shown only by `false`. `"header"` is the middle case: keep the header,
|
|
158
|
+
* drop the details. Anything else, including an absent key (pi's default is
|
|
159
|
+
* `false`), reads as `false`.
|
|
160
|
+
*
|
|
161
|
+
* Read through `readEffectiveSettings`, so a project-scoped `quietStartup` and
|
|
162
|
+
* any in-process override count — the same object pi applies. One input is out
|
|
163
|
+
* of reach: pi's verbose-startup CLI flag overrides the setting for pi's own
|
|
164
|
+
* rendering and is not exposed to extensions, so on such a run the splash is
|
|
165
|
+
* quieter than pi's output. Taking the `pi` handle (not `ctx`) is required —
|
|
166
|
+
* `getSettings()` lives on `ExtensionAPI`.
|
|
167
|
+
*
|
|
168
|
+
* Only consulted when `startupSplash` is on (see the gate in `index.ts`): with
|
|
169
|
+
* the splash off this package installs no header at all and has no use for the
|
|
170
|
+
* answer.
|
|
171
|
+
*/
|
|
172
|
+
export function readQuietStartup(pi: unknown): boolean | "header" {
|
|
173
|
+
const value = (readEffectiveSettings(pi) as { quietStartup?: unknown }).quietStartup;
|
|
174
|
+
return value === true ? true : value === "header" ? "header" : false;
|
|
175
|
+
}
|
package/src/editor/index.test.ts
CHANGED
|
@@ -20,7 +20,11 @@ const widthProbe = vi.hoisted(() => ({
|
|
|
20
20
|
probe: (s: string): number => s.length,
|
|
21
21
|
}));
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// `foregroundAnsi` comes from the real module: `core/chrome.ts` asks the host's
|
|
24
|
+
// own colour emitter to turn a token's `Color` into a foreground escape, so
|
|
25
|
+
// stubbing it here would make the slab-colour assertions below tautological.
|
|
26
|
+
vi.mock("@earendil-works/pi-tui", async (importOriginal) => ({
|
|
27
|
+
...(await importOriginal<typeof import("@earendil-works/pi-tui")>()),
|
|
24
28
|
truncateToWidth: (s: string, _w: number, _pad?: string, _incl?: boolean) => s,
|
|
25
29
|
isKeyRelease: () => false,
|
|
26
30
|
visibleWidth: (s: string) => widthProbe.probe!(s),
|
|
@@ -95,6 +99,8 @@ const stubTheme = {
|
|
|
95
99
|
|
|
96
100
|
interface ConstructOpts {
|
|
97
101
|
spin?: boolean;
|
|
102
|
+
/** The theme `getTheme()` hands the palette resolver; defaults to `stubTheme`. */
|
|
103
|
+
theme?: Theme;
|
|
98
104
|
/** The `editorSpinSpeed` setting; × multiplies the style's native per-tick tempo (× 1.5 / × 1 / × 0.6), 32 ms tick floor. */
|
|
99
105
|
spinSpeed?: "slow" | "normal" | "fast";
|
|
100
106
|
/** The `editorSpinStyle` setting (raw setting string tolerated; a style not registered in `SPIN_VARIANTS` yet (batch 4) normalizes to typing frames — the fallback, which stays typing even though the default setting is now pendulum). */
|
|
@@ -123,7 +129,7 @@ function makeEditor(opts: ConstructOpts = {}): {
|
|
|
123
129
|
? opts.onSpinInterval
|
|
124
130
|
: vi.fn();
|
|
125
131
|
const editor = new HephaestusEditor(tui, editorTheme, keybindings, {
|
|
126
|
-
getTheme: () => stubTheme,
|
|
132
|
+
getTheme: () => opts.theme ?? stubTheme,
|
|
127
133
|
isIdle: () => (seq.length > 0 ? (seq.shift() as boolean) : true),
|
|
128
134
|
shutdown: vi.fn(),
|
|
129
135
|
spin: opts.spin,
|
|
@@ -818,3 +824,113 @@ describe("EAW terminal fallback", () => {
|
|
|
818
824
|
}
|
|
819
825
|
});
|
|
820
826
|
});
|
|
827
|
+
|
|
828
|
+
// ── 6. Edge rows: a cushion in the panel's own colour, or nothing at all ───
|
|
829
|
+
|
|
830
|
+
describe("editor edge rows", () => {
|
|
831
|
+
/**
|
|
832
|
+
* The shape pi 1.2.0's DEFAULT `system` theme answers with on a terminal that
|
|
833
|
+
* did not report a background (`indexedColors` in pi's system-theme.js gives
|
|
834
|
+
* every panel token `""`, and the neutral foreground tokens `""` plus SGR 2):
|
|
835
|
+
* measured against the installed package, not written from the prose —
|
|
836
|
+
* `getBgAnsi("userMessageBg")` is `"\x1b[49m"` and `getFgAnsi("borderMuted")`
|
|
837
|
+
* is `"\x1b[39m\x1b[2m"`. `resolvePalette` reads both as *no colour*, so the
|
|
838
|
+
* palette hands the editor `panelBg: ""` / `panelEdge: ""`.
|
|
839
|
+
*/
|
|
840
|
+
const terminalDefaultTheme = {
|
|
841
|
+
appearance: "dark",
|
|
842
|
+
fg: (_key: string, text: string) => text,
|
|
843
|
+
getBgAnsi: () => "\x1b[49m",
|
|
844
|
+
getFgAnsi: () => "\x1b[39m\x1b[2m",
|
|
845
|
+
} as unknown as Theme;
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* A theme that gives the panel a tint, shaped like the installed dark theme:
|
|
849
|
+
* `userMessageBg: "#213b49"` — so `theme.colors.userMessageBg` is that rgb —
|
|
850
|
+
* and a *different* `borderMuted: "#768186"`. Having the two colours far apart
|
|
851
|
+
* is the point: the slab must pick the panel's own, or the editor grows a grey
|
|
852
|
+
* rule hugging the text area.
|
|
853
|
+
*/
|
|
854
|
+
const tintedTheme = {
|
|
855
|
+
appearance: "dark",
|
|
856
|
+
fg: (_key: string, text: string) => text,
|
|
857
|
+
getBgAnsi: (key: string) =>
|
|
858
|
+
key === "userMessageBg" ? "\x1b[48;2;33;59;73m" : "\x1b[49m",
|
|
859
|
+
getFgAnsi: (key: string) =>
|
|
860
|
+
key === "borderMuted" ? "\x1b[38;2;118;129;134m" : "\x1b[39m",
|
|
861
|
+
colors: { userMessageBg: { kind: "rgb", r: 33, g: 59, b: 73 } },
|
|
862
|
+
getColorMode: () => "truecolor",
|
|
863
|
+
} as unknown as Theme;
|
|
864
|
+
|
|
865
|
+
/** The panel's own tint and the theme's border, as foreground escapes. */
|
|
866
|
+
const PANEL_TINT = "\x1b[38;2;33;59;73m";
|
|
867
|
+
const BORDER_MUTED = "\x1b[38;2;118;129;134m";
|
|
868
|
+
|
|
869
|
+
const SLABS = ["▁", "▔"];
|
|
870
|
+
|
|
871
|
+
it("paints no slab glyphs when the palette has no edge colour", () => {
|
|
872
|
+
// `panelEdge: ""` documents "no edge". Prepending nothing to a row of `▁`
|
|
873
|
+
// does not remove the row: the glyphs then paint in the terminal's OWN
|
|
874
|
+
// foreground, so the default theme on a dark terminal got bright bars above
|
|
875
|
+
// and below the editor — the opposite of "no colour".
|
|
876
|
+
const { editor } = makeEditor({ theme: terminalDefaultTheme });
|
|
877
|
+
const lines = editor.render(60);
|
|
878
|
+
for (const line of lines) {
|
|
879
|
+
for (const slab of SLABS) {
|
|
880
|
+
expect(line, JSON.stringify(line)).not.toContain(slab);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it("blanks the rows when the panel IS tinted but the edge has no colour", () => {
|
|
886
|
+
// The realistic degradation, not the empty-theme case: a modern theme (so a
|
|
887
|
+
// real panel tint) on a host that cannot render a colour as a foreground.
|
|
888
|
+
// `panelEdge` is "" while `panelBg` is a colour, so what these rows do is
|
|
889
|
+
// decided by the edge alone — painting on the body's colour instead would
|
|
890
|
+
// put a truecolour background behind the glyphs, and dropping the rows
|
|
891
|
+
// outright would break the frame's height.
|
|
892
|
+
const { colors: _noColors, getColorMode: _noMode, ...noForeground } =
|
|
893
|
+
tintedTheme as unknown as Record<string, unknown>;
|
|
894
|
+
const lines = makeEditor({
|
|
895
|
+
theme: noForeground as unknown as Theme,
|
|
896
|
+
}).editor.render(60);
|
|
897
|
+
expect(plain(lines[0]!)).toBe(" ".repeat(60));
|
|
898
|
+
expect(plain(lines[lines.length - 1]!)).toBe(" ".repeat(60));
|
|
899
|
+
for (const slab of SLABS) {
|
|
900
|
+
expect(lines[0]!).not.toContain(slab);
|
|
901
|
+
expect(lines[lines.length - 1]!).not.toContain(slab);
|
|
902
|
+
}
|
|
903
|
+
// The body kept its tint: only the cushion gave up.
|
|
904
|
+
expect(lines[1]).toContain("\x1b[48;2;33;59;73m");
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
it("paints the slab rows in the panel's OWN colour, so they vanish into the panel", () => {
|
|
908
|
+
const { editor } = makeEditor({ theme: tintedTheme });
|
|
909
|
+
const lines = editor.render(60);
|
|
910
|
+
// The slab sits directly against the panel body, so painted in the panel's
|
|
911
|
+
// own tint it is invisible by construction — the colour cushion it was
|
|
912
|
+
// invented for.
|
|
913
|
+
expect(lines[0]).toContain(PANEL_TINT);
|
|
914
|
+
expect(lines[lines.length - 1]).toContain(PANEL_TINT);
|
|
915
|
+
// And NOT in the border token, which is what turned the cushion into a
|
|
916
|
+
// visible grey rule hugging the text area.
|
|
917
|
+
expect(lines[0]).not.toContain(BORDER_MUTED);
|
|
918
|
+
expect(lines[lines.length - 1]).not.toContain(BORDER_MUTED);
|
|
919
|
+
// The colour source is not allowed to move anything: same glyphs, same run.
|
|
920
|
+
expect(plain(lines[0]!)).toBe("▁".repeat(60));
|
|
921
|
+
expect(plain(lines[lines.length - 1]!)).toBe("▔".repeat(60));
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
it("keeps the row count identical either way (blank row, not a removed row)", () => {
|
|
925
|
+
const painted = makeEditor({ theme: tintedTheme }).editor.render(60);
|
|
926
|
+
const blank = makeEditor({ theme: terminalDefaultTheme }).editor.render(60);
|
|
927
|
+
// The frame's height is a layout contract: the edge rows go blank, they do
|
|
928
|
+
// not disappear.
|
|
929
|
+
expect(blank).toHaveLength(painted.length);
|
|
930
|
+
expect(plain(blank[0]!)).toBe(" ".repeat(60));
|
|
931
|
+
expect(plain(blank[blank.length - 1]!)).toBe(" ".repeat(60));
|
|
932
|
+
// Which row carries the frame is part of the same contract.
|
|
933
|
+
expect(plain(painted[1]!).trim().startsWith("┌")).toBe(true);
|
|
934
|
+
expect(plain(blank[1]!).trim().startsWith("┌")).toBe(true);
|
|
935
|
+
});
|
|
936
|
+
});
|
package/src/editor/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
resolvePalette,
|
|
23
23
|
} from "@pi-archimedes/core/chrome";
|
|
24
24
|
import { isParentBorder, formatKey } from "@pi-archimedes/core/text";
|
|
25
|
+
import { diag, describeError } from "@pi-archimedes/core/log";
|
|
25
26
|
import { SPIN_INTERVALS, BorderTypeSpinner } from "./spin.js";
|
|
26
27
|
export { spinFrame } from "./spin.js";
|
|
27
28
|
import { pickQuip, QUIP_ROTATION_MAX_SECS, QUIP_ROTATION_MIN_SECS } from "./spin-quips.js";
|
|
@@ -82,6 +83,19 @@ export class HephaestusEditor extends CustomEditor {
|
|
|
82
83
|
onSpinInterval,
|
|
83
84
|
}: {
|
|
84
85
|
getTheme: () => Theme;
|
|
86
|
+
/**
|
|
87
|
+
* Whether the agent is idle right now — the only source of busy/idle
|
|
88
|
+
* state for the spinner and the quip. It is injected rather than read
|
|
89
|
+
* from a stored renderer so a screen-mode switch cannot strand it.
|
|
90
|
+
*
|
|
91
|
+
* pi 1.2.0 semantics, checked rather than assumed: `ctx.isIdle()` turns
|
|
92
|
+
* false at `before_agent_start`, i.e. when a run begins — including one
|
|
93
|
+
* an extension started with `pi.sendMessage(…, { triggerTurn: true })` —
|
|
94
|
+
* not when the first token arrives. That is the *Busy episode* boundary
|
|
95
|
+
* in `CONTEXT.md` (idle → working → idle), so the spinner starting on the
|
|
96
|
+
* run's first tick is what the term promises; there is no early-start
|
|
97
|
+
* bug to correct here.
|
|
98
|
+
*/
|
|
85
99
|
isIdle: () => boolean;
|
|
86
100
|
shutdown: () => void;
|
|
87
101
|
/** Type a 4-cell spinner window into the editor's top border while the agent is busy (the animation mechanism lives in `BorderTypeSpinner`, `./spin.js`): the style-configured 4-cell window (the gallery-derived styles in `SPIN_VARIANTS` — the 2×4 braille dot block (⠁ → ⣿, Unicode chart order, `typing`) grows cell-by-cell left→right — each cell walking the 8 stages in 2-step line pairs (⠁⠉/⠋⠛/⠟⠿/⡿⣿) — then holds, clears, repeats (in EAW terminals the stage set falls back to the width-1 shading ░ → █; raw setting strings are tolerated — unknown names normalize to typing frames). */
|
|
@@ -368,12 +382,29 @@ export class HephaestusEditor extends CustomEditor {
|
|
|
368
382
|
return p.panelBg + pad + patched + pad + RESET;
|
|
369
383
|
};
|
|
370
384
|
|
|
371
|
-
|
|
372
|
-
|
|
385
|
+
// The slab rows are the panel's own colour, extending its tint flush to
|
|
386
|
+
// the frame (see `resolvePalette`: the edge is the panel token asked for
|
|
387
|
+
// as a foreground, never a border token — a border colour here is a
|
|
388
|
+
// visible rule hugging the text area).
|
|
389
|
+
//
|
|
390
|
+
// No edge colour means the theme declined a panel colour entirely (a
|
|
391
|
+
// terminal-default token) or cannot answer its own token as a foreground.
|
|
392
|
+
// Either way an un-prefixed row of `▁`/`▔` would paint those glyphs in the
|
|
393
|
+
// terminal's own FOREGROUND — a hard, bright bar above and below the
|
|
394
|
+
// editor (what pi's default `system` theme on a terminal that reported no
|
|
395
|
+
// background produces). No colour, no slab: the rows go blank rather than
|
|
396
|
+
// disappearing, because the frame's height is a layout contract.
|
|
397
|
+
const edgeRow = (glyph: string): string =>
|
|
398
|
+
p.panelEdge ? p.panelEdge + glyph.repeat(width) + RESET : " ".repeat(width);
|
|
399
|
+
const topEdge = edgeRow("▁");
|
|
400
|
+
const botEdge = edgeRow("▔");
|
|
373
401
|
|
|
374
402
|
return [topEdge, ...raw.map(wrap), botEdge];
|
|
375
403
|
} catch (e) {
|
|
376
|
-
|
|
404
|
+
// Re-thrown, so pi's own error handling still sees it — and logged with
|
|
405
|
+
// its stack, because a console write here would land inside the frame
|
|
406
|
+
// being rendered and the log is the only record that survives.
|
|
407
|
+
diag("ui", `HephaestusEditor render error: ${describeError(e)}`);
|
|
377
408
|
throw e;
|
|
378
409
|
}
|
|
379
410
|
}
|
package/src/editor/spin-quips.ts
CHANGED
|
@@ -20,7 +20,7 @@ export const SPIN_QUIPS: readonly string[] = Object.freeze([
|
|
|
20
20
|
"Collapsing quantum wavefunctions just by looking at them...",
|
|
21
21
|
"Hunting for rogue gravitons inside the server rack...",
|
|
22
22
|
"Paging memory to disk because we refused to optimize...",
|
|
23
|
-
"Optimizing the optimizer so it can optimize
|
|
23
|
+
"Optimizing the optimizer so it can optimize optimially...",
|
|
24
24
|
"Searching the entire keyboard for the legendary 'Any' key...",
|
|
25
25
|
"Reticulating multi-dimensional splines...",
|
|
26
26
|
"Running gradient ascent until we reach enlightenment...",
|