pi-fast-resume 1.0.1 → 1.1.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.
Files changed (3) hide show
  1. package/README.md +37 -7
  2. package/fast-resume.ts +96 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -107,9 +107,31 @@ Reload with `/reload` after any install method.
107
107
 
108
108
  ### Keyboard shortcut
109
109
 
110
- | Shortcut | Action |
111
- | -------------- | --------------------------- |
112
- | `Ctrl+Shift+F` | Open the fast resume picker |
110
+ There are two ways to bind a key to the fast resume picker:
111
+
112
+ **Option 1: Rebind `app.session.resume`** (hijack mode only)
113
+
114
+ In hijack mode (default), the fast picker replaces `/resume` — bind `app.session.resume` in `~/.pi/agent/keybindings.json`:
115
+
116
+ ```json
117
+ {
118
+ "app.session.resume": "alt+u"
119
+ }
120
+ ```
121
+
122
+ **Option 2: Standalone shortcut** (works regardless of hijack mode)
123
+
124
+ Set the `shortcut` key in `~/.pi/agent/extensions/pi-fast-resume.json`:
125
+
126
+ ```json
127
+ {
128
+ "shortcut": "ctrl+shift+f"
129
+ }
130
+ ```
131
+
132
+ This registers an independent shortcut that opens the fast picker without overriding the built-in `/resume`. Works in both hijack and non-hijack modes.
133
+
134
+ See [keybindings.md](https://github.com/earendil-works/pi-coding-agent/blob/main/docs/keybindings.md) for the key format and all available actions.
113
135
 
114
136
  ### Picker controls
115
137
 
@@ -184,17 +206,25 @@ However, pi-fast-resume can **prototype-patch** `InteractiveMode.showSessionSele
184
206
  - `/fast-resume` is not registered (no duplicate command)
185
207
  - `pi -r` / `pi --resume` are **not** affected (they run before the interactive mode starts)
186
208
 
187
- ### Disable
209
+ ### Config options
210
+
211
+ All options go in `~/.pi/agent/extensions/pi-fast-resume.json`:
212
+
213
+ | Key | Type | Default | Description |
214
+ | --- | ---- | ------- | ----------- |
215
+ | `hijackResume` | `boolean` | `true` | When `true`, `/resume` and `app.session.resume` open the fast picker. Set `false` to use the built-in picker instead and keep `/fast-resume` as a separate command. |
216
+ | `shortcut` | `string` | (none) | Register a standalone keybinding to open the fast picker. Works regardless of `hijackResume`. Example: `"ctrl+shift+f"` or `"alt+u"`.
188
217
 
189
- Create or edit `~/.pi/agent/extensions/pi-fast-resume.json`:
218
+ Example:
190
219
 
191
220
  ```json
192
221
  {
193
- "hijackResume": false
222
+ "hijackResume": false,
223
+ "shortcut": "alt+u"
194
224
  }
195
225
  ```
196
226
 
197
- Then reload with `/reload`. To re-enable, set `hijackResume` to `true` (or delete the key) and reload.
227
+ Reload with `/reload` after changing config.
198
228
 
199
229
  ### How it works
200
230
 
package/fast-resume.ts CHANGED
@@ -9,9 +9,11 @@
9
9
  *
10
10
  * Usage:
11
11
  * /fast-resume [query] Open fast session picker (current project scope)
12
- * Ctrl+Shift+F Open fast session picker via shortcut
13
12
  *
14
- * Hijack mode (on by default, opt-out via ~/.pi/agent/extensions/pi-fast-resume.json):
13
+ * Config (~/.pi/agent/extensions/pi-fast-resume.json):
14
+ * { "hijackResume": false }
15
+ *
16
+ * Hijack mode (on by default, opt-out via config):
15
17
  * { "hijackResume": false }
16
18
  *
17
19
  * When enabled, /resume and Ctrl+Shift+R open the fast picker instead.
@@ -41,6 +43,7 @@
41
43
  */
42
44
 
43
45
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
46
+ import type { KeyId } from "@earendil-works/pi-tui";
44
47
  import {
45
48
  DynamicBorder,
46
49
  InteractiveMode,
@@ -54,7 +57,6 @@ import {
54
57
  type Component,
55
58
  getKeybindings,
56
59
  Input,
57
- Key,
58
60
  Spacer,
59
61
  Text,
60
62
  truncateToWidth,
@@ -92,10 +94,12 @@ import type { PickerScope } from "./src/picker-state.js";
92
94
  const HOME = homedir();
93
95
 
94
96
  // Config — read from ~/.pi/agent/extensions/pi-fast-resume.json
95
- // Example: { "hijackResume": false } to disable hijack
97
+ // Example: { "hijackResume": false, "shortcut": "alt+u" }
96
98
  // By default hijackResume is true — /resume opens the fast picker
99
+ // Set shortcut to register a standalone shortcut (e.g. "ctrl+shift+f")
97
100
  interface FastResumeConfig {
98
101
  hijackResume?: boolean;
102
+ shortcut?: string;
99
103
  }
100
104
 
101
105
  const CONFIG_PATH = join(HOME, ".pi", "agent", "extensions", "pi-fast-resume.json");
@@ -990,9 +994,52 @@ async function showFastResumePicker(
990
994
  }
991
995
  }
992
996
 
997
+ // Stored reference to the extension runner, captured via prototype patch on
998
+ // InteractiveMode.prototype.setupExtensionShortcuts. Used by the shortcut
999
+ // handler to create an ExtensionCommandContext with switchSession(), since
1000
+ // pi.registerShortcut handlers only receive ExtensionContext.
1001
+ let storedExtensionRunner: any = null;
1002
+
993
1003
  // Reference to the original showSessionSelector, saved before patching
994
1004
  let origShowSessionSelector: ((this: InteractiveMode) => void) | null = null;
995
1005
 
1006
+ // Reference to the original setupExtensionShortcuts, saved before patching
1007
+ let origSetupExtensionShortcuts: Function | null = null;
1008
+
1009
+ function patchSetupExtensionShortcuts(): void {
1010
+ if (origSetupExtensionShortcuts !== null) return; // Already patched
1011
+ const proto = InteractiveMode.prototype as any;
1012
+ if (
1013
+ !InteractiveMode ||
1014
+ typeof InteractiveMode !== "function" ||
1015
+ typeof proto.setupExtensionShortcuts !== "function"
1016
+ ) {
1017
+ return;
1018
+ }
1019
+ origSetupExtensionShortcuts = proto.setupExtensionShortcuts;
1020
+ proto.setupExtensionShortcuts = function (
1021
+ this: InteractiveMode,
1022
+ extensionRunner: any,
1023
+ ) {
1024
+ storedExtensionRunner = extensionRunner;
1025
+ origSetupExtensionShortcuts!.call(this, extensionRunner);
1026
+ };
1027
+ }
1028
+
1029
+ function unpatchSetupExtensionShortcuts(): void {
1030
+ if (origSetupExtensionShortcuts === null) return;
1031
+ const proto = InteractiveMode.prototype as any;
1032
+ if (
1033
+ InteractiveMode &&
1034
+ typeof InteractiveMode === "function" &&
1035
+ typeof proto.setupExtensionShortcuts === "function"
1036
+ ) {
1037
+ proto.setupExtensionShortcuts = origSetupExtensionShortcuts;
1038
+ }
1039
+ origSetupExtensionShortcuts = null;
1040
+ storedExtensionRunner = null;
1041
+ }
1042
+
996
1043
  function installResumeHijack(): void {
997
1044
  if (origShowSessionSelector !== null) return; // Already patched
998
1045
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- prototype patching requires any cast for private access
@@ -1056,17 +1103,55 @@ export default function (pi: ExtensionAPI) {
1056
1103
  });
1057
1104
  }
1058
1105
 
1059
- pi.registerShortcut(Key.ctrlShift("f"), {
1060
- description: hijackResume ? "Open fast session resume (also /resume)" : "Open fast session resume picker",
1061
- handler: async (ctx) => {
1062
- await showFastResumePicker(ctx as unknown as ExtensionCommandContext);
1063
- },
1064
- });
1106
+ // Register a standalone keyboard shortcut for the fast resume picker.
1107
+ // pi.registerShortcut handlers receive ExtensionContext (no switchSession),
1108
+ // so we capture the extension runner via a prototype patch on
1109
+ // InteractiveMode.prototype.setupExtensionShortcuts and use it to create
1110
+ // an ExtensionCommandContext inside the handler.
1111
+ //
1112
+ // Users can rebind the key via pi-fast-resume.json:
1113
+ // { "shortcut": "alt+u" }
1114
+ //
1115
+ // In hijack mode, app.session.resume also opens the fast picker (rebindable
1116
+ // in ~/.pi/agent/keybindings.json). The shortcut config is an additional
1117
+ // independent binding that does not override the built-in /resume.
1118
+ const shortcut = config.shortcut;
1119
+ if (shortcut) {
1120
+ // Patch setupExtensionShortcuts so we can capture the extension runner.
1121
+ // This runs before setupExtensionShortcuts is called (during extension
1122
+ // load, which precedes the shortcut setup phase).
1123
+ patchSetupExtensionShortcuts();
1124
+
1125
+ pi.registerShortcut(shortcut as KeyId, {
1126
+ description: "Fast session resume",
1127
+ handler: async (ctx) => {
1128
+ // Use the stored extension runner to get a full command context
1129
+ // with switchSession(), since the shortcut handler ctx (ExtensionContext)
1130
+ // does not include session-switching methods.
1131
+ if (
1132
+ !storedExtensionRunner ||
1133
+ typeof storedExtensionRunner.createCommandContext !== "function"
1134
+ ) {
1135
+ ctx.ui.notify(
1136
+ "Fast resume shortcut: extension runner not available. Try reloading with /reload.",
1137
+ "error",
1138
+ );
1139
+ return;
1140
+ }
1141
+ const cmdCtx =
1142
+ storedExtensionRunner.createCommandContext() as ExtensionCommandContext;
1143
+ await showFastResumePicker(cmdCtx);
1144
+ },
1145
+ });
1146
+ }
1065
1147
 
1066
- // Clean up the prototype patch on session shutdown (reload, quit, session switch)
1148
+ // Clean up prototype patches on session shutdown (reload, quit, session switch)
1067
1149
  pi.on("session_shutdown", () => {
1068
1150
  if (hijackResume) {
1069
1151
  uninstallResumeHijack();
1070
1152
  }
1153
+ if (shortcut) {
1154
+ unpatchSetupExtensionShortcuts();
1155
+ }
1071
1156
  });
1072
1157
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fast-resume",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
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",