pi-fast-resume 1.4.13 → 1.4.15
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 +13 -0
- package/fast-resume.ts +43 -154
- package/package.json +8 -3
- package/src/interactive-patch.ts +26 -0
package/README.md
CHANGED
|
@@ -264,3 +264,16 @@ All other features — tree view, regex/exact-phrase search, sort modes, scope t
|
|
|
264
264
|
## License
|
|
265
265
|
|
|
266
266
|
[MIT](./LICENSE)
|
|
267
|
+
|
|
268
|
+
## Pi 1.0 compatibility (1.4.15)
|
|
269
|
+
|
|
270
|
+
Tested against Pi **1.0.0** with exact SDK development pins and wildcard host peers.
|
|
271
|
+
The live mapped InteractiveMode opens the fast picker and restores its selector hook on shutdown.
|
|
272
|
+
|
|
273
|
+
Run `bun run test:host` for the offline real-host regression. Set `PI1_HOST_PACKAGE` to an installed Pi package directory and `PI1_HOST_ENTRY=bundle` to test its bundled CLI runtime.
|
|
274
|
+
|
|
275
|
+
## Pi 0.99 compatibility (1.4.14)
|
|
276
|
+
|
|
277
|
+
Selector and shortcut patches now activate only in live TUI sessions, reinstall after session replacement, and compose safely with other picker wrappers. The native 0.99 selector/shortcut method contracts and actual loading were verified.
|
|
278
|
+
|
|
279
|
+
Tested with Pi 0.99.0. Host-provided Pi packages are wildcard peers, not bundled dependencies; development uses exact 0.99.0 versions.
|
package/fast-resume.ts
CHANGED
|
@@ -1425,164 +1425,53 @@ async function showFastResumePicker(
|
|
|
1425
1425
|
}
|
|
1426
1426
|
}
|
|
1427
1427
|
|
|
1428
|
-
|
|
1429
|
-
// InteractiveMode.prototype.setupExtensionShortcuts. Used by the shortcut
|
|
1430
|
-
// handler to create an ExtensionCommandContext with switchSession(), since
|
|
1431
|
-
// pi.registerShortcut handlers only receive ExtensionContext.
|
|
1432
|
-
let storedExtensionRunner: any = null;
|
|
1433
|
-
|
|
1434
|
-
// Reference to the original showSessionSelector, saved before patching
|
|
1435
|
-
let origShowSessionSelector: ((this: InteractiveMode) => void) | null = null;
|
|
1436
|
-
|
|
1437
|
-
// Reference to the original setupExtensionShortcuts, saved before patching
|
|
1438
|
-
let origSetupExtensionShortcuts: Function | null = null;
|
|
1439
|
-
|
|
1440
|
-
function patchSetupExtensionShortcuts(): void {
|
|
1441
|
-
if (origSetupExtensionShortcuts !== null) return; // Already patched
|
|
1442
|
-
const proto = InteractiveMode.prototype as any;
|
|
1443
|
-
if (
|
|
1444
|
-
!InteractiveMode ||
|
|
1445
|
-
typeof InteractiveMode !== "function" ||
|
|
1446
|
-
typeof proto.setupExtensionShortcuts !== "function"
|
|
1447
|
-
) {
|
|
1448
|
-
return;
|
|
1449
|
-
}
|
|
1450
|
-
origSetupExtensionShortcuts = proto.setupExtensionShortcuts;
|
|
1451
|
-
proto.setupExtensionShortcuts = function (
|
|
1452
|
-
this: InteractiveMode,
|
|
1453
|
-
extensionRunner: any,
|
|
1454
|
-
) {
|
|
1455
|
-
storedExtensionRunner = extensionRunner;
|
|
1456
|
-
origSetupExtensionShortcuts!.call(this, extensionRunner);
|
|
1457
|
-
};
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
function unpatchSetupExtensionShortcuts(): void {
|
|
1461
|
-
if (origSetupExtensionShortcuts === null) return;
|
|
1462
|
-
const proto = InteractiveMode.prototype as any;
|
|
1463
|
-
if (
|
|
1464
|
-
InteractiveMode &&
|
|
1465
|
-
typeof InteractiveMode === "function" &&
|
|
1466
|
-
typeof proto.setupExtensionShortcuts === "function"
|
|
1467
|
-
) {
|
|
1468
|
-
proto.setupExtensionShortcuts = origSetupExtensionShortcuts;
|
|
1469
|
-
}
|
|
1470
|
-
origSetupExtensionShortcuts = null;
|
|
1471
|
-
storedExtensionRunner = null;
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
function installResumeHijack(): void {
|
|
1475
|
-
if (origShowSessionSelector !== null) return; // Already patched
|
|
1476
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- prototype patching requires any cast for private access
|
|
1477
|
-
const proto = InteractiveMode.prototype as any;
|
|
1478
|
-
if (
|
|
1479
|
-
!InteractiveMode ||
|
|
1480
|
-
typeof InteractiveMode !== "function" ||
|
|
1481
|
-
typeof proto.showSessionSelector !== "function"
|
|
1482
|
-
) {
|
|
1483
|
-
return; // Guard: API changed or not available
|
|
1484
|
-
}
|
|
1485
|
-
origShowSessionSelector = proto.showSessionSelector;
|
|
1486
|
-
proto.showSessionSelector = function (this: InteractiveMode) {
|
|
1487
|
-
// Try to get an ExtensionCommandContext from the running session's extension runner
|
|
1488
|
-
const session = (this as any).session;
|
|
1489
|
-
if (!session?.extensionRunner?.createCommandContext) {
|
|
1490
|
-
// Fallback to original if we can't get a command context
|
|
1491
|
-
origShowSessionSelector!.call(this);
|
|
1492
|
-
return;
|
|
1493
|
-
}
|
|
1494
|
-
const ctx = session.extensionRunner.createCommandContext() as ExtensionCommandContext;
|
|
1495
|
-
// Fire-and-forget — same pattern as the original (synchronous, UI appears immediately)
|
|
1496
|
-
void showFastResumePicker(ctx);
|
|
1497
|
-
};
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
function uninstallResumeHijack(): void {
|
|
1501
|
-
if (origShowSessionSelector === null) return;
|
|
1502
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- prototype patching requires any cast for private access
|
|
1503
|
-
const proto = InteractiveMode.prototype as any;
|
|
1504
|
-
if (
|
|
1505
|
-
InteractiveMode &&
|
|
1506
|
-
typeof InteractiveMode === "function" &&
|
|
1507
|
-
typeof proto.showSessionSelector === "function"
|
|
1508
|
-
) {
|
|
1509
|
-
proto.showSessionSelector = origShowSessionSelector;
|
|
1510
|
-
}
|
|
1511
|
-
origShowSessionSelector = null;
|
|
1512
|
-
}
|
|
1428
|
+
import { installInteractivePatch } from "./src/interactive-patch.js";
|
|
1513
1429
|
|
|
1514
1430
|
export default function (pi: ExtensionAPI) {
|
|
1515
1431
|
const config = readConfig();
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
if (!
|
|
1528
|
-
return
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
//
|
|
1543
|
-
// Users can rebind the key via pi-fast-resume.json:
|
|
1544
|
-
// { "shortcut": "alt+u" }
|
|
1545
|
-
//
|
|
1546
|
-
// In hijack mode, app.session.resume also opens the fast picker (rebindable
|
|
1547
|
-
// in ~/.pi/agent/keybindings.json). The shortcut config is an additional
|
|
1548
|
-
// independent binding that does not override the built-in /resume.
|
|
1549
|
-
const shortcut = config.shortcut;
|
|
1550
|
-
if (shortcut) {
|
|
1551
|
-
// Patch setupExtensionShortcuts so we can capture the extension runner.
|
|
1552
|
-
// This runs before setupExtensionShortcuts is called (during extension
|
|
1553
|
-
// load, which precedes the shortcut setup phase).
|
|
1554
|
-
patchSetupExtensionShortcuts();
|
|
1555
|
-
|
|
1556
|
-
pi.registerShortcut(shortcut as KeyId, {
|
|
1557
|
-
description: "Fast session resume",
|
|
1558
|
-
handler: async (ctx) => {
|
|
1559
|
-
// Use the stored extension runner to get a full command context
|
|
1560
|
-
// with switchSession(), since the shortcut handler ctx (ExtensionContext)
|
|
1561
|
-
// does not include session-switching methods.
|
|
1562
|
-
if (
|
|
1563
|
-
!storedExtensionRunner ||
|
|
1564
|
-
typeof storedExtensionRunner.createCommandContext !== "function"
|
|
1565
|
-
) {
|
|
1566
|
-
ctx.ui.notify(
|
|
1567
|
-
"Fast resume shortcut: extension runner not available. Try reloading with /reload.",
|
|
1568
|
-
"error",
|
|
1569
|
-
);
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
const cmdCtx =
|
|
1573
|
-
storedExtensionRunner.createCommandContext() as ExtensionCommandContext;
|
|
1574
|
-
await showFastResumePicker(cmdCtx);
|
|
1575
|
-
},
|
|
1576
|
-
});
|
|
1577
|
-
}
|
|
1432
|
+
const hijack = config.hijackResume !== false;
|
|
1433
|
+
let runner: { createCommandContext(): ExtensionCommandContext } | undefined;
|
|
1434
|
+
let restores: Array<() => void> = [];
|
|
1435
|
+
|
|
1436
|
+
// Pi 0.99 binds session_start before setupExtensionShortcuts, including
|
|
1437
|
+
// session replacement. Factories loaded for discovery must not patch the UI.
|
|
1438
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1439
|
+
if (ctx.mode !== "tui" || restores.length) return;
|
|
1440
|
+
try {
|
|
1441
|
+
if (hijack) restores.push(installInteractivePatch(InteractiveMode.prototype, "showSessionSelector", function (original, ...args) {
|
|
1442
|
+
const currentRunner = this.session?.extensionRunner;
|
|
1443
|
+
if (!currentRunner?.createCommandContext) return original.apply(this, args);
|
|
1444
|
+
return showFastResumePicker(currentRunner.createCommandContext()).catch((error: unknown) => {
|
|
1445
|
+
ctx.ui.notify(`Fast resume: ${String(error)}`, "error");
|
|
1446
|
+
});
|
|
1447
|
+
}));
|
|
1448
|
+
if (config.shortcut) restores.push(installInteractivePatch(InteractiveMode.prototype, "setupExtensionShortcuts", function (original, extensionRunner) {
|
|
1449
|
+
runner = extensionRunner;
|
|
1450
|
+
return original.call(this, extensionRunner);
|
|
1451
|
+
}));
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
for (const restore of restores.reverse()) restore();
|
|
1454
|
+
restores = [];
|
|
1455
|
+
ctx.ui.notify(String(error), "warning");
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1578
1458
|
|
|
1579
|
-
|
|
1459
|
+
if (!hijack) pi.registerCommand("fast-resume", {
|
|
1460
|
+
description: "Fast session resume",
|
|
1461
|
+
getArgumentCompletions: (prefix: string) => prefix ? [{ value: prefix, label: `Search: ${prefix}` }] : null,
|
|
1462
|
+
handler: async (args, ctx) => { await showFastResumePicker(ctx, args?.trim() || undefined); },
|
|
1463
|
+
});
|
|
1464
|
+
if (config.shortcut) pi.registerShortcut(config.shortcut as KeyId, {
|
|
1465
|
+
description: "Fast session resume",
|
|
1466
|
+
handler: async (ctx) => {
|
|
1467
|
+
if (ctx.mode !== "tui") return;
|
|
1468
|
+
if (!runner) { ctx.ui.notify("Fast resume: session shortcuts are not bound", "error"); return; }
|
|
1469
|
+
await showFastResumePicker(runner.createCommandContext());
|
|
1470
|
+
},
|
|
1471
|
+
});
|
|
1580
1472
|
pi.on("session_shutdown", () => {
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
if (shortcut) {
|
|
1585
|
-
unpatchSetupExtensionShortcuts();
|
|
1586
|
-
}
|
|
1473
|
+
for (const restore of restores.reverse()) restore();
|
|
1474
|
+
restores = [];
|
|
1475
|
+
runner = undefined;
|
|
1587
1476
|
});
|
|
1588
1477
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-fast-resume",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.15",
|
|
4
4
|
"description": "Fast session picker for pi — reads headers + first messages from 16KB partial reads instead of full-file parsing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Tom X Nguyen",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"README.md"
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
|
+
"test:host": "node scripts/pi1-host.mjs",
|
|
33
34
|
"test": "vitest run",
|
|
34
35
|
"test:watch": "vitest",
|
|
35
36
|
"test:coverage": "vitest run --coverage",
|
|
@@ -38,8 +39,8 @@
|
|
|
38
39
|
"lint:dead": "knip --no-gitignore"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
42
|
-
"@earendil-works/pi-tui": "0.
|
|
42
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
43
|
+
"@earendil-works/pi-tui": "1.0.0",
|
|
43
44
|
"@types/node": "25.9.1",
|
|
44
45
|
"@vitest/coverage-v8": "4.1.7",
|
|
45
46
|
"knip": "6.14.1",
|
|
@@ -53,5 +54,9 @@
|
|
|
53
54
|
},
|
|
54
55
|
"overrides": {
|
|
55
56
|
"brace-expansion": "5.0.6"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
60
|
+
"@earendil-works/pi-tui": "*"
|
|
56
61
|
}
|
|
57
62
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Shared metadata lets independent picker packages unwind wrappers in either order.
|
|
2
|
+
const PATCH = Symbol.for("pi.fast-picker.patch.v1");
|
|
3
|
+
type Method = (this: any, ...args: any[]) => any;
|
|
4
|
+
type Wrapped = Method & { [PATCH]?: { active: boolean; original: Method } };
|
|
5
|
+
function unwrap(method: Wrapped): Method {
|
|
6
|
+
while (method[PATCH] && !method[PATCH]!.active) method = method[PATCH]!.original;
|
|
7
|
+
return method;
|
|
8
|
+
}
|
|
9
|
+
export function installInteractivePatch(
|
|
10
|
+
prototype: any,
|
|
11
|
+
name: string,
|
|
12
|
+
handler: (this: any, original: Method, ...args: any[]) => any,
|
|
13
|
+
): () => void {
|
|
14
|
+
const original = prototype[name] as Method;
|
|
15
|
+
if (typeof original !== "function") throw new Error(`Pi interactive contract missing: ${name}`);
|
|
16
|
+
const state = { active: true, original };
|
|
17
|
+
const wrapped: Wrapped = function (...args) {
|
|
18
|
+
return state.active ? handler.call(this, original, ...args) : original.apply(this, args);
|
|
19
|
+
};
|
|
20
|
+
wrapped[PATCH] = state;
|
|
21
|
+
prototype[name] = wrapped;
|
|
22
|
+
return () => {
|
|
23
|
+
state.active = false;
|
|
24
|
+
if (prototype[name] === wrapped) prototype[name] = unwrap(original);
|
|
25
|
+
};
|
|
26
|
+
}
|