pi-archimedes 2.8.0 → 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.
- package/README.md +5 -16
- package/package.json +16 -15
- package/src/config.ts +21 -7
- package/src/factory-lifecycle.test.ts +74 -25
- package/src/index.ts +36 -24
- package/src/onboarding/index.test.ts +391 -0
- package/src/onboarding/index.ts +80 -0
- package/src/onboarding/overlay.test.ts +503 -0
- package/src/onboarding/overlay.ts +255 -0
- package/src/plugins.test.ts +24 -10
- package/src/plugins.ts +5 -4
- package/src/settings.ts +25 -21
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
An extra pair of eyes on your code. Agents working in parallel. A terminal that keeps you in the loop—and looks good doing it.
|
|
5
5
|
|
|
6
|
-
**Archimedes brings subagents, shared task lists,
|
|
6
|
+
**Archimedes brings subagents, shared task lists, and a polished interface to [Pi](https://github.com/earendil-works/pi). Install them together, use what you like, and make the setup yours.**
|
|
7
7
|
|
|
8
8
|
[](https://www.npmjs.com/package/pi-archimedes)
|
|
9
9
|
[](https://nodejs.org)
|
|
@@ -23,7 +23,7 @@ One command:
|
|
|
23
23
|
pi install npm:pi-archimedes
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Your `~/.pi/agent/` stays as it is — Archimedes only adds its namespaces under `settings.json`. Pi's own `auth.json`, `keybindings.json`, agents, and sessions are untouched
|
|
26
|
+
Your `~/.pi/agent/` stays as it is — Archimedes only adds its namespaces under `settings.json`. Pi's own `auth.json`, `keybindings.json`, agents, and sessions are untouched.
|
|
27
27
|
|
|
28
28
|
Then run `/reload` in your session (or start a new one) to pick it up — that reloads the extensions *and* your keybindings, so any shortcuts you've customized in `~/.pi/agent/keybindings.json` keep working.
|
|
29
29
|
|
|
@@ -104,14 +104,6 @@ You don't have to copy messages between terminals to stay involved.
|
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
107
|
-
## Bring the tools you already use.
|
|
108
|
-
|
|
109
|
-
[Connect MCP servers](https://github.com/danielcherubini/pi-archimedes/blob/main/packages/mcp/README.md), browse their tools, and handle authentication inside Pi. Import server definitions from Cursor, Claude Code, Claude Desktop, or VS Code rather than rebuilding your setup.
|
|
110
|
-
|
|
111
|
-
Start with `/mcp setup`. Manage it with `/mcp`.
|
|
112
|
-
|
|
113
|
-
---
|
|
114
|
-
|
|
115
107
|
## See what changed. Not just that something changed.
|
|
116
108
|
|
|
117
109
|
[Syntax-highlighted diffs](https://github.com/danielcherubini/pi-archimedes/blob/main/packages/diff/README.md), side by side when there's room and unified when there isn't. Word-level highlights draw your eye to the changes inside each line.
|
|
@@ -150,7 +142,7 @@ You can leave the terminal to do its thing.
|
|
|
150
142
|
|
|
151
143
|
One install brings everything together. Switch optional extensions on or off with `/plugins`, then `/reload` to apply. Use `/archimedes` to adjust the available settings.
|
|
152
144
|
|
|
153
|
-
Only want the diffs
|
|
145
|
+
Only want the diffs or footer? Each component is available separately — see [Components](#components).
|
|
154
146
|
|
|
155
147
|
---
|
|
156
148
|
|
|
@@ -158,11 +150,10 @@ Only want the diffs, footer, or MCP tools? Each component is available separatel
|
|
|
158
150
|
|
|
159
151
|
| Command | Scope | Notes |
|
|
160
152
|
|---------|-------|-------|
|
|
161
|
-
| `/plugins` | Suite | Toggle the
|
|
153
|
+
| `/plugins` | Suite | Toggle the eleven optional extensions (core is always on and not toggleable). Toggles persist immediately; `/reload` (or a fresh session) applies them. |
|
|
162
154
|
| `/archimedes` | Suite | Interactive settings panel — up/down moves, left/right changes values, Enter edits supported fields, `s` saves, Esc discards the current edits. Settings captured at startup need `/reload`. Not every setting has a panel control. |
|
|
163
155
|
| `/agents` | Suite, subagent enabled | Browse, create, and edit custom subagent definitions in `.pi/agents/*.md`. |
|
|
164
156
|
| `/todos` | Todo component | Refreshes the todo widget and reports its status. `/todos clear` clears the list. (The board's visibility is not a `/todos` toggle — see the [todo docs](https://github.com/danielcherubini/pi-archimedes/blob/main/packages/todo/README.md).) |
|
|
165
|
-
| `/mcp`, `/mcp setup` | MCP component | Manage servers and run logins; the setup wizard scaffolds `.mcp.json` or imports configs from Cursor, Claude Code, Claude Desktop, or VS Code. |
|
|
166
157
|
| `/sudo`, `/sudo forget` | Sudo component | Inspect cached credential state; `forget` clears it. |
|
|
167
158
|
| `/reload` | Pi | Applies plugin changes and settings read at startup. |
|
|
168
159
|
|
|
@@ -170,7 +161,7 @@ Only want the diffs, footer, or MCP tools? Each component is available separatel
|
|
|
170
161
|
|
|
171
162
|
## Settings
|
|
172
163
|
|
|
173
|
-
Every component keeps its own namespace under `~/.pi/agent/settings.json`, which Pi parses as **strict JSON
|
|
164
|
+
Every component keeps its own namespace under `~/.pi/agent/settings.json`, which Pi parses as **strict JSON**. Each component's README documents its namespace, fields, and defaults — including [core](https://github.com/danielcherubini/pi-archimedes/blob/main/packages/core/README.md) (chrome, spinner, thinking), [footer](https://github.com/danielcherubini/pi-archimedes/blob/main/packages/footer/README.md), [diff](https://github.com/danielcherubini/pi-archimedes/blob/main/packages/diff/README.md), [notify](https://github.com/danielcherubini/pi-archimedes/blob/main/packages/notify/README.md), [image-paste](https://github.com/danielcherubini/pi-archimedes/blob/main/packages/image-paste/README.md), and [sudo](https://github.com/danielcherubini/pi-archimedes/blob/main/packages/sudo/README.md) (also strict JSON). The `/archimedes` panel covers the settings that have a control; not everything does.
|
|
174
165
|
|
|
175
166
|
---
|
|
176
167
|
|
|
@@ -182,7 +173,6 @@ Every component keeps its own namespace under `~/.pi/agent/settings.json`, which
|
|
|
182
173
|
| **Subagent** | [`@pi-archimedes/subagent`](https://www.npmjs.com/package/@pi-archimedes/subagent) | Live subagent dispatch, custom agent definitions; `/agents` editor with the suite |
|
|
183
174
|
| **Todo** | [`@pi-archimedes/todo`](https://www.npmjs.com/package/@pi-archimedes/todo) | Multi-column todo board with subagent columns and auto-clear |
|
|
184
175
|
| **Ask** | [`@pi-archimedes/ask`](https://www.npmjs.com/package/@pi-archimedes/ask) | Structured questions — including subagent questions relayed into your terminal |
|
|
185
|
-
| **MCP** | [`@pi-archimedes/mcp`](https://www.npmjs.com/package/@pi-archimedes/mcp) | `/mcp` management, setup wizard, OAuth, config imports |
|
|
186
176
|
| **Sudo** | [`@pi-archimedes/sudo`](https://www.npmjs.com/package/@pi-archimedes/sudo) | `sudo_exec` with masked password prompt and interactive-sudo guard |
|
|
187
177
|
| **Diff** | [`@pi-archimedes/diff`](https://www.npmjs.com/package/@pi-archimedes/diff) | Syntax-highlighted side-by-side and unified diffs with word-level highlights |
|
|
188
178
|
| **Footer** | [`@pi-archimedes/footer`](https://www.npmjs.com/package/@pi-archimedes/footer) | Branch, model, context usage, and token/cost status bar |
|
|
@@ -199,7 +189,6 @@ pi install npm:@pi-archimedes/core
|
|
|
199
189
|
pi install npm:@pi-archimedes/subagent
|
|
200
190
|
pi install npm:@pi-archimedes/todo
|
|
201
191
|
pi install npm:@pi-archimedes/ask
|
|
202
|
-
pi install npm:@pi-archimedes/mcp
|
|
203
192
|
pi install npm:@pi-archimedes/sudo
|
|
204
193
|
pi install npm:@pi-archimedes/diff
|
|
205
194
|
pi install npm:@pi-archimedes/footer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-archimedes",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/danielcherubini/pi-archimedes.git"
|
|
@@ -9,32 +9,33 @@
|
|
|
9
9
|
"keywords": [
|
|
10
10
|
"pi-package"
|
|
11
11
|
],
|
|
12
|
-
"description": "Parallel agents, shared task lists,
|
|
12
|
+
"description": "Parallel agents, shared task lists, and a polished terminal for the Pi coding agent.",
|
|
13
13
|
"files": [
|
|
14
14
|
"src",
|
|
15
15
|
"README.md"
|
|
16
16
|
],
|
|
17
17
|
"main": "./src/index.ts",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@pi-archimedes/core": "2.
|
|
20
|
-
"@pi-archimedes/footer": "2.
|
|
21
|
-
"@pi-archimedes/diff": "2.
|
|
22
|
-
"@pi-archimedes/ask": "2.
|
|
23
|
-
"@pi-archimedes/image-paste": "2.
|
|
24
|
-
"@pi-archimedes/subagent": "2.
|
|
25
|
-
"@pi-archimedes/
|
|
26
|
-
"@pi-archimedes/
|
|
27
|
-
"@pi-archimedes/session-name": "2.
|
|
28
|
-
"@pi-archimedes/
|
|
29
|
-
"@pi-archimedes/
|
|
19
|
+
"@pi-archimedes/core": "2.9.0",
|
|
20
|
+
"@pi-archimedes/footer": "2.9.0",
|
|
21
|
+
"@pi-archimedes/diff": "2.9.0",
|
|
22
|
+
"@pi-archimedes/ask": "2.9.0",
|
|
23
|
+
"@pi-archimedes/image-paste": "2.9.0",
|
|
24
|
+
"@pi-archimedes/subagent": "2.9.0",
|
|
25
|
+
"@pi-archimedes/todo": "2.9.0",
|
|
26
|
+
"@pi-archimedes/notify": "2.9.0",
|
|
27
|
+
"@pi-archimedes/session-name": "2.9.0",
|
|
28
|
+
"@pi-archimedes/sudo": "2.9.0",
|
|
29
|
+
"@pi-archimedes/ui": "2.9.0",
|
|
30
|
+
"@pi-archimedes/web": "2.9.0"
|
|
30
31
|
},
|
|
31
32
|
"peerDependencies": {
|
|
32
33
|
"@earendil-works/pi-coding-agent": ">=0.1.0",
|
|
33
34
|
"@earendil-works/pi-tui": ">=0.1.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
37
|
-
"@earendil-works/pi-tui": "^0.
|
|
37
|
+
"@earendil-works/pi-coding-agent": "^0.87.0",
|
|
38
|
+
"@earendil-works/pi-tui": "^0.87.0",
|
|
38
39
|
"typescript": "^6.0.0"
|
|
39
40
|
},
|
|
40
41
|
"pi": {
|
package/src/config.ts
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
// ── Re-export
|
|
1
|
+
// ── Re-export UI config ──────────────────────────────────────────────
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
loadCoreConfig,
|
|
5
|
-
saveCoreConfig,
|
|
6
|
-
DEFAULT_CORE_CONFIG,
|
|
7
|
-
ANIMATION_STYLES,
|
|
8
5
|
type CoreConfig,
|
|
9
6
|
} from "@pi-archimedes/core/config";
|
|
10
7
|
export {
|
|
11
8
|
loadCoreConfig,
|
|
12
|
-
saveCoreConfig,
|
|
13
|
-
DEFAULT_CORE_CONFIG,
|
|
14
|
-
ANIMATION_STYLES,
|
|
15
9
|
type CoreConfig,
|
|
16
10
|
} from "@pi-archimedes/core/config";
|
|
17
11
|
|
|
12
|
+
// ── Re-export UI config ──────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
loadUIConfig,
|
|
16
|
+
saveUIConfig,
|
|
17
|
+
DEFAULT_UI_CONFIG,
|
|
18
|
+
type UIConfig,
|
|
19
|
+
ANIMATION_STYLES,
|
|
20
|
+
} from "@pi-archimedes/ui/config";
|
|
21
|
+
export {
|
|
22
|
+
loadUIConfig,
|
|
23
|
+
saveUIConfig,
|
|
24
|
+
DEFAULT_UI_CONFIG,
|
|
25
|
+
type UIConfig,
|
|
26
|
+
ANIMATION_STYLES,
|
|
27
|
+
} from "@pi-archimedes/ui/config";
|
|
28
|
+
|
|
18
29
|
// ── Re-export footer config ────────────────────────────────────────────
|
|
19
30
|
|
|
20
31
|
import {
|
|
@@ -40,6 +51,7 @@ export const DEFAULT_DIFF_CONFIG: DiffConfig = {
|
|
|
40
51
|
diffTheme: "github-dark",
|
|
41
52
|
diffSplitMinWidth: 150,
|
|
42
53
|
diffSplitMinCodeWidth: 60,
|
|
54
|
+
diffSplitWrapCheck: true,
|
|
43
55
|
};
|
|
44
56
|
|
|
45
57
|
const NAMESPACE = "archimedes.diff";
|
|
@@ -71,6 +83,7 @@ export {
|
|
|
71
83
|
|
|
72
84
|
export function loadAllConfig(): {
|
|
73
85
|
core: CoreConfig;
|
|
86
|
+
ui: UIConfig;
|
|
74
87
|
footer: FooterConfig;
|
|
75
88
|
diff: DiffConfig;
|
|
76
89
|
notify: NotifyConfig;
|
|
@@ -78,6 +91,7 @@ export function loadAllConfig(): {
|
|
|
78
91
|
} {
|
|
79
92
|
return {
|
|
80
93
|
core: loadCoreConfig(),
|
|
94
|
+
ui: loadUIConfig(),
|
|
81
95
|
footer: loadFooterConfig(),
|
|
82
96
|
diff: loadDiffConfig(),
|
|
83
97
|
notify: loadNotifyConfig(),
|
|
@@ -53,6 +53,9 @@ vi.mock("@pi-archimedes/core/profiler", () => ({
|
|
|
53
53
|
}));
|
|
54
54
|
vi.mock("@pi-archimedes/core", () => ({
|
|
55
55
|
registerCore: vi.fn(),
|
|
56
|
+
}));
|
|
57
|
+
vi.mock("@pi-archimedes/ui", () => ({
|
|
58
|
+
registerUI: vi.fn(),
|
|
56
59
|
unpatchConsoleLog: vi.fn(),
|
|
57
60
|
}));
|
|
58
61
|
vi.mock("@pi-archimedes/footer", () => ({ registerFooter: vi.fn() }));
|
|
@@ -63,8 +66,8 @@ vi.mock("@pi-archimedes/session-name", () => ({ registerSessionName: vi.fn() }))
|
|
|
63
66
|
vi.mock("@pi-archimedes/sudo", () => ({ registerSudo: vi.fn() }));
|
|
64
67
|
|
|
65
68
|
// Dynamic imports done in the session_start handler — mock EXACTLY the
|
|
66
|
-
// properties index.ts uses via destructured `ipMod.*` / `diffMod` / `saMod`
|
|
67
|
-
//
|
|
69
|
+
// properties index.ts uses via destructured `ipMod.*` / `diffMod` / `saMod`
|
|
70
|
+
// access (import result objects, not named destructure).
|
|
68
71
|
vi.mock("@pi-archimedes/diff", () => ({
|
|
69
72
|
registerDiffTools: vi.fn(),
|
|
70
73
|
}));
|
|
@@ -80,14 +83,12 @@ vi.mock("@pi-archimedes/subagent", () => ({
|
|
|
80
83
|
registerSubagent: vi.fn(),
|
|
81
84
|
registerAgentsCommand: vi.fn(),
|
|
82
85
|
}));
|
|
83
|
-
vi.mock("@pi-archimedes/mcp", () => ({
|
|
84
|
-
registerMcp: vi.fn(),
|
|
85
|
-
}));
|
|
86
86
|
|
|
87
87
|
// Meta-local modules the factory imports — not under test here
|
|
88
88
|
vi.mock("./config.js", () => ({ loadDiffConfig: vi.fn(() => ({})) }));
|
|
89
89
|
vi.mock("./settings.js", () => ({ openSettings: vi.fn() }));
|
|
90
90
|
vi.mock("./plugin-manager.js", () => ({ registerPluginsCommand: vi.fn() }));
|
|
91
|
+
vi.mock("./onboarding/index.js", () => ({ runOnboarding: vi.fn(async () => {}) }));
|
|
91
92
|
|
|
92
93
|
// The real plugins.ts gate semantics are what these tests exercise
|
|
93
94
|
// (read via the mocked settings-io on each call → mutable mid-session).
|
|
@@ -95,9 +96,11 @@ const { default: metaFactory } = await import("./index.js");
|
|
|
95
96
|
|
|
96
97
|
const { registerImagePaste, shutdownImagePaste, initImagePasteSession } =
|
|
97
98
|
await import("@pi-archimedes/image-paste");
|
|
99
|
+
const { unpatchConsoleLog } = await import("@pi-archimedes/ui");
|
|
98
100
|
|
|
99
101
|
const { offerKeybindingFix } =
|
|
100
102
|
await import("@pi-archimedes/image-paste/keybinding-offer");
|
|
103
|
+
const { runOnboarding } = await import("./onboarding/index.js");
|
|
101
104
|
|
|
102
105
|
// ── Stub pi: record pi.on() registrations and command/tool registrations ───
|
|
103
106
|
|
|
@@ -132,10 +135,13 @@ function freshFactory(): {
|
|
|
132
135
|
harness: PiHarness;
|
|
133
136
|
startSession: (ctx: unknown) => Promise<unknown>;
|
|
134
137
|
shutdownSession: () => unknown;
|
|
135
|
-
/** Fire ONLY the
|
|
136
|
-
* before the lazy-load handler). Useful for isolating
|
|
137
|
-
* without triggering the heavy
|
|
138
|
-
|
|
138
|
+
/** Fire ONLY the first-run (merged) session_start handler (the one
|
|
139
|
+
* registered before the lazy-load handler). Useful for isolating the
|
|
140
|
+
* offer → onboarding sequencing without triggering the heavy
|
|
141
|
+
* dynamic-import path. */
|
|
142
|
+
fireFirstRunHandler: (ctx: unknown) => void;
|
|
143
|
+
/** Number of session_start handlers the factory registered. */
|
|
144
|
+
startRcCount: number;
|
|
139
145
|
} {
|
|
140
146
|
const harness = makePi();
|
|
141
147
|
metaFactory(harness.pi);
|
|
@@ -147,18 +153,20 @@ function freshFactory(): {
|
|
|
147
153
|
// The LAST session_start handler is the lazy-load one (existing tests rely on this).
|
|
148
154
|
const startRc = (startRcs[startRcs.length - 1] ?? expect.fail("no session_start handler")) as (...args: unknown[]) => unknown;
|
|
149
155
|
const shutdownRc = (shutdownRcs[shutdownRcs.length - 1] ?? expect.fail("no session_shutdown handler")) as (...args: unknown[]) => unknown;
|
|
150
|
-
// The
|
|
151
|
-
//
|
|
152
|
-
// package is un-mocked
|
|
153
|
-
|
|
156
|
+
// The first-run MERGED handler (offer + onboarding, sequenced) is at
|
|
157
|
+
// index 0 in this fully-mocked harness — all earlier package
|
|
158
|
+
// registrations are stubbed to no-op vi.fn(); if a package is un-mocked
|
|
159
|
+
// here, update the index.
|
|
160
|
+
const firstRunRc = (startRcs[0] ?? expect.fail("no first-run session_start handler")) as (...args: unknown[]) => unknown;
|
|
154
161
|
|
|
155
162
|
return {
|
|
156
163
|
harness,
|
|
157
164
|
startSession: (ctx: unknown) => startRc(undefined, ctx) as Promise<unknown>,
|
|
158
165
|
shutdownSession: () => shutdownRc(undefined, {}),
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
fireFirstRunHandler: (ctx: unknown) => {
|
|
167
|
+
firstRunRc(undefined, ctx);
|
|
161
168
|
},
|
|
169
|
+
startRcCount: startRcs.length,
|
|
162
170
|
};
|
|
163
171
|
}
|
|
164
172
|
|
|
@@ -182,6 +190,7 @@ describe("image-paste factory lifecycle (registration is config-gated, teardown
|
|
|
182
190
|
// Session ends → cleanup MUST still run for this session's registration.
|
|
183
191
|
shutdownSession();
|
|
184
192
|
expect(vi.mocked(shutdownImagePaste)).toHaveBeenCalledTimes(1);
|
|
193
|
+
expect(vi.mocked(unpatchConsoleLog)).toHaveBeenCalledTimes(1);
|
|
185
194
|
});
|
|
186
195
|
|
|
187
196
|
it("registers nothing and tears down nothing when config is off at session start", async () => {
|
|
@@ -214,30 +223,68 @@ describe("image-paste factory lifecycle (registration is config-gated, teardown
|
|
|
214
223
|
});
|
|
215
224
|
});
|
|
216
225
|
|
|
217
|
-
describe("
|
|
218
|
-
it("
|
|
226
|
+
describe("first-run sequencing (session_start → offerKeybindingFix, then runOnboarding)", () => {
|
|
227
|
+
it("registers exactly two session_start handlers (merged first-run + lazy-load)", () => {
|
|
228
|
+
const { startRcCount } = freshFactory();
|
|
229
|
+
// The keybinding offer and the onboarding are ONE merged handler — not
|
|
230
|
+
// two stacked first-run dialogs (which is what this guards against).
|
|
231
|
+
expect(startRcCount).toBe(2);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("calls offerKeybindingFix exactly once with the session ctx (before the onboarding)", () => {
|
|
219
235
|
const ctx = { ui: { theme: "dark" } };
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
236
|
+
// The merged handler awaits the offer; resolve false so the onboarding
|
|
237
|
+
// branch runs (a resolved promise, so nothing is left pending).
|
|
238
|
+
vi.mocked(offerKeybindingFix).mockResolvedValueOnce(false);
|
|
239
|
+
const { fireFirstRunHandler } = freshFactory();
|
|
223
240
|
|
|
224
|
-
|
|
225
|
-
fireOfferHandler(ctx);
|
|
241
|
+
fireFirstRunHandler(ctx);
|
|
226
242
|
|
|
227
243
|
expect(vi.mocked(offerKeybindingFix)).toHaveBeenCalledTimes(1);
|
|
228
244
|
expect(vi.mocked(offerKeybindingFix)).toHaveBeenCalledWith(ctx);
|
|
229
245
|
});
|
|
230
246
|
|
|
247
|
+
it("when offerKeybindingFix resolves true (reloaded), runOnboarding is NOT called", async () => {
|
|
248
|
+
// The offer triggered a reload: the reloaded session runs this handler
|
|
249
|
+
// again (where the offer is a no-op), so the onboarding must NOT run on
|
|
250
|
+
// the now-stale ctx.
|
|
251
|
+
vi.mocked(offerKeybindingFix).mockResolvedValueOnce(true);
|
|
252
|
+
const ctx = { ui: { theme: "dark" } };
|
|
253
|
+
const { fireFirstRunHandler } = freshFactory();
|
|
254
|
+
|
|
255
|
+
fireFirstRunHandler(ctx);
|
|
256
|
+
// Drain the microtask queue so the awaited offer has resolved.
|
|
257
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
258
|
+
|
|
259
|
+
expect(vi.mocked(offerKeybindingFix)).toHaveBeenCalledTimes(1);
|
|
260
|
+
expect(vi.mocked(runOnboarding)).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("when offerKeybindingFix resolves false, runOnboarding IS called (fire-and-forget)", async () => {
|
|
264
|
+
vi.mocked(offerKeybindingFix).mockResolvedValueOnce(false);
|
|
265
|
+
const ctx = { ui: { theme: "dark" } };
|
|
266
|
+
const { fireFirstRunHandler } = freshFactory();
|
|
267
|
+
|
|
268
|
+
fireFirstRunHandler(ctx);
|
|
269
|
+
// Drain the microtask queue so the awaited offer has resolved and the
|
|
270
|
+
// (fire-and-forget) onboarding has been kicked off.
|
|
271
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
272
|
+
|
|
273
|
+
expect(vi.mocked(offerKeybindingFix)).toHaveBeenCalledTimes(1);
|
|
274
|
+
expect(vi.mocked(runOnboarding)).toHaveBeenCalledTimes(1);
|
|
275
|
+
expect(vi.mocked(runOnboarding)).toHaveBeenCalledWith(ctx);
|
|
276
|
+
});
|
|
277
|
+
|
|
231
278
|
it("session_start resolves even when offerKeybindingFix rejects (fire-and-forget safety)", async () => {
|
|
232
279
|
// Arrange: make the mock reject once to simulate an unexpected failure.
|
|
233
280
|
vi.mocked(offerKeybindingFix).mockRejectedValueOnce(new Error("boom"));
|
|
234
281
|
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
235
282
|
|
|
236
283
|
const ctx = { ui: { theme: "dark" } };
|
|
237
|
-
const {
|
|
284
|
+
const { fireFirstRunHandler } = freshFactory();
|
|
238
285
|
|
|
239
|
-
// Act: invoke the
|
|
240
|
-
expect(() =>
|
|
286
|
+
// Act: invoke the handler; it must not throw synchronously.
|
|
287
|
+
expect(() => fireFirstRunHandler(ctx)).not.toThrow();
|
|
241
288
|
|
|
242
289
|
// Drain the microtask queue so the .catch() branch has had time to run.
|
|
243
290
|
await new Promise((r) => setTimeout(r, 0));
|
|
@@ -247,6 +294,8 @@ describe("keybinding-offer wiring (session_start → offerKeybindingFix, fire-an
|
|
|
247
294
|
"[archimedes] keybinding offer failed:",
|
|
248
295
|
expect.any(Error),
|
|
249
296
|
);
|
|
297
|
+
// A failed offer must not block the onboarding (reloaded stays false).
|
|
298
|
+
expect(vi.mocked(runOnboarding)).toHaveBeenCalledTimes(1);
|
|
250
299
|
|
|
251
300
|
consoleSpy.mockRestore();
|
|
252
301
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import { time as archTime, print as archPrintTimings, reset as archResetTimings } from "@pi-archimedes/core/profiler";
|
|
4
|
-
import { registerCore
|
|
4
|
+
import { registerCore } from "@pi-archimedes/core";
|
|
5
|
+
import { registerUI, unpatchConsoleLog } from "@pi-archimedes/ui";
|
|
5
6
|
import { registerFooter } from "@pi-archimedes/footer";
|
|
6
7
|
|
|
7
8
|
// Mark when module finishes evaluating (before factory runs) for gap analysis
|
|
@@ -15,9 +16,10 @@ import { registerNotify } from "@pi-archimedes/notify";
|
|
|
15
16
|
import { registerSessionName } from "@pi-archimedes/session-name";
|
|
16
17
|
import { registerSudo } from "@pi-archimedes/sudo";
|
|
17
18
|
// Light module from the image-paste package (no heavy deps) — the offer
|
|
18
|
-
// gates are self-contained, so it is statically imported and
|
|
19
|
-
//
|
|
19
|
+
// gates are self-contained, so it is statically imported and sequenced in
|
|
20
|
+
// the first-run session_start handler below, before the lazy-load.
|
|
20
21
|
import { offerKeybindingFix } from "@pi-archimedes/image-paste/keybinding-offer";
|
|
22
|
+
import { runOnboarding } from "./onboarding/index.js";
|
|
21
23
|
import { loadDiffConfig } from "./config.js";
|
|
22
24
|
import { openSettings } from "./settings.js"
|
|
23
25
|
import { registerPluginsCommand } from "./plugin-manager.js"
|
|
@@ -39,6 +41,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
39
41
|
// Register all component extensions (static imports already compiled by jiti above)
|
|
40
42
|
registerCore(pi);
|
|
41
43
|
archTime("registerCore");
|
|
44
|
+
if (isPluginEnabled("ui")) {
|
|
45
|
+
registerUI(pi);
|
|
46
|
+
archTime("registerUI");
|
|
47
|
+
}
|
|
42
48
|
if (isPluginEnabled("footer")) registerFooter(pi);
|
|
43
49
|
archTime("registerFooter");
|
|
44
50
|
|
|
@@ -79,31 +85,34 @@ export default function (pi: ExtensionAPI): void {
|
|
|
79
85
|
archPrintTimings();
|
|
80
86
|
});
|
|
81
87
|
|
|
82
|
-
// First-run
|
|
83
|
-
// packages/image-paste/src/keybinding-offer.ts
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// → confirm) are self-contained, so ordering with that handler is not a
|
|
87
|
-
// correctness issue.
|
|
88
|
+
// First-run flows, sequenced in ONE handler (see the module docs in
|
|
89
|
+
// packages/image-paste/src/keybinding-offer.ts and meta/src/onboarding/
|
|
90
|
+
// index.ts): the keybinding offer (if any) fully resolves BEFORE the
|
|
91
|
+
// onboarding overlay shows — no stacked first-run dialogs.
|
|
88
92
|
//
|
|
89
93
|
// Deliberately NOT gated with isPluginEnabled("image-paste"): that call
|
|
90
94
|
// resolves to isConfigEnabled("archimedes.imagePaste") (ADR 0012), which
|
|
91
95
|
// is exactly the same key the module's own gate 1 already checks — adding
|
|
92
96
|
// a wrapper here would be a redundant duplicate of the identical check.
|
|
93
|
-
// This handler runs unconditionally on every session_start; the
|
|
94
|
-
// five gates make it a no-op when not
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
// throw is logged, never propagated.
|
|
97
|
+
// This handler runs unconditionally on every session_start; the offer's
|
|
98
|
+
// five gates (and the onboarding's two) make it a no-op when not
|
|
99
|
+
// applicable.
|
|
100
|
+
pi.on("session_start", async (_event, ctx: ExtensionContext) => {
|
|
101
|
+
// The offer may block on a user confirm; it must never fail session
|
|
102
|
+
// startup — any throw is logged, never propagated.
|
|
103
|
+
let reloaded = false;
|
|
99
104
|
try {
|
|
100
|
-
|
|
101
|
-
console.error("[archimedes] keybinding offer failed:", e);
|
|
102
|
-
});
|
|
105
|
+
reloaded = await offerKeybindingFix(ctx);
|
|
103
106
|
} catch (e) {
|
|
104
107
|
console.error("[archimedes] keybinding offer failed:", e);
|
|
105
108
|
}
|
|
109
|
+
// If the offer triggered a reload, the fresh session runs this handler
|
|
110
|
+
// again (where the offer is a no-op — keybindings.json now exists), so
|
|
111
|
+
// do NOT run the onboarding on the now-stale ctx.
|
|
112
|
+
if (reloaded) return;
|
|
113
|
+
void runOnboarding(ctx).catch((err) => console.error("[archimedes] onboarding failed:", err));
|
|
106
114
|
});
|
|
115
|
+
archTime("first-run handler");
|
|
107
116
|
|
|
108
117
|
pi.on("session_start", async (_event, ctx: ExtensionContext) => {
|
|
109
118
|
archTime(`session_start (factory was ${Date.now() - _moduleEvalAt}ms ago)`);
|
|
@@ -111,8 +120,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
111
120
|
// Update module-level context ref so lazy-loaded callbacks always see current session
|
|
112
121
|
currentCtx = ctx;
|
|
113
122
|
|
|
114
|
-
// ── Parallel lazy-load all
|
|
115
|
-
const [diffMod, ipMod, saMod,
|
|
123
|
+
// ── Parallel lazy-load all four packages (saves ~100ms vs sequential) ──
|
|
124
|
+
const [diffMod, ipMod, saMod, webMod] = await Promise.all([
|
|
116
125
|
isPluginEnabled("diff")
|
|
117
126
|
? import("@pi-archimedes/diff").catch((e) => { console.error("[archimedes] diff load failed:", e); return null; })
|
|
118
127
|
: Promise.resolve(null),
|
|
@@ -122,8 +131,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
122
131
|
isPluginEnabled("subagent")
|
|
123
132
|
? import("@pi-archimedes/subagent").catch((e) => { console.error("[archimedes] subagent load failed:", e); return null; })
|
|
124
133
|
: Promise.resolve(null),
|
|
125
|
-
isPluginEnabled("
|
|
126
|
-
? import("@pi-archimedes/
|
|
134
|
+
isPluginEnabled("web")
|
|
135
|
+
? import("@pi-archimedes/web").catch((e) => {
|
|
136
|
+
console.error("[archimedes] web load failed:", e);
|
|
137
|
+
return null;
|
|
138
|
+
})
|
|
127
139
|
: Promise.resolve(null),
|
|
128
140
|
]);
|
|
129
141
|
archTime("4 packages loaded in parallel");
|
|
@@ -151,8 +163,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
151
163
|
saMod.registerSubagent(pi);
|
|
152
164
|
saMod.registerAgentsCommand(pi);
|
|
153
165
|
}
|
|
154
|
-
if (
|
|
155
|
-
|
|
166
|
+
if (webMod) {
|
|
167
|
+
webMod.registerWeb(pi);
|
|
156
168
|
}
|
|
157
169
|
});
|
|
158
170
|
|