pi-superwhisper-paste 0.1.5 → 0.1.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/CHANGELOG.md +65 -1
- package/README.md +2 -0
- package/docs/examples.md +6 -0
- package/extensions/index.ts +189 -12
- package/package.json +4 -3
- package/docs/github-template.md +0 -43
- package/docs/repository-settings.md +0 -40
- package/docs/typescript.md +0 -75
package/CHANGELOG.md
CHANGED
|
@@ -4,13 +4,77 @@ 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.15] - 2026-06-05
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Remove stale template-maintenance docs (`github-template.md`, `repository-settings.md`, `typescript.md`) from the published package.
|
|
12
|
+
|
|
13
|
+
## [0.1.14] - 2026-06-05
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Re-add `Code` / `Cursor` host processes to the clipboard-owner denylist so editor-hosted clipboard updates are blocked again during debugging.
|
|
18
|
+
|
|
19
|
+
## [0.1.13] - 2026-06-05
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- 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.
|
|
24
|
+
|
|
25
|
+
## [0.1.12] - 2026-06-05
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Add an explicit `v0.1.12` marker to the status text so stale package loads are visible during live debugging.
|
|
30
|
+
|
|
31
|
+
## [0.1.11] - 2026-06-05
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Broaden the terminal clipboard-owner denylist with `OpenConsole` / explicit `.exe` variants.
|
|
36
|
+
- Surface the last clipboard decision in the Pi status text (`blocked ...`, `... via ...`) so real-machine owner mismatches can be diagnosed quickly.
|
|
37
|
+
|
|
38
|
+
## [0.1.10] - 2026-06-05
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- 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.
|
|
43
|
+
|
|
44
|
+
## [0.1.9] - 2026-06-05
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- 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.
|
|
49
|
+
|
|
50
|
+
## [0.1.8] - 2026-06-05
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- Filter clipboard updates by clipboard-owner process so Pi auto-pastes only when the clipboard owner looks like Superwhisper.
|
|
55
|
+
- Keep local terminal copy suppression as a fallback for forwarded copy shortcuts while blocking mouse/right-click terminal copies via owner filtering.
|
|
56
|
+
|
|
57
|
+
## [0.1.7] - 2026-06-05
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- Suppress auto-paste for a short window after local terminal copy shortcuts so copying inside the active CLI no longer pastes into Pi.
|
|
62
|
+
- Refresh the clipboard baseline after local copy intent so the bridge keeps waiting for the next Superwhisper clipboard update.
|
|
63
|
+
|
|
64
|
+
## [0.1.6] - 2026-06-04
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
|
|
68
|
+
- Added `version:check` PR guard support: package script + `scripts/check-version-bump.mjs`.
|
|
69
|
+
- Added CI verification that publishable changes must bump `package.json` and update `CHANGELOG.md` in the same PR.
|
|
8
70
|
|
|
9
71
|
### Fixed
|
|
10
72
|
|
|
11
73
|
- Truncate clipboard text in PowerShell before writing stdout so huge clipboards no longer exceed Node `maxBuffer` on extension load.
|
|
12
74
|
- Treat clipboard read failures (including `maxBuffer`) as empty reads so `session_start` never surfaces as an extension error.
|
|
13
75
|
|
|
76
|
+
## [0.1.5] - 2026-06-03
|
|
77
|
+
|
|
14
78
|
## [0.1.4] - 2026-06-03
|
|
15
79
|
|
|
16
80
|
### 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.15",
|
|
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
|
}
|
package/docs/github-template.md
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# GitHub Template Repository
|
|
2
|
-
|
|
3
|
-
Enable template mode on the source repository:
|
|
4
|
-
|
|
5
|
-
```txt
|
|
6
|
-
GitHub repo → Settings → General → Template repository
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
Create a public repository from the template:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
gh repo create OWNER/new-pi-extension \
|
|
13
|
-
--public \
|
|
14
|
-
--template OWNER/pi-extension-template \
|
|
15
|
-
--clone
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Create a private repository from the template:
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
gh repo create OWNER/new-pi-extension \
|
|
22
|
-
--private \
|
|
23
|
-
--template OWNER/pi-extension-template \
|
|
24
|
-
--clone
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Include all branches if needed:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
gh repo create OWNER/new-pi-extension \
|
|
31
|
-
--public \
|
|
32
|
-
--template OWNER/pi-extension-template \
|
|
33
|
-
--include-all-branches \
|
|
34
|
-
--clone
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
After creation:
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
cd new-pi-extension
|
|
41
|
-
npm install
|
|
42
|
-
npm run ci
|
|
43
|
-
```
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# Repository Settings
|
|
2
|
-
|
|
3
|
-
Use this file after creating a real repository from the template.
|
|
4
|
-
|
|
5
|
-
## GitHub About
|
|
6
|
-
|
|
7
|
-
Suggested fields:
|
|
8
|
-
|
|
9
|
-
- Description: Bridge Superwhisper clipboard dictation into the Pi TUI editor on Windows.
|
|
10
|
-
- Website: npm package URL or project docs URL
|
|
11
|
-
- Topics:
|
|
12
|
-
- `pi`
|
|
13
|
-
- `pi-package`
|
|
14
|
-
- `pi-extension`
|
|
15
|
-
- `superwhisper`
|
|
16
|
-
- `clipboard`
|
|
17
|
-
- `windows`
|
|
18
|
-
- `typescript`
|
|
19
|
-
|
|
20
|
-
## Template mode
|
|
21
|
-
|
|
22
|
-
This repository is a concrete package, not a template. Do not enable template mode unless it is intentionally turned into a reusable starter.
|
|
23
|
-
|
|
24
|
-
## Branch protection
|
|
25
|
-
|
|
26
|
-
Recommended for public packages:
|
|
27
|
-
|
|
28
|
-
- Require pull request before merging
|
|
29
|
-
- Require status checks to pass
|
|
30
|
-
- Require `CI` workflow
|
|
31
|
-
- Block force pushes on the default branch
|
|
32
|
-
|
|
33
|
-
## npm package page
|
|
34
|
-
|
|
35
|
-
After first publish:
|
|
36
|
-
|
|
37
|
-
- Confirm README renders correctly
|
|
38
|
-
- Confirm package provenance appears
|
|
39
|
-
- Confirm package contents are intentional
|
|
40
|
-
- Add npm URL to GitHub About and README
|
package/docs/typescript.md
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# TypeScript Guide
|
|
2
|
-
|
|
3
|
-
This is a TypeScript-first Pi package template.
|
|
4
|
-
|
|
5
|
-
## Layout
|
|
6
|
-
|
|
7
|
-
```txt
|
|
8
|
-
extensions/*.ts Pi extension entrypoints
|
|
9
|
-
lib/*.ts Shared TypeScript helpers
|
|
10
|
-
skills/*/SKILL.md Agent Skills
|
|
11
|
-
prompts/*.md Prompt templates
|
|
12
|
-
themes/*.json Themes
|
|
13
|
-
tests/*.test.mjs Smoke tests
|
|
14
|
-
tests/*.test.ts Optional TypeScript tests if you add a TS test runner
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Pi loads TypeScript extensions directly, so no build step is required for normal use.
|
|
18
|
-
|
|
19
|
-
## Strict mode
|
|
20
|
-
|
|
21
|
-
`tsconfig.json` keeps `strict: true`. Prefer fixing types over loosening compiler options.
|
|
22
|
-
|
|
23
|
-
## Extension entrypoints
|
|
24
|
-
|
|
25
|
-
Two entrypoint styles are shown:
|
|
26
|
-
|
|
27
|
-
- `extensions/hello.ts`: single-file extension
|
|
28
|
-
- `extensions/index.ts`: index-style extension that imports shared code from `lib/`
|
|
29
|
-
|
|
30
|
-
For larger packages, keep entrypoints thin and put reusable logic in `lib/`.
|
|
31
|
-
|
|
32
|
-
## TypeBox schemas
|
|
33
|
-
|
|
34
|
-
Use TypeBox schemas for custom tool parameters.
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
import { Type } from "typebox";
|
|
38
|
-
|
|
39
|
-
const parameters = Type.Object({
|
|
40
|
-
name: Type.String({ description: "Name to greet" }),
|
|
41
|
-
});
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## String enums
|
|
45
|
-
|
|
46
|
-
For string choices, use the local `StringEnum` helper from `lib/schema.ts`.
|
|
47
|
-
|
|
48
|
-
```ts
|
|
49
|
-
import { StringEnum } from "../lib/schema.ts";
|
|
50
|
-
|
|
51
|
-
const mode = StringEnum(["short", "friendly"], {
|
|
52
|
-
description: "Greeting style",
|
|
53
|
-
});
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
This emits a JSON Schema `enum`, which is friendlier to model providers than a union of string literals.
|
|
57
|
-
|
|
58
|
-
## Runtime dependencies vs peer dependencies
|
|
59
|
-
|
|
60
|
-
Pi bundles core packages for extension authors. Keep Pi-provided packages as `peerDependencies` and also install them as `devDependencies` for local typechecking.
|
|
61
|
-
|
|
62
|
-
Use `peerDependencies` for:
|
|
63
|
-
|
|
64
|
-
- `@earendil-works/pi-coding-agent`
|
|
65
|
-
- `@earendil-works/pi-ai`
|
|
66
|
-
- `@earendil-works/pi-tui`
|
|
67
|
-
- `typebox`
|
|
68
|
-
|
|
69
|
-
Use `dependencies` for runtime packages your extension imports that Pi does not provide.
|
|
70
|
-
|
|
71
|
-
Use `devDependencies` for local-only tools such as TypeScript, test runners, and linters.
|
|
72
|
-
|
|
73
|
-
## Package contents
|
|
74
|
-
|
|
75
|
-
Control npm package contents with `package.json` `files`. Prefer this over `.npmignore` so the published package stays explicit.
|