pi-better-btw-plus 1.0.0 → 1.0.2

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/banner.png CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-btw-plus",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "pi extension: /btw side-chat overlay — maintained fork of @yceachan/pi-better-btw (+ right-click copy/paste, fork model switch, turn-level retry)",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -6,9 +6,9 @@
6
6
  * is self-built because `readClipboardText` is not re-exported from the
7
7
  * package entry. Each platform maps to a primary channel:
8
8
  *
9
- * - win32 → PowerShell `Get-Clipboard -Raw` (same channel family as pi's
10
- * clipboard-image reader), then an OSC 52 query;
11
- * - darwin → `pbpaste`, then an OSC 52 query;
9
+ * - win32 → native addon `getText` (pi's own clipboard read), then
10
+ * PowerShell `Get-Clipboard -Raw`, then an OSC 52 query;
11
+ * - darwin → native addon `getText`, then `pbpaste`, then OSC 52 query;
12
12
  * - linux → OSC 52 query (`\x1b]52;c;?\x07`, read the reply).
13
13
  *
14
14
  * Channels are injected functions, so tests mock success / fallback /
@@ -19,6 +19,9 @@
19
19
  */
20
20
  import { spawnSync } from "node:child_process";
21
21
 
22
+ import { createRequire } from "node:module";
23
+ import { dirname, join } from "node:path";
24
+ import { pathToFileURL } from "node:url";
22
25
  /**
23
26
  * Outcome of reading the system clipboard: text, an explicitly empty
24
27
  * clipboard (a channel ran and saw nothing), or no usable channel at all.
@@ -159,6 +162,87 @@ function makeCommandChannel(
159
162
  };
160
163
  }
161
164
 
165
+ /**
166
+ * Native clipboard addon (win32 / darwin primary read channel): the
167
+ * `@mariozechner/clipboard` napi addon pi itself uses for reads and writes.
168
+ * Its `getText` is a sub-millisecond, non-blocking call — vs. the PowerShell
169
+ * `Get-Clipboard` subprocess, which cold-starts ~1s and blocks the event loop
170
+ * with `spawnSync` (the source of the right-click-paste lag). Resolution
171
+ * mirrors pi's `clipboard-native` loader: the package's own `require` first,
172
+ * then a `require` rooted at the pi executable's directory.
173
+ *
174
+ * The addon is an optional dependency provided at runtime by the pi host
175
+ * environment — deliberately not listed in package.json to avoid
176
+ * cross-platform prebuild dependency issues. When it is missing or fails to
177
+ * load, the channel reports `unavailable` and the fallback chain
178
+ * (PowerShell / pbpaste → OSC 52) takes over.
179
+ */
180
+ export interface NativeClipboardAddon {
181
+ /** Resolve the clipboard's plain text; null when it holds no text. */
182
+ getText(): Promise<string | null>;
183
+ }
184
+
185
+ let cachedNativeAddon: NativeClipboardAddon | null | undefined;
186
+
187
+ function loadNativeAddon(): NativeClipboardAddon | null {
188
+ if (cachedNativeAddon !== undefined) return cachedNativeAddon;
189
+ const moduleRequire = createRequire(import.meta.url);
190
+ const executableDirRequire = createRequire(
191
+ pathToFileURL(join(dirname(process.execPath), "package.json")).href,
192
+ );
193
+ cachedNativeAddon = null;
194
+ for (const requireClipboard of [moduleRequire, executableDirRequire]) {
195
+ try {
196
+ const addon = requireClipboard("@mariozechner/clipboard") as
197
+ | { getText?: () => Promise<string | null> }
198
+ | undefined;
199
+ if (addon && typeof addon.getText === "function") {
200
+ cachedNativeAddon = addon as NativeClipboardAddon;
201
+ break;
202
+ }
203
+ } catch {
204
+ // Try the next resolution root.
205
+ }
206
+ }
207
+ return cachedNativeAddon;
208
+ }
209
+
210
+ export interface NativeChannelOptions {
211
+ /** Native addon loader override (tests). Defaults to {@link loadNativeAddon}. */
212
+ loadNative?: () => NativeClipboardAddon | null;
213
+ }
214
+
215
+ /**
216
+ * Native channel: read plain text via the `@mariozechner/clipboard` addon.
217
+ * A missing/unloadable addon reports `unavailable`; a loaded addon that sees
218
+ * no text reports `empty` (definitive, stops the cascade).
219
+ */
220
+ export function makeNativeChannel(
221
+ options: NativeChannelOptions = {},
222
+ ): ClipboardReadChannel {
223
+ const load = options.loadNative ?? loadNativeAddon;
224
+ return {
225
+ name: "native",
226
+ async read() {
227
+ let addon: NativeClipboardAddon | null;
228
+ try {
229
+ addon = load();
230
+ } catch {
231
+ addon = null;
232
+ }
233
+ if (!addon) return { ok: false, reason: "unavailable" };
234
+ try {
235
+ const text = await addon.getText();
236
+ return text && text.length > 0
237
+ ? { ok: true, text }
238
+ : { ok: false, reason: "empty" };
239
+ } catch {
240
+ return { ok: false, reason: "unavailable" };
241
+ }
242
+ },
243
+ };
244
+ }
245
+
162
246
  /**
163
247
  * PowerShell channel: `powershell.exe -NoProfile -Command "Get-Clipboard
164
248
  * -Raw"`, mirroring pi's clipboard-image PowerShell channel family. The
@@ -298,13 +382,14 @@ export interface PlatformChannelsOptions {
298
382
  exec?: ExecFn;
299
383
  write?: (sequence: string) => void;
300
384
  readReply?: (timeoutMs: number) => Promise<string | null>;
385
+ /** Native addon loader override (tests). Defaults to the real loader. */
386
+ loadNative?: () => NativeClipboardAddon | null;
301
387
  }
302
-
303
388
  /**
304
389
  * The per-platform channel matrix (primary first, then fallbacks). win32 /
305
- * darwin lead with their native tool and fall back to an OSC 52 query;
306
- * linux (and unknown platforms) query OSC 52 directly — mirroring the
307
- * write-side cascade where OSC 52 is the universal last resort.
390
+ * darwin lead with the native addon, fall back to their platform tool, then
391
+ * to an OSC 52 query; linux (and unknown platforms) query OSC 52 directly —
392
+ * mirroring the write-side cascade where OSC 52 is the universal last resort.
308
393
  */
309
394
  export function buildPlatformChannels(
310
395
  options: PlatformChannelsOptions = {},
@@ -316,11 +401,13 @@ export function buildPlatformChannels(
316
401
  switch (options.platform ?? process.platform) {
317
402
  case "win32":
318
403
  return [
404
+ makeNativeChannel({ loadNative: options.loadNative }),
319
405
  makePowerShellChannel({ exec: options.exec, env: options.env }),
320
406
  osc52,
321
407
  ];
322
408
  case "darwin":
323
409
  return [
410
+ makeNativeChannel({ loadNative: options.loadNative }),
324
411
  makePbpasteChannel({ exec: options.exec, env: options.env }),
325
412
  osc52,
326
413
  ];