pi-fast-resume 1.0.2 → 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.
package/README.md CHANGED
@@ -107,7 +107,11 @@ Reload with `/reload` after any install method.
107
107
 
108
108
  ### Keyboard shortcut
109
109
 
110
- In hijack mode (default), the fast picker replaces `/resume` so bind `app.session.resume` in `~/.pi/agent/keybindings.json`:
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`:
111
115
 
112
116
  ```json
113
117
  {
@@ -115,6 +119,18 @@ In hijack mode (default), the fast picker replaces `/resume` — so bind `app.se
115
119
  }
116
120
  ```
117
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
+
118
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.
119
135
 
120
136
  ### Picker controls
@@ -190,17 +206,25 @@ However, pi-fast-resume can **prototype-patch** `InteractiveMode.showSessionSele
190
206
  - `/fast-resume` is not registered (no duplicate command)
191
207
  - `pi -r` / `pi --resume` are **not** affected (they run before the interactive mode starts)
192
208
 
193
- ### 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"`.
194
217
 
195
- Create or edit `~/.pi/agent/extensions/pi-fast-resume.json`:
218
+ Example:
196
219
 
197
220
  ```json
198
221
  {
199
- "hijackResume": false
222
+ "hijackResume": false,
223
+ "shortcut": "alt+u"
200
224
  }
201
225
  ```
202
226
 
203
- Then reload with `/reload`. To re-enable, set `hijackResume` to `true` (or delete the key) and reload.
227
+ Reload with `/reload` after changing config.
204
228
 
205
229
  ### How it works
206
230
 
package/fast-resume.ts CHANGED
@@ -43,6 +43,7 @@
43
43
  */
44
44
 
45
45
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
46
+ import type { KeyId } from "@earendil-works/pi-tui";
46
47
  import {
47
48
  DynamicBorder,
48
49
  InteractiveMode,
@@ -93,10 +94,12 @@ import type { PickerScope } from "./src/picker-state.js";
93
94
  const HOME = homedir();
94
95
 
95
96
  // Config — read from ~/.pi/agent/extensions/pi-fast-resume.json
96
- // Example: { "hijackResume": false }
97
+ // Example: { "hijackResume": false, "shortcut": "alt+u" }
97
98
  // By default hijackResume is true — /resume opens the fast picker
99
+ // Set shortcut to register a standalone shortcut (e.g. "ctrl+shift+f")
98
100
  interface FastResumeConfig {
99
101
  hijackResume?: boolean;
102
+ shortcut?: string;
100
103
  }
101
104
 
102
105
  const CONFIG_PATH = join(HOME, ".pi", "agent", "extensions", "pi-fast-resume.json");
@@ -991,9 +994,52 @@ async function showFastResumePicker(
991
994
  }
992
995
  }
993
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
+
994
1003
  // Reference to the original showSessionSelector, saved before patching
995
1004
  let origShowSessionSelector: ((this: InteractiveMode) => void) | null = null;
996
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
+
997
1043
  function installResumeHijack(): void {
998
1044
  if (origShowSessionSelector !== null) return; // Already patched
999
1045
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- prototype patching requires any cast for private access
@@ -1057,17 +1103,55 @@ export default function (pi: ExtensionAPI) {
1057
1103
  });
1058
1104
  }
1059
1105
 
1060
- // No extension shortcut pi's registerShortcut only provides
1061
- // ExtensionContext (no switchSession), so the handler can't switch
1062
- // sessions. Instead, rebind app.session.resume in keybindings.json:
1063
- // { "app.session.resume": "alt+u" }
1064
- // In hijack mode (default), that key opens the fast picker.
1065
- // In non-hijack mode, type /fast-resume.
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
+ }
1066
1147
 
1067
- // Clean up the prototype patch on session shutdown (reload, quit, session switch)
1148
+ // Clean up prototype patches on session shutdown (reload, quit, session switch)
1068
1149
  pi.on("session_shutdown", () => {
1069
1150
  if (hijackResume) {
1070
1151
  uninstallResumeHijack();
1071
1152
  }
1153
+ if (shortcut) {
1154
+ unpatchSetupExtensionShortcuts();
1155
+ }
1072
1156
  });
1073
1157
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fast-resume",
3
- "version": "1.0.2",
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",