pi-superwhisper-paste 0.1.5 → 0.1.14
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/CHANGELOG.md +59 -1
- package/README.md +2 -0
- package/docs/examples.md +6 -0
- package/extensions/index.ts +189 -12
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,13 +4,71 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project follows semantic versioning.
|
|
6
6
|
|
|
7
|
-
## [0.1.
|
|
7
|
+
## [0.1.14] - 2026-06-05
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Re-add `Code` / `Cursor` host processes to the clipboard-owner denylist so editor-hosted clipboard updates are blocked again during debugging.
|
|
12
|
+
|
|
13
|
+
## [0.1.13] - 2026-06-05
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Remove `Code.exe` / `Cursor.exe` from the terminal-owner denylist so editor-hosted Superwhisper clipboard updates are not blocked just because Pi is running inside Code/Cursor.
|
|
18
|
+
|
|
19
|
+
## [0.1.12] - 2026-06-05
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Add an explicit `v0.1.12` marker to the status text so stale package loads are visible during live debugging.
|
|
24
|
+
|
|
25
|
+
## [0.1.11] - 2026-06-05
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Broaden the terminal clipboard-owner denylist with `OpenConsole` / explicit `.exe` variants.
|
|
30
|
+
- Surface the last clipboard decision in the Pi status text (`blocked ...`, `... via ...`) so real-machine owner mismatches can be diagnosed quickly.
|
|
31
|
+
|
|
32
|
+
## [0.1.10] - 2026-06-05
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Fix the Windows PowerShell clipboard-owner probe script so the owner-aware bridge works in real Pi sessions instead of failing on multiline `Add-Type` syntax.
|
|
37
|
+
|
|
38
|
+
## [0.1.9] - 2026-06-05
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Replace the strict Superwhisper owner allowlist with a terminal-owner denylist so unknown Superwhisper owner names still auto-paste while terminal/CLI clipboard owners stay blocked.
|
|
43
|
+
|
|
44
|
+
## [0.1.8] - 2026-06-05
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- Filter clipboard updates by clipboard-owner process so Pi auto-pastes only when the clipboard owner looks like Superwhisper.
|
|
49
|
+
- Keep local terminal copy suppression as a fallback for forwarded copy shortcuts while blocking mouse/right-click terminal copies via owner filtering.
|
|
50
|
+
|
|
51
|
+
## [0.1.7] - 2026-06-05
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- Suppress auto-paste for a short window after local terminal copy shortcuts so copying inside the active CLI no longer pastes into Pi.
|
|
56
|
+
- Refresh the clipboard baseline after local copy intent so the bridge keeps waiting for the next Superwhisper clipboard update.
|
|
57
|
+
|
|
58
|
+
## [0.1.6] - 2026-06-04
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- Added `version:check` PR guard support: package script + `scripts/check-version-bump.mjs`.
|
|
63
|
+
- Added CI verification that publishable changes must bump `package.json` and update `CHANGELOG.md` in the same PR.
|
|
8
64
|
|
|
9
65
|
### Fixed
|
|
10
66
|
|
|
11
67
|
- Truncate clipboard text in PowerShell before writing stdout so huge clipboards no longer exceed Node `maxBuffer` on extension load.
|
|
12
68
|
- Treat clipboard read failures (including `maxBuffer`) as empty reads so `session_start` never surfaces as an extension error.
|
|
13
69
|
|
|
70
|
+
## [0.1.5] - 2026-06-03
|
|
71
|
+
|
|
14
72
|
## [0.1.4] - 2026-06-03
|
|
15
73
|
|
|
16
74
|
### Changed
|
package/README.md
CHANGED
|
@@ -50,6 +50,8 @@ pi install git:github.com/eiei114/pi-superwhisper-paste
|
|
|
50
50
|
- Default-on after the extension loads.
|
|
51
51
|
- Slash commands: `/sw-paste:on` and `/sw-paste:off`.
|
|
52
52
|
- Windows clipboard polling with active-tab gating.
|
|
53
|
+
- Brief copy-suppression window so local terminal copy actions do not get auto-pasted back into Pi.
|
|
54
|
+
- Clipboard-owner filtering so terminal/CLI-owned clipboard updates stay blocked by default.
|
|
53
55
|
|
|
54
56
|
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
55
57
|
|
package/docs/examples.md
CHANGED
|
@@ -28,6 +28,8 @@ Run these commands inside Pi's input box:
|
|
|
28
28
|
| `PI_SUPERWHISPER_PASTE` | `on` | Set to `off`, `0`, `false`, or `no` to disable by default. |
|
|
29
29
|
| `PI_SUPERWHISPER_PASTE_INTERVAL_MS` | `800` | Clipboard polling interval. |
|
|
30
30
|
| `PI_SUPERWHISPER_PASTE_MAX_CHARS` | `8000` | Maximum clipboard text length to paste. |
|
|
31
|
+
| `PI_SUPERWHISPER_PASTE_IGNORE_COPY_MS` | `1500` | Ignore clipboard changes briefly after local terminal copy shortcuts. |
|
|
32
|
+
| `PI_SUPERWHISPER_PASTE_OWNER_DENYLIST` | `windowsterminal,...` | Comma-separated clipboard owner process/path substrings blocked from auto-paste. |
|
|
31
33
|
|
|
32
34
|
## Expected workflow
|
|
33
35
|
|
|
@@ -40,3 +42,7 @@ Run these commands inside Pi's input box:
|
|
|
40
42
|
When multiple Pi tabs are open, only the tab that most recently had terminal focus or keyboard input should paste the new clipboard text.
|
|
41
43
|
|
|
42
44
|
The bridge does not wait for Pi's agent turn to become idle. If Pi is thinking, the active tab should still receive the next Superwhisper clipboard update.
|
|
45
|
+
|
|
46
|
+
When you copy text inside the active CLI, the bridge now suppresses auto-paste briefly and refreshes the clipboard baseline so the copied text is not inserted into Pi.
|
|
47
|
+
|
|
48
|
+
The bridge also inspects the Windows clipboard owner and blocks auto-paste when the owner looks like a terminal/CLI app.
|
package/extensions/index.ts
CHANGED
|
@@ -6,19 +6,53 @@ import { join } from "node:path";
|
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
|
+
const EXTENSION_VERSION = "0.1.14";
|
|
9
10
|
|
|
10
11
|
const STATUS_KEY = "superwhisper-paste";
|
|
11
12
|
const DEFAULT_INTERVAL_MS = 800;
|
|
12
13
|
const DEFAULT_MAX_CHARS = 8000;
|
|
14
|
+
const DEFAULT_IGNORE_COPY_MS = 1500;
|
|
15
|
+
const BASELINE_REFRESH_DELAY_MS = 120;
|
|
16
|
+
const DEFAULT_OWNER_DENYLIST = [
|
|
17
|
+
"windowsterminal",
|
|
18
|
+
"windowsterminal.exe",
|
|
19
|
+
"windows terminal",
|
|
20
|
+
"openconsole",
|
|
21
|
+
"openconsole.exe",
|
|
22
|
+
"wezterm",
|
|
23
|
+
"alacritty",
|
|
24
|
+
"mintty",
|
|
25
|
+
"conhost",
|
|
26
|
+
"cmd.exe",
|
|
27
|
+
"code",
|
|
28
|
+
"code.exe",
|
|
29
|
+
"powershell",
|
|
30
|
+
"pwsh",
|
|
31
|
+
"bash",
|
|
32
|
+
"cursor",
|
|
33
|
+
"cursor.exe",
|
|
34
|
+
"wsl",
|
|
35
|
+
];
|
|
13
36
|
const ACTIVE_STATE_FILE = "pi-superwhisper-paste-active.json";
|
|
14
37
|
const FOCUS_IN = "\x1b[I";
|
|
15
38
|
const FOCUS_OUT = "\x1b[O";
|
|
16
39
|
const ENABLE_FOCUS_REPORTING = "\x1b[?1004h";
|
|
17
40
|
const DISABLE_FOCUS_REPORTING = "\x1b[?1004l";
|
|
18
41
|
const INSTANCE_ID = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
42
|
+
const CTRL_C = "\x03";
|
|
43
|
+
const CTRL_INSERT = "\x1b[2;5~";
|
|
44
|
+
const CTRL_SHIFT_INSERT = "\x1b[2;6~";
|
|
45
|
+
const CTRL_SHIFT_C_CSI_U = "\x1b[99;6u";
|
|
46
|
+
const CTRL_SHIFT_UPPER_C_CSI_U = "\x1b[67;6u";
|
|
19
47
|
|
|
20
48
|
type BridgeMode = "off" | "on";
|
|
21
49
|
|
|
50
|
+
type ClipboardSnapshot = {
|
|
51
|
+
text?: string;
|
|
52
|
+
ownerProcessName?: string;
|
|
53
|
+
ownerProcessPath?: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
22
56
|
type PiEditorUi = {
|
|
23
57
|
setStatus(key: string, value: string | undefined): void;
|
|
24
58
|
notify(message: string, level?: "info" | "warning" | "error" | "success"): void;
|
|
@@ -42,11 +76,13 @@ type SessionHandle = {
|
|
|
42
76
|
type BridgeState = {
|
|
43
77
|
mode: BridgeMode;
|
|
44
78
|
interval?: NodeJS.Timeout;
|
|
79
|
+
baselineRefreshTimeout?: NodeJS.Timeout;
|
|
45
80
|
generation: number;
|
|
46
81
|
session?: SessionHandle;
|
|
47
82
|
inFlightGeneration?: number;
|
|
48
83
|
activatingGeneration?: number;
|
|
49
84
|
terminalFocused: boolean;
|
|
85
|
+
suppressPasteUntil?: number;
|
|
50
86
|
unsubscribeTerminalInput?: () => void;
|
|
51
87
|
lastClipboard?: string;
|
|
52
88
|
lastPasted?: string;
|
|
@@ -77,13 +113,44 @@ function maxChars(): number {
|
|
|
77
113
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CHARS;
|
|
78
114
|
}
|
|
79
115
|
|
|
116
|
+
/** Ignore clipboard changes briefly after local copy shortcuts. */
|
|
117
|
+
function ignoreCopyMs(): number {
|
|
118
|
+
const parsed = Number(process.env.PI_SUPERWHISPER_PASTE_IGNORE_COPY_MS);
|
|
119
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_IGNORE_COPY_MS;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Process-name/path substrings denied from triggering auto-paste. */
|
|
123
|
+
function ownerDenylist(): string[] {
|
|
124
|
+
const raw = process.env.PI_SUPERWHISPER_PASTE_OWNER_DENYLIST;
|
|
125
|
+
if (!raw) return DEFAULT_OWNER_DENYLIST;
|
|
126
|
+
|
|
127
|
+
const tokens = raw
|
|
128
|
+
.split(",")
|
|
129
|
+
.map((token) => token.trim().toLowerCase())
|
|
130
|
+
.filter(Boolean);
|
|
131
|
+
|
|
132
|
+
return tokens.length > 0 ? tokens : DEFAULT_OWNER_DENYLIST;
|
|
133
|
+
}
|
|
134
|
+
|
|
80
135
|
/** Status bar label for the paste bridge. */
|
|
81
136
|
function statusText(): string | undefined {
|
|
82
137
|
if (state.mode === "off") return undefined;
|
|
83
138
|
const focusText = state.terminalFocused ? "active" : "standby";
|
|
84
139
|
return state.lastPasteSummary
|
|
85
|
-
? `SW paste: ${focusText} (${state.lastPasteSummary})`
|
|
86
|
-
: `SW paste: ${focusText}`;
|
|
140
|
+
? `SW paste v${EXTENSION_VERSION}: ${focusText} (${state.lastPasteSummary})`
|
|
141
|
+
: `SW paste v${EXTENSION_VERSION}: ${focusText}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Short display name for clipboard owner debugging. */
|
|
145
|
+
function clipboardOwnerSummary(snapshot: ClipboardSnapshot): string {
|
|
146
|
+
const name = snapshot.ownerProcessName?.trim();
|
|
147
|
+
if (name) return name;
|
|
148
|
+
|
|
149
|
+
const rawPath = snapshot.ownerProcessPath?.trim();
|
|
150
|
+
if (!rawPath) return "unknown";
|
|
151
|
+
|
|
152
|
+
const segments = rawPath.split(/[\\/]/).filter(Boolean);
|
|
153
|
+
return segments.at(-1) ?? rawPath;
|
|
87
154
|
}
|
|
88
155
|
|
|
89
156
|
/** True when `ctx`/`generation` still match the active Pi session. */
|
|
@@ -164,17 +231,43 @@ function markPasted(ctx: PiRuntimeContext, generation: number, text: string): vo
|
|
|
164
231
|
function clipboardPowerShellScript(limit: number): string {
|
|
165
232
|
return [
|
|
166
233
|
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
|
|
234
|
+
"$signature = @\"",
|
|
235
|
+
"using System;",
|
|
236
|
+
"using System.Runtime.InteropServices;",
|
|
237
|
+
"public static class ClipboardOwnerNative {",
|
|
238
|
+
" [DllImport(\"user32.dll\")] public static extern IntPtr GetClipboardOwner();",
|
|
239
|
+
" [DllImport(\"user32.dll\")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);",
|
|
240
|
+
"}",
|
|
241
|
+
"\"@",
|
|
242
|
+
"Add-Type -TypeDefinition $signature -ErrorAction SilentlyContinue",
|
|
167
243
|
`$limit = ${limit}`,
|
|
168
244
|
"$text = Get-Clipboard -Raw -Format Text -ErrorAction SilentlyContinue",
|
|
245
|
+
"$ownerProcessName = $null",
|
|
246
|
+
"$ownerProcessPath = $null",
|
|
247
|
+
"try {",
|
|
248
|
+
" $ownerWindow = [ClipboardOwnerNative]::GetClipboardOwner()",
|
|
249
|
+
" if ($ownerWindow -ne [IntPtr]::Zero) {",
|
|
250
|
+
" $ownerPid = 0",
|
|
251
|
+
" [void][ClipboardOwnerNative]::GetWindowThreadProcessId($ownerWindow, [ref]$ownerPid)",
|
|
252
|
+
" if ($ownerPid -gt 0) {",
|
|
253
|
+
" $ownerProcess = Get-Process -Id $ownerPid -ErrorAction SilentlyContinue",
|
|
254
|
+
" if ($null -ne $ownerProcess) {",
|
|
255
|
+
" $ownerProcessName = $ownerProcess.ProcessName",
|
|
256
|
+
" $ownerProcessPath = $ownerProcess.Path",
|
|
257
|
+
" }",
|
|
258
|
+
" }",
|
|
259
|
+
" }",
|
|
260
|
+
"} catch {}",
|
|
169
261
|
"if ($null -ne $text) {",
|
|
170
262
|
" if ($text.Length -gt $limit) { $text = $text.Substring(0, $limit) }",
|
|
171
|
-
" [Console]::Out.Write($text)",
|
|
172
263
|
"}",
|
|
173
|
-
|
|
264
|
+
"$result = @{ text = $text; ownerProcessName = $ownerProcessName; ownerProcessPath = $ownerProcessPath }",
|
|
265
|
+
"[Console]::Out.Write(($result | ConvertTo-Json -Compress))",
|
|
266
|
+
].join("\n");
|
|
174
267
|
}
|
|
175
268
|
|
|
176
|
-
/** Reads clipboard text
|
|
177
|
-
async function
|
|
269
|
+
/** Reads clipboard text and owner metadata via PowerShell. */
|
|
270
|
+
async function readClipboardSnapshot(): Promise<ClipboardSnapshot | undefined> {
|
|
178
271
|
const limit = maxChars();
|
|
179
272
|
|
|
180
273
|
try {
|
|
@@ -190,8 +283,17 @@ async function readClipboardText(): Promise<string | undefined> {
|
|
|
190
283
|
);
|
|
191
284
|
|
|
192
285
|
const stdout = typeof result.stdout === "string" ? result.stdout : undefined;
|
|
193
|
-
if (stdout
|
|
194
|
-
|
|
286
|
+
if (!stdout) return undefined;
|
|
287
|
+
|
|
288
|
+
const snapshot = JSON.parse(stdout) as ClipboardSnapshot;
|
|
289
|
+
const text = typeof snapshot.text === "string" ? snapshot.text : undefined;
|
|
290
|
+
return {
|
|
291
|
+
text: text && text.length > limit ? text.slice(0, limit) : text,
|
|
292
|
+
ownerProcessName:
|
|
293
|
+
typeof snapshot.ownerProcessName === "string" ? snapshot.ownerProcessName : undefined,
|
|
294
|
+
ownerProcessPath:
|
|
295
|
+
typeof snapshot.ownerProcessPath === "string" ? snapshot.ownerProcessPath : undefined,
|
|
296
|
+
};
|
|
195
297
|
} catch {
|
|
196
298
|
// Oversized clipboard, PowerShell failure, or maxBuffer must not break extension load.
|
|
197
299
|
return undefined;
|
|
@@ -237,11 +339,26 @@ async function refreshClipboardBaseline(
|
|
|
237
339
|
ctx: PiRuntimeContext,
|
|
238
340
|
generation: number,
|
|
239
341
|
): Promise<void> {
|
|
240
|
-
const text = await
|
|
342
|
+
const text = (await readClipboardSnapshot())?.text;
|
|
241
343
|
if (!isCurrentSession(ctx, generation)) return;
|
|
242
344
|
if (text !== undefined) state.lastClipboard = text;
|
|
243
345
|
}
|
|
244
346
|
|
|
347
|
+
/** Best-effort delayed baseline refresh after local copy shortcuts. */
|
|
348
|
+
function scheduleClipboardBaselineRefresh(ctx: PiRuntimeContext, generation: number): void {
|
|
349
|
+
if (!isCurrentSession(ctx, generation)) return;
|
|
350
|
+
|
|
351
|
+
if (state.baselineRefreshTimeout) {
|
|
352
|
+
clearTimeout(state.baselineRefreshTimeout);
|
|
353
|
+
state.baselineRefreshTimeout = undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
state.baselineRefreshTimeout = setTimeout(() => {
|
|
357
|
+
state.baselineRefreshTimeout = undefined;
|
|
358
|
+
if (isCurrentSession(ctx, generation)) void refreshClipboardBaseline(ctx, generation);
|
|
359
|
+
}, BASELINE_REFRESH_DELAY_MS);
|
|
360
|
+
}
|
|
361
|
+
|
|
245
362
|
/** Marks tab focused, refreshes clipboard baseline, claims active tab. */
|
|
246
363
|
async function activateTab(ctx: PiRuntimeContext, generation: number): Promise<void> {
|
|
247
364
|
if (!isCurrentSession(ctx, generation) || state.activatingGeneration !== undefined) return;
|
|
@@ -269,6 +386,38 @@ async function markActiveFromInput(ctx: PiRuntimeContext, generation: number): P
|
|
|
269
386
|
setStatus(ctx, generation);
|
|
270
387
|
}
|
|
271
388
|
|
|
389
|
+
/** True while recent local copy shortcuts should suppress bridge pastes. */
|
|
390
|
+
function shouldSuppressPasteForRecentCopy(): boolean {
|
|
391
|
+
return (state.suppressPasteUntil ?? 0) > Date.now();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Detects common terminal copy shortcuts forwarded to Pi. */
|
|
395
|
+
function isLocalCopyShortcut(data: string): boolean {
|
|
396
|
+
return [
|
|
397
|
+
CTRL_C,
|
|
398
|
+
CTRL_INSERT,
|
|
399
|
+
CTRL_SHIFT_INSERT,
|
|
400
|
+
CTRL_SHIFT_C_CSI_U,
|
|
401
|
+
CTRL_SHIFT_UPPER_C_CSI_U,
|
|
402
|
+
].includes(data);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Suppresses auto-paste briefly after local terminal copy intent. */
|
|
406
|
+
function suppressPasteAfterLocalCopy(ctx: PiRuntimeContext, generation: number): void {
|
|
407
|
+
if (!isCurrentSession(ctx, generation)) return;
|
|
408
|
+
state.suppressPasteUntil = Date.now() + ignoreCopyMs();
|
|
409
|
+
state.lastPasteSummary = "blocked recent copy";
|
|
410
|
+
void markActiveFromInput(ctx, generation);
|
|
411
|
+
scheduleClipboardBaselineRefresh(ctx, generation);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** True when clipboard owner metadata does not look like a terminal copy source. */
|
|
415
|
+
function shouldAcceptClipboardSource(snapshot: ClipboardSnapshot): boolean {
|
|
416
|
+
const owner = `${snapshot.ownerProcessName ?? ""}\n${snapshot.ownerProcessPath ?? ""}`.toLowerCase();
|
|
417
|
+
if (!owner.trim()) return true;
|
|
418
|
+
return !ownerDenylist().some((token) => owner.includes(token));
|
|
419
|
+
}
|
|
420
|
+
|
|
272
421
|
/** Whether clipboard text should be auto-pasted into the editor. */
|
|
273
422
|
function shouldPaste(text: string, ctx: PiRuntimeContext, generation: number): boolean {
|
|
274
423
|
if (!text.trim()) return false;
|
|
@@ -298,17 +447,35 @@ async function pasteClipboardChange(ctx: PiRuntimeContext, generation: number):
|
|
|
298
447
|
|
|
299
448
|
state.inFlightGeneration = generation;
|
|
300
449
|
try {
|
|
301
|
-
const
|
|
450
|
+
const snapshot = await readClipboardSnapshot();
|
|
451
|
+
const text = snapshot?.text;
|
|
302
452
|
if (!isCurrentSession(ctx, generation)) return;
|
|
303
453
|
if (text === undefined || text === state.lastClipboard) return;
|
|
304
454
|
|
|
305
455
|
state.lastClipboard = text;
|
|
456
|
+
if (!snapshot) {
|
|
457
|
+
state.lastPasteSummary = "clipboard read failed";
|
|
458
|
+
setStatus(ctx, generation);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!shouldAcceptClipboardSource(snapshot)) {
|
|
462
|
+
state.lastPasteSummary = `blocked ${clipboardOwnerSummary(snapshot)}`;
|
|
463
|
+
setStatus(ctx, generation);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (shouldSuppressPasteForRecentCopy()) {
|
|
467
|
+
state.lastPasteSummary = `blocked recent copy (${clipboardOwnerSummary(snapshot)})`;
|
|
468
|
+
setStatus(ctx, generation);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
306
471
|
if (!shouldPaste(text, ctx, generation)) return;
|
|
307
472
|
if (!(await isActiveTab())) return;
|
|
308
473
|
if (!isCurrentSession(ctx, generation)) return;
|
|
309
474
|
|
|
310
475
|
if (!pasteToEditor(ctx, generation, text)) return;
|
|
311
476
|
markPasted(ctx, generation, text);
|
|
477
|
+
state.lastPasteSummary = `${text.length} chars via ${clipboardOwnerSummary(snapshot)}`;
|
|
478
|
+
setStatus(ctx, generation);
|
|
312
479
|
} catch {
|
|
313
480
|
// Clipboard polling should stay quiet while the user is typing.
|
|
314
481
|
} finally {
|
|
@@ -343,6 +510,11 @@ function setupTerminalFocusTracking(ctx: PiRuntimeContext, generation: number):
|
|
|
343
510
|
return { consume: true };
|
|
344
511
|
}
|
|
345
512
|
|
|
513
|
+
if (isLocalCopyShortcut(data)) {
|
|
514
|
+
suppressPasteAfterLocalCopy(ctx, generation);
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
|
|
346
518
|
if (data.length > 0) {
|
|
347
519
|
void markActiveFromInput(ctx, generation);
|
|
348
520
|
}
|
|
@@ -374,9 +546,14 @@ function teardownSessionResources(): void {
|
|
|
374
546
|
clearInterval(state.interval);
|
|
375
547
|
state.interval = undefined;
|
|
376
548
|
}
|
|
549
|
+
if (state.baselineRefreshTimeout) {
|
|
550
|
+
clearTimeout(state.baselineRefreshTimeout);
|
|
551
|
+
state.baselineRefreshTimeout = undefined;
|
|
552
|
+
}
|
|
377
553
|
teardownTerminalFocusTracking();
|
|
378
554
|
state.inFlightGeneration = undefined;
|
|
379
555
|
state.activatingGeneration = undefined;
|
|
556
|
+
state.suppressPasteUntil = undefined;
|
|
380
557
|
}
|
|
381
558
|
|
|
382
559
|
/** Starts a new session generation and tears down the previous one. */
|
|
@@ -423,7 +600,7 @@ function stopPolling(ctx: PiRuntimeContext, generation: number): void {
|
|
|
423
600
|
|
|
424
601
|
/** Baselines clipboard then starts polling (session_start path). */
|
|
425
602
|
async function startPolling(ctx: PiRuntimeContext, generation: number): Promise<void> {
|
|
426
|
-
const text = await
|
|
603
|
+
const text = (await readClipboardSnapshot())?.text;
|
|
427
604
|
if (!isCurrentSession(ctx, generation)) return;
|
|
428
605
|
state.lastClipboard = text;
|
|
429
606
|
ensurePolling(ctx, generation);
|
|
@@ -431,7 +608,7 @@ async function startPolling(ctx: PiRuntimeContext, generation: number): Promise<
|
|
|
431
608
|
|
|
432
609
|
/** Enables bridge mode and notifies the user. */
|
|
433
610
|
async function arm(ctx: PiRuntimeContext, generation: number): Promise<void> {
|
|
434
|
-
const text = await
|
|
611
|
+
const text = (await readClipboardSnapshot())?.text;
|
|
435
612
|
if (!isCurrentSession(ctx, generation)) return;
|
|
436
613
|
state.lastClipboard = text;
|
|
437
614
|
state.mode = "on";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-superwhisper-paste",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Superwhisper clipboard bridge for Pi on Windows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"typecheck": "tsc --noEmit",
|
|
38
38
|
"test": "node --test tests/*.test.mjs",
|
|
39
39
|
"ci": "npm run typecheck && npm test && npm run pack:check",
|
|
40
|
-
"pack:check": "npm pack --dry-run"
|
|
40
|
+
"pack:check": "npm pack --dry-run",
|
|
41
|
+
"version:check": "node scripts/check-version-bump.mjs"
|
|
41
42
|
},
|
|
42
43
|
"pi": {
|
|
43
44
|
"extensions": [
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
"@earendil-works/pi-ai": "latest",
|
|
57
58
|
"@earendil-works/pi-coding-agent": "latest",
|
|
58
59
|
"@earendil-works/pi-tui": "latest",
|
|
59
|
-
"@types/node": "^
|
|
60
|
+
"@types/node": "^25.9.1",
|
|
60
61
|
"typescript": "^6.0.3"
|
|
61
62
|
}
|
|
62
63
|
}
|