pi-archimedes 2.6.3 → 2.7.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 +2 -2
- package/package.json +16 -12
- package/src/factory-lifecycle.test.ts +64 -0
- package/src/index.ts +30 -0
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ Then run `/reload` in your session (or start a new one) to pick it up — that r
|
|
|
64
64
|
}
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
If it already exists, just add/set `"app.clipboard.pasteImage": []` in there and run `/reload`.
|
|
67
|
+
If it already exists, just add/set `"app.clipboard.pasteImage": []` in there and run `/reload`. Or skip the step entirely if the file doesn't exist yet: on the first TUI session the suite offers to create it automatically with the snippet above — accepting creates it and reloads the terminal automatically (the cleared binding applies immediately); declining or cancelling (Esc) never asks again.
|
|
68
68
|
|
|
69
69
|
`/login` signs you into a supported provider (subscription or API key) and `/model` selects a model from it. Model access comes through the providers you configure in Pi — Pi's [provider docs](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/providers.md) list the supported ones, and Archimedes doesn't ship a model of its own. For the broader first run, Pi's [quickstart](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/quickstart.md) is worth a read.
|
|
70
70
|
|
|
@@ -170,7 +170,7 @@ Only want the diffs, footer, or MCP tools? Each component is available separatel
|
|
|
170
170
|
|
|
171
171
|
## Settings
|
|
172
172
|
|
|
173
|
-
Every component keeps its own namespace under `~/.pi/agent/settings.json`, which Pi parses as **strict JSON** (no comments — unlike MCP server configs, which accept JSONC). 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), [mcp](https://github.com/danielcherubini/pi-archimedes/blob/main/packages/mcp/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.
|
|
173
|
+
Every component keeps its own namespace under `~/.pi/agent/settings.json`, which Pi parses as **strict JSON** (no comments — unlike MCP server configs, which accept JSONC). 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), [mcp](https://github.com/danielcherubini/pi-archimedes/blob/main/packages/mcp/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
174
|
|
|
175
175
|
---
|
|
176
176
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-archimedes",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.1",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/danielcherubini/pi-archimedes.git"
|
|
7
|
+
},
|
|
4
8
|
"type": "module",
|
|
5
9
|
"keywords": [
|
|
6
10
|
"pi-package"
|
|
@@ -12,17 +16,17 @@
|
|
|
12
16
|
],
|
|
13
17
|
"main": "./src/index.ts",
|
|
14
18
|
"dependencies": {
|
|
15
|
-
"@pi-archimedes/core": "2.
|
|
16
|
-
"@pi-archimedes/
|
|
17
|
-
"@pi-archimedes/
|
|
18
|
-
"@pi-archimedes/image-paste": "2.
|
|
19
|
-
"@pi-archimedes/ask": "2.
|
|
20
|
-
"@pi-archimedes/subagent": "2.
|
|
21
|
-
"@pi-archimedes/notify": "2.
|
|
22
|
-
"@pi-archimedes/todo": "2.
|
|
23
|
-
"@pi-archimedes/mcp": "2.
|
|
24
|
-
"@pi-archimedes/
|
|
25
|
-
"@pi-archimedes/
|
|
19
|
+
"@pi-archimedes/core": "2.7.1",
|
|
20
|
+
"@pi-archimedes/footer": "2.7.1",
|
|
21
|
+
"@pi-archimedes/diff": "2.7.1",
|
|
22
|
+
"@pi-archimedes/image-paste": "2.7.1",
|
|
23
|
+
"@pi-archimedes/ask": "2.7.1",
|
|
24
|
+
"@pi-archimedes/subagent": "2.7.1",
|
|
25
|
+
"@pi-archimedes/notify": "2.7.1",
|
|
26
|
+
"@pi-archimedes/todo": "2.7.1",
|
|
27
|
+
"@pi-archimedes/mcp": "2.7.1",
|
|
28
|
+
"@pi-archimedes/session-name": "2.7.1",
|
|
29
|
+
"@pi-archimedes/sudo": "2.7.1"
|
|
26
30
|
},
|
|
27
31
|
"peerDependencies": {
|
|
28
32
|
"@earendil-works/pi-coding-agent": ">=0.1.0",
|
|
@@ -60,6 +60,7 @@ vi.mock("@pi-archimedes/todo", () => ({ registerTodo: vi.fn() }));
|
|
|
60
60
|
vi.mock("@pi-archimedes/ask", () => ({ registerAsk: vi.fn() }));
|
|
61
61
|
vi.mock("@pi-archimedes/notify", () => ({ registerNotify: vi.fn() }));
|
|
62
62
|
vi.mock("@pi-archimedes/session-name", () => ({ registerSessionName: vi.fn() }));
|
|
63
|
+
vi.mock("@pi-archimedes/sudo", () => ({ registerSudo: vi.fn() }));
|
|
63
64
|
|
|
64
65
|
// Dynamic imports done in the session_start handler — mock EXACTLY the
|
|
65
66
|
// properties index.ts uses via destructured `ipMod.*` / `diffMod` / `saMod` /
|
|
@@ -72,6 +73,9 @@ vi.mock("@pi-archimedes/image-paste", () => ({
|
|
|
72
73
|
shutdownImagePaste: vi.fn(),
|
|
73
74
|
initImagePasteSession: vi.fn(),
|
|
74
75
|
}));
|
|
76
|
+
vi.mock("@pi-archimedes/image-paste/keybinding-offer", () => ({
|
|
77
|
+
offerKeybindingFix: vi.fn(),
|
|
78
|
+
}));
|
|
75
79
|
vi.mock("@pi-archimedes/subagent", () => ({
|
|
76
80
|
registerSubagent: vi.fn(),
|
|
77
81
|
registerAgentsCommand: vi.fn(),
|
|
@@ -92,6 +96,9 @@ const { default: metaFactory } = await import("./index.js");
|
|
|
92
96
|
const { registerImagePaste, shutdownImagePaste, initImagePasteSession } =
|
|
93
97
|
await import("@pi-archimedes/image-paste");
|
|
94
98
|
|
|
99
|
+
const { offerKeybindingFix } =
|
|
100
|
+
await import("@pi-archimedes/image-paste/keybinding-offer");
|
|
101
|
+
|
|
95
102
|
// ── Stub pi: record pi.on() registrations and command/tool registrations ───
|
|
96
103
|
|
|
97
104
|
interface PiHarness {
|
|
@@ -125,6 +132,12 @@ function freshFactory(): {
|
|
|
125
132
|
harness: PiHarness;
|
|
126
133
|
startSession: (ctx: unknown) => Promise<unknown>;
|
|
127
134
|
shutdownSession: () => unknown;
|
|
135
|
+
/** Fire EVERY registered session_start handler in registration order. */
|
|
136
|
+
fireAllStartHandlers: (ctx: unknown) => Promise<void>;
|
|
137
|
+
/** Fire ONLY the keybinding-offer session_start handler (the one registered
|
|
138
|
+
* before the lazy-load handler). Useful for isolating offer behaviour
|
|
139
|
+
* without triggering the heavy dynamic-import path. */
|
|
140
|
+
fireOfferHandler: (ctx: unknown) => void;
|
|
128
141
|
} {
|
|
129
142
|
const harness = makePi();
|
|
130
143
|
metaFactory(harness.pi);
|
|
@@ -133,13 +146,26 @@ function freshFactory(): {
|
|
|
133
146
|
const shutdownRcs = harness.handlers["session_shutdown"] ?? [];
|
|
134
147
|
expect(startRcs.length).toBeGreaterThan(0);
|
|
135
148
|
expect(shutdownRcs.length).toBeGreaterThan(0);
|
|
149
|
+
// The LAST session_start handler is the lazy-load one (existing tests rely on this).
|
|
136
150
|
const startRc = (startRcs[startRcs.length - 1] ?? expect.fail("no session_start handler")) as (...args: unknown[]) => unknown;
|
|
137
151
|
const shutdownRc = (shutdownRcs[shutdownRcs.length - 1] ?? expect.fail("no session_shutdown handler")) as (...args: unknown[]) => unknown;
|
|
152
|
+
// The keybinding-offer handler is at index 0 in this fully-mocked harness —
|
|
153
|
+
// all earlier package registrations are stubbed to no-op vi.fn(); if a
|
|
154
|
+
// package is un-mocked here, update the index.
|
|
155
|
+
const offerRc = (startRcs[0] ?? expect.fail("no offer session_start handler")) as (...args: unknown[]) => unknown;
|
|
138
156
|
|
|
139
157
|
return {
|
|
140
158
|
harness,
|
|
141
159
|
startSession: (ctx: unknown) => startRc(undefined, ctx) as Promise<unknown>,
|
|
142
160
|
shutdownSession: () => shutdownRc(undefined, {}),
|
|
161
|
+
fireAllStartHandlers: async (ctx: unknown) => {
|
|
162
|
+
for (const handler of startRcs) {
|
|
163
|
+
await (handler(undefined, ctx) as Promise<unknown>);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
fireOfferHandler: (ctx: unknown) => {
|
|
167
|
+
offerRc(undefined, ctx);
|
|
168
|
+
},
|
|
143
169
|
};
|
|
144
170
|
}
|
|
145
171
|
|
|
@@ -194,3 +220,41 @@ describe("image-paste factory lifecycle (registration is config-gated, teardown
|
|
|
194
220
|
expect(vi.mocked(shutdownImagePaste)).toHaveBeenCalledTimes(1); // exactly once total
|
|
195
221
|
});
|
|
196
222
|
});
|
|
223
|
+
|
|
224
|
+
describe("keybinding-offer wiring (session_start → offerKeybindingFix, fire-and-forget)", () => {
|
|
225
|
+
it("calls offerKeybindingFix exactly once with the session ctx", () => {
|
|
226
|
+
const ctx = { ui: { theme: "dark" } };
|
|
227
|
+
// Return a resolved promise so the handler's .catch() has nothing to report.
|
|
228
|
+
vi.mocked(offerKeybindingFix).mockResolvedValueOnce(undefined);
|
|
229
|
+
const { fireOfferHandler } = freshFactory();
|
|
230
|
+
|
|
231
|
+
// The offer handler is synchronous from the caller's perspective (fire-and-forget).
|
|
232
|
+
fireOfferHandler(ctx);
|
|
233
|
+
|
|
234
|
+
expect(vi.mocked(offerKeybindingFix)).toHaveBeenCalledTimes(1);
|
|
235
|
+
expect(vi.mocked(offerKeybindingFix)).toHaveBeenCalledWith(ctx);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("session_start resolves even when offerKeybindingFix rejects (fire-and-forget safety)", async () => {
|
|
239
|
+
// Arrange: make the mock reject once to simulate an unexpected failure.
|
|
240
|
+
vi.mocked(offerKeybindingFix).mockRejectedValueOnce(new Error("boom"));
|
|
241
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
242
|
+
|
|
243
|
+
const ctx = { ui: { theme: "dark" } };
|
|
244
|
+
const { fireOfferHandler } = freshFactory();
|
|
245
|
+
|
|
246
|
+
// Act: invoke the offer handler; it must not throw synchronously.
|
|
247
|
+
expect(() => fireOfferHandler(ctx)).not.toThrow();
|
|
248
|
+
|
|
249
|
+
// Drain the microtask queue so the .catch() branch has had time to run.
|
|
250
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
251
|
+
|
|
252
|
+
// The rejection must have been swallowed and logged — not re-thrown.
|
|
253
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
254
|
+
"[archimedes] keybinding offer failed:",
|
|
255
|
+
expect.any(Error),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
consoleSpy.mockRestore();
|
|
259
|
+
});
|
|
260
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,10 @@ import { isPluginEnabled, migrateLegacyPluginsMap } from "./plugins.js";
|
|
|
14
14
|
import { registerNotify } from "@pi-archimedes/notify";
|
|
15
15
|
import { registerSessionName } from "@pi-archimedes/session-name";
|
|
16
16
|
import { registerSudo } from "@pi-archimedes/sudo";
|
|
17
|
+
// Light module from the image-paste package (no heavy deps) — the offer
|
|
18
|
+
// gates are self-contained, so it is statically imported and runs in its
|
|
19
|
+
// own top-level session_start handler, independent of the lazy-load.
|
|
20
|
+
import { offerKeybindingFix } from "@pi-archimedes/image-paste/keybinding-offer";
|
|
17
21
|
import { loadDiffConfig } from "./config.js";
|
|
18
22
|
import { openSettings } from "./settings.js"
|
|
19
23
|
import { registerPluginsCommand } from "./plugin-manager.js"
|
|
@@ -75,6 +79,32 @@ export default function (pi: ExtensionAPI): void {
|
|
|
75
79
|
archPrintTimings();
|
|
76
80
|
});
|
|
77
81
|
|
|
82
|
+
// First-run keybinding offer (see the module doc in
|
|
83
|
+
// packages/image-paste/src/keybinding-offer.ts). Registered at top level
|
|
84
|
+
// (AGENTS.md), and placed BEFORE the lazy-load handler below: the
|
|
85
|
+
// module's own gates (config enabled → TUI → flag unset → file absent
|
|
86
|
+
// → confirm) are self-contained, so ordering with that handler is not a
|
|
87
|
+
// correctness issue.
|
|
88
|
+
//
|
|
89
|
+
// Deliberately NOT gated with isPluginEnabled("image-paste"): that call
|
|
90
|
+
// resolves to isConfigEnabled("archimedes.imagePaste") (ADR 0012), which
|
|
91
|
+
// is exactly the same key the module's own gate 1 already checks — adding
|
|
92
|
+
// a wrapper here would be a redundant duplicate of the identical check.
|
|
93
|
+
// This handler runs unconditionally on every session_start; the module's
|
|
94
|
+
// five gates make it a no-op when not applicable.
|
|
95
|
+
pi.on("session_start", (_event, ctx: ExtensionContext) => {
|
|
96
|
+
// Fire-and-forget: the offer is async (it may block on a user
|
|
97
|
+
// confirm) and must never block or take down session startup — any
|
|
98
|
+
// throw is logged, never propagated.
|
|
99
|
+
try {
|
|
100
|
+
void offerKeybindingFix(ctx).catch((e) => {
|
|
101
|
+
console.error("[archimedes] keybinding offer failed:", e);
|
|
102
|
+
});
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.error("[archimedes] keybinding offer failed:", e);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
78
108
|
pi.on("session_start", async (_event, ctx: ExtensionContext) => {
|
|
79
109
|
archTime(`session_start (factory was ${Date.now() - _moduleEvalAt}ms ago)`);
|
|
80
110
|
|