@selesai/code 0.13.26 → 0.13.27

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 (119) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/cli/args.js +1 -0
  3. package/dist/cli/session-picker.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/core/agent-session-auto-handoff.test.js +2 -1
  6. package/dist/core/agent-session-length-continuation.test.js +2 -1
  7. package/dist/core/agent-session.d.ts +58 -7
  8. package/dist/core/agent-session.js +334 -167
  9. package/dist/core/bug-report-upload.d.ts +11 -0
  10. package/dist/core/bug-report-upload.js +21 -0
  11. package/dist/core/bug-report.d.ts +175 -0
  12. package/dist/core/bug-report.js +291 -0
  13. package/dist/core/cache-stats.js +12 -1
  14. package/dist/core/cache-warmer.d.ts +101 -0
  15. package/dist/core/cache-warmer.js +341 -0
  16. package/dist/core/compaction/branch-summarization.js +2 -2
  17. package/dist/core/compaction/compaction.d.ts +6 -5
  18. package/dist/core/compaction/compaction.js +49 -42
  19. package/dist/core/crash-log.d.ts +21 -0
  20. package/dist/core/crash-log.js +70 -0
  21. package/dist/core/experimental.d.ts +0 -4
  22. package/dist/core/experimental.js +0 -4
  23. package/dist/core/extensions/index.d.ts +1 -1
  24. package/dist/core/extensions/jiti-loader.d.ts +1 -0
  25. package/dist/core/extensions/jiti-loader.js +3 -0
  26. package/dist/core/extensions/jiti-static-loader.d.ts +1 -0
  27. package/dist/core/extensions/jiti-static-loader.js +3 -0
  28. package/dist/core/extensions/loader.js +39 -49
  29. package/dist/core/extensions/runner.d.ts +10 -7
  30. package/dist/core/extensions/runner.js +85 -67
  31. package/dist/core/extensions/types.d.ts +66 -52
  32. package/dist/core/extensions/virtual-modules.d.ts +2 -0
  33. package/dist/core/extensions/virtual-modules.js +39 -0
  34. package/dist/core/extensions/wrapper.js +1 -20
  35. package/dist/core/index.d.ts +2 -1
  36. package/dist/core/keybindings.d.ts +1 -1
  37. package/dist/core/keybindings.js +1 -1
  38. package/dist/core/messages.js +1 -0
  39. package/dist/core/model-config.d.ts +101 -20
  40. package/dist/core/model-config.js +25 -18
  41. package/dist/core/model-resolver.js +2 -1
  42. package/dist/core/model-runtime.js +5 -3
  43. package/dist/core/provider-composer.d.ts +7 -2
  44. package/dist/core/provider-composer.js +10 -5
  45. package/dist/core/radius.d.ts +3 -0
  46. package/dist/core/radius.js +6 -0
  47. package/dist/core/sdk.js +60 -38
  48. package/dist/core/session-export.d.ts +6 -2
  49. package/dist/core/session-export.js +14 -13
  50. package/dist/core/session-format.test.d.ts +1 -0
  51. package/dist/core/session-format.test.js +96 -0
  52. package/dist/core/session-manager.d.ts +29 -6
  53. package/dist/core/session-manager.js +159 -96
  54. package/dist/core/settings-manager.d.ts +22 -5
  55. package/dist/core/settings-manager.js +56 -23
  56. package/dist/core/slash-commands.js +1 -0
  57. package/dist/core/system-prompt.d.ts +49 -4
  58. package/dist/core/system-prompt.js +155 -102
  59. package/dist/core/system-prompt.test.js +61 -23
  60. package/dist/core/tools/bash.d.ts +2 -1
  61. package/dist/core/tools/bash.js +10 -4
  62. package/dist/core/tools/edit.js +1 -2
  63. package/dist/core/tools/read.js +1 -2
  64. package/dist/core/tools/renderers/bash.js +9 -1
  65. package/dist/core/tools/write.js +1 -2
  66. package/dist/core/usage-totals.d.ts +1 -1
  67. package/dist/core/usage-totals.js +5 -1
  68. package/dist/extensions/pi-web-agent/src/backends/factory.ts +14 -6
  69. package/dist/extensions/pi-web-agent/src/extension.ts +3 -1
  70. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +7 -3
  71. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +3 -0
  72. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +2 -0
  73. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +4 -2
  74. package/dist/extensions/pi-web-agent/src/search/tokenin.ts +4 -1
  75. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +2 -1
  76. package/dist/extensions/pi-web-agent/src/types.ts +4 -0
  77. package/dist/index.d.ts +3 -2
  78. package/dist/main.js +13 -9
  79. package/dist/migrations.js +2 -2
  80. package/dist/modes/interactive/bug-report.d.ts +15 -0
  81. package/dist/modes/interactive/bug-report.js +205 -0
  82. package/dist/modes/interactive/chat-viewport.js +1 -1
  83. package/dist/modes/interactive/components/branch-summary-message.js +12 -5
  84. package/dist/modes/interactive/components/compaction-summary-message.js +12 -5
  85. package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
  86. package/dist/modes/interactive/components/extension-editor.d.ts +4 -1
  87. package/dist/modes/interactive/components/extension-editor.js +7 -2
  88. package/dist/modes/interactive/components/extension-input.d.ts +2 -0
  89. package/dist/modes/interactive/components/extension-input.js +6 -0
  90. package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
  91. package/dist/modes/interactive/components/extension-selector.js +4 -0
  92. package/dist/modes/interactive/components/footer.js +4 -1
  93. package/dist/modes/interactive/components/session-selector.d.ts +5 -5
  94. package/dist/modes/interactive/components/session-selector.js +74 -48
  95. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  96. package/dist/modes/interactive/components/settings-selector.js +11 -0
  97. package/dist/modes/interactive/components/skill-invocation-message.js +11 -4
  98. package/dist/modes/interactive/components/status-indicator.d.ts +2 -2
  99. package/dist/modes/interactive/components/status-indicator.js +7 -7
  100. package/dist/modes/interactive/components/tool-execution.js +18 -9
  101. package/dist/modes/interactive/components/tree-selector.js +2 -0
  102. package/dist/modes/interactive/interactive-mode.d.ts +15 -2
  103. package/dist/modes/interactive/interactive-mode.js +257 -81
  104. package/dist/modes/interactive/session-share.d.ts +2 -0
  105. package/dist/modes/interactive/session-share.js +8 -4
  106. package/dist/modes/interactive/tui-renderer.js +2 -2
  107. package/dist/modes/rpc/rpc-mode.js +2 -2
  108. package/dist/utils/clipboard-command.d.ts +6 -0
  109. package/dist/utils/clipboard-command.js +44 -0
  110. package/dist/utils/clipboard-image.js +55 -89
  111. package/dist/utils/clipboard.d.ts +1 -1
  112. package/dist/utils/clipboard.js +118 -108
  113. package/dist/utils/wsl.d.ts +2 -0
  114. package/dist/utils/wsl.js +14 -0
  115. package/dist/utils/zip.d.ts +6 -0
  116. package/dist/utils/zip.js +59 -0
  117. package/package.json +9 -9
  118. package/dist/utils/clipboard-native.d.ts +0 -10
  119. package/dist/utils/clipboard-native.js +0 -19
@@ -24,8 +24,8 @@ export function createInteractiveTui(options) {
24
24
  await copyToClipboard(text);
25
25
  return true;
26
26
  }
27
- catch {
28
- return false;
27
+ catch (error) {
28
+ return error instanceof Error ? error.message : String(error);
29
29
  }
30
30
  },
31
31
  });
@@ -435,11 +435,11 @@ export async function runRpcMode(runtimeHost) {
435
435
  return undefined;
436
436
  }
437
437
  case "steer": {
438
- await session.steer(command.message, command.images);
438
+ await session.steer(command.message, command.images, { source: "rpc" });
439
439
  return success(id, "steer");
440
440
  }
441
441
  case "follow_up": {
442
- await session.followUp(command.message, command.images);
442
+ await session.followUp(command.message, command.images, { source: "rpc" });
443
443
  return success(id, "follow_up");
444
444
  }
445
445
  case "abort": {
@@ -0,0 +1,6 @@
1
+ /** Undefined means the command failed; an empty buffer is a successful result. */
2
+ export declare function runClipboardCommand(command: string, args: readonly string[], options?: {
3
+ input?: string;
4
+ timeoutMs?: number;
5
+ maxBufferBytes?: number;
6
+ }): Promise<Buffer | undefined>;
@@ -0,0 +1,44 @@
1
+ import { spawn } from "node:child_process";
2
+ /** Undefined means the command failed; an empty buffer is a successful result. */
3
+ export function runClipboardCommand(command, args, options) {
4
+ return new Promise((resolve) => {
5
+ // Clipboard writers can daemonize. Do not give them output pipes to retain.
6
+ const child = spawn(command, args, {
7
+ stdio: ["pipe", options?.input === undefined ? "pipe" : "ignore", "ignore"],
8
+ windowsHide: true,
9
+ });
10
+ const chunks = [];
11
+ let length = 0;
12
+ let settled = false;
13
+ const finish = (result) => {
14
+ if (settled)
15
+ return;
16
+ settled = true;
17
+ clearTimeout(timer);
18
+ resolve(result);
19
+ };
20
+ const abort = () => {
21
+ child.kill("SIGKILL");
22
+ child.stdout?.destroy();
23
+ child.stdin?.destroy();
24
+ finish(undefined);
25
+ };
26
+ const timer = setTimeout(abort, options?.timeoutMs ?? 3000);
27
+ child.on("error", () => finish(undefined));
28
+ child.on("close", (code) => {
29
+ if (!settled)
30
+ finish(code === 0 ? Buffer.concat(chunks, length) : undefined);
31
+ });
32
+ child.stdout?.on("data", (chunk) => {
33
+ if (settled)
34
+ return;
35
+ length += chunk.length;
36
+ if (length > (options?.maxBufferBytes ?? 50 * 1024 * 1024))
37
+ abort();
38
+ else
39
+ chunks.push(chunk);
40
+ });
41
+ child.stdin?.on("error", () => { }); // A writer may exit before consuming all input.
42
+ child.stdin?.end(options?.input);
43
+ });
44
+ }
@@ -1,15 +1,15 @@
1
- import { spawnSync } from "child_process";
1
+ import { getNativeClipboard } from "@earendil-works/pi-tui";
2
2
  import { randomUUID } from "crypto";
3
3
  import { readFileSync, unlinkSync } from "fs";
4
4
  import { tmpdir } from "os";
5
5
  import { join } from "path";
6
- import { clipboard } from "./clipboard-native.js";
6
+ import { runClipboardCommand } from "./clipboard-command.js";
7
+ import { detectSupportedImageMimeType } from "./mime.js";
7
8
  import { loadPhoton } from "./photon.js";
9
+ import { isWSL } from "./wsl.js";
8
10
  const SUPPORTED_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
9
11
  const DEFAULT_LIST_TIMEOUT_MS = 1000;
10
- const DEFAULT_READ_TIMEOUT_MS = 3000;
11
12
  const DEFAULT_POWERSHELL_TIMEOUT_MS = 5000;
12
- const DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
13
13
  export function isWaylandSession(env = process.env) {
14
14
  return Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === "wayland";
15
15
  }
@@ -70,31 +70,13 @@ async function convertToPng(bytes) {
70
70
  return null;
71
71
  }
72
72
  }
73
- function runCommand(command, args, options) {
74
- const timeoutMs = options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS;
75
- const maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
76
- const result = spawnSync(command, args, {
77
- timeout: timeoutMs,
78
- maxBuffer: maxBufferBytes,
79
- env: options?.env,
80
- });
81
- if (result.error) {
82
- return { ok: false, stdout: Buffer.alloc(0) };
83
- }
84
- if (result.status !== 0) {
85
- return { ok: false, stdout: Buffer.alloc(0) };
86
- }
87
- const stdout = Buffer.isBuffer(result.stdout)
88
- ? result.stdout
89
- : Buffer.from(result.stdout ?? "", typeof result.stdout === "string" ? "utf-8" : undefined);
90
- return { ok: true, stdout };
91
- }
92
- function readClipboardImageViaWlPaste() {
93
- const list = runCommand("wl-paste", ["--list-types"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
94
- if (!list.ok) {
95
- return null;
96
- }
97
- const types = list.stdout
73
+ // Undefined means the backend failed; null means it has no image. An empty
74
+ // Wayland clipboard must not fall through to stale X11 clipboard contents.
75
+ async function readClipboardImageViaWlPaste() {
76
+ const list = await runClipboardCommand("wl-paste", ["--list-types"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
77
+ if (list === undefined)
78
+ return undefined;
79
+ const types = list
98
80
  .toString("utf-8")
99
81
  .split(/\r?\n/)
100
82
  .map((t) => t.trim())
@@ -103,40 +85,28 @@ function readClipboardImageViaWlPaste() {
103
85
  if (!selectedType) {
104
86
  return null;
105
87
  }
106
- const data = runCommand("wl-paste", ["--type", selectedType, "--no-newline"]);
107
- if (!data.ok || data.stdout.length === 0) {
88
+ const data = await runClipboardCommand("wl-paste", ["--type", selectedType, "--no-newline"]);
89
+ if (data === undefined)
90
+ return undefined;
91
+ if (data.length === 0)
108
92
  return null;
109
- }
110
- return { bytes: data.stdout, mimeType: baseMimeType(selectedType) };
111
- }
112
- function isWSL(env = process.env) {
113
- if (env.WSL_DISTRO_NAME || env.WSLENV) {
114
- return true;
115
- }
116
- try {
117
- const release = readFileSync("/proc/version", "utf-8");
118
- return /microsoft|wsl/i.test(release);
119
- }
120
- catch {
121
- return false;
122
- }
93
+ return { bytes: data, mimeType: baseMimeType(selectedType) };
123
94
  }
124
95
  /**
125
96
  * On WSL, the Linux clipboard (Wayland/X11) does not receive image data from
126
97
  * Windows screenshots (Win+Shift+S). PowerShell can access the Windows clipboard
127
98
  * directly, so we use it as a fallback.
128
99
  */
129
- function readClipboardImageViaPowerShell(options) {
130
- const tmpFile = join(tmpdir(), `pi-clip-${randomUUID()}.png`);
100
+ async function readClipboardImageViaPowerShell() {
101
+ const tmpFile = join(tmpdir(), `pi-wsl-clip-${randomUUID()}.png`);
131
102
  try {
132
- let winPath = tmpFile;
133
- if (options?.wsl) {
134
- const winPathResult = runCommand("wslpath", ["-w", tmpFile], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
135
- if (!winPathResult.ok) {
136
- return null;
137
- }
138
- winPath = winPathResult.stdout.toString("utf-8").trim();
103
+ const winPathResult = await runClipboardCommand("wslpath", ["-w", tmpFile], {
104
+ timeoutMs: DEFAULT_LIST_TIMEOUT_MS,
105
+ });
106
+ if (winPathResult === undefined) {
107
+ return null;
139
108
  }
109
+ const winPath = winPathResult.toString("utf-8").trim();
140
110
  if (!winPath) {
141
111
  return null;
142
112
  }
@@ -148,13 +118,13 @@ function readClipboardImageViaPowerShell(options) {
148
118
  "$img = [System.Windows.Forms.Clipboard]::GetImage()",
149
119
  "if ($img) { $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }",
150
120
  ].join("; ");
151
- const result = runCommand("powershell.exe", ["-NoProfile", "-STA", "-Command", psScript], {
121
+ const result = await runClipboardCommand("powershell.exe", ["-NoProfile", "-Command", psScript], {
152
122
  timeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS,
153
123
  });
154
- if (!result.ok) {
124
+ if (result === undefined) {
155
125
  return null;
156
126
  }
157
- const output = result.stdout.toString("utf-8").trim();
127
+ const output = result.toString("utf-8").trim();
158
128
  if (output !== "ok") {
159
129
  return null;
160
130
  }
@@ -176,38 +146,37 @@ function readClipboardImageViaPowerShell(options) {
176
146
  }
177
147
  }
178
148
  }
179
- function readClipboardImageViaXclip() {
180
- const targets = runCommand("xclip", ["-selection", "clipboard", "-t", "TARGETS", "-o"], {
149
+ async function readClipboardImageViaXclip() {
150
+ const targets = await runClipboardCommand("xclip", ["-selection", "clipboard", "-t", "TARGETS", "-o"], {
181
151
  timeoutMs: DEFAULT_LIST_TIMEOUT_MS,
182
152
  });
183
153
  let candidateTypes = [];
184
- if (targets.ok) {
185
- candidateTypes = targets.stdout
154
+ if (targets !== undefined) {
155
+ candidateTypes = targets
186
156
  .toString("utf-8")
187
157
  .split(/\r?\n/)
188
158
  .map((t) => t.trim())
189
159
  .filter(Boolean);
190
160
  }
191
- const preferred = candidateTypes.length > 0 ? selectPreferredImageMimeType(candidateTypes) : null;
192
- const tryTypes = preferred ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] : [...SUPPORTED_IMAGE_MIME_TYPES];
161
+ const preferred = selectPreferredImageMimeType(candidateTypes);
162
+ if (targets !== undefined && !preferred)
163
+ return null;
164
+ const tryTypes = new Set(preferred ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] : SUPPORTED_IMAGE_MIME_TYPES);
193
165
  for (const mimeType of tryTypes) {
194
- const data = runCommand("xclip", ["-selection", "clipboard", "-t", mimeType, "-o"]);
195
- if (data.ok && data.stdout.length > 0) {
196
- return { bytes: data.stdout, mimeType: baseMimeType(mimeType) };
166
+ const data = await runClipboardCommand("xclip", ["-selection", "clipboard", "-t", mimeType, "-o"]);
167
+ if (data !== undefined && data.length > 0) {
168
+ return { bytes: data, mimeType: baseMimeType(mimeType) };
197
169
  }
198
170
  }
199
- return null;
171
+ return undefined;
200
172
  }
201
173
  async function readClipboardImageViaNativeClipboard() {
202
- if (!clipboard || !clipboard.hasImage()) {
174
+ const bytes = await getNativeClipboard()?.getImage();
175
+ if (bytes === undefined)
176
+ return undefined;
177
+ if (!bytes?.length)
203
178
  return null;
204
- }
205
- const imageData = await clipboard.getImageBinary();
206
- if (!imageData || imageData.length === 0) {
207
- return null;
208
- }
209
- const bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);
210
- return { bytes, mimeType: "image/png" };
179
+ return { bytes, mimeType: detectSupportedImageMimeType(bytes) ?? "application/octet-stream" };
211
180
  }
212
181
  export async function readClipboardImage(options) {
213
182
  const env = options?.env ?? process.env;
@@ -215,22 +184,19 @@ export async function readClipboardImage(options) {
215
184
  if (env.TERMUX_VERSION) {
216
185
  return null;
217
186
  }
218
- let image = null;
187
+ let image;
219
188
  if (platform === "linux") {
220
189
  const wsl = isWSL(env);
221
- const wayland = isWaylandSession(env);
222
- if (wayland || wsl) {
223
- image = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();
224
- }
225
- if (!image && wsl) {
226
- image = readClipboardImageViaPowerShell({ wsl: true });
190
+ if (isWaylandSession(env) || wsl) {
191
+ image = await readClipboardImageViaWlPaste();
227
192
  }
228
- if (!image && !wayland) {
229
- image = (await readClipboardImageViaNativeClipboard()) ?? readClipboardImageViaXclip();
230
- }
231
- }
232
- else if (platform === "win32") {
233
- image = await readClipboardImageViaNativeClipboard() ?? readClipboardImageViaPowerShell();
193
+ if (image === undefined)
194
+ image = await readClipboardImageViaXclip();
195
+ // Preserve Linux's empty/unavailable distinction if Windows has no image.
196
+ if (!image && wsl)
197
+ image = (await readClipboardImageViaPowerShell()) ?? image;
198
+ if (image === undefined)
199
+ image = await readClipboardImageViaNativeClipboard();
234
200
  }
235
201
  else {
236
202
  image = await readClipboardImageViaNativeClipboard();
@@ -238,7 +204,7 @@ export async function readClipboardImage(options) {
238
204
  if (!image) {
239
205
  return null;
240
206
  }
241
- // Convert unsupported formats (e.g., BMP from WSLg) to PNG
207
+ // Convert unsupported formats (e.g., Windows DIB data wrapped as BMP) to PNG
242
208
  if (!isSupportedImageMimeType(image.mimeType)) {
243
209
  const pngBytes = await convertToPng(image.bytes);
244
210
  if (!pngBytes) {
@@ -1,3 +1,3 @@
1
- /** Read plain text from the system clipboard, if native clipboard access is available. */
1
+ /** Read plain text from the system clipboard. */
2
2
  export declare function readClipboardText(): Promise<string | null>;
3
3
  export declare function copyToClipboard(text: string): Promise<void>;
@@ -1,17 +1,12 @@
1
- import { execSync, spawn } from "child_process";
2
- import { platform } from "os";
3
- import { isWaylandSession } from "./clipboard-image.js";
4
- import { clipboard } from "./clipboard-native.js";
5
- function copyToX11Clipboard(options) {
6
- try {
7
- execSync("xclip -selection clipboard", options);
8
- }
9
- catch {
10
- execSync("xsel --clipboard --input", options);
11
- }
12
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { unlinkSync, writeFileSync } from "node:fs";
3
+ import { platform, tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { getNativeClipboard } from "@earendil-works/pi-tui";
6
+ import { runClipboardCommand } from "./clipboard-command.js";
7
+ import { isWSL } from "./wsl.js";
13
8
  const MAX_OSC52_ENCODED_LENGTH = 100_000;
14
- function isRemoteSession(env = process.env) {
9
+ function isRemoteSession(env) {
15
10
  return Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.MOSH_CONNECTION);
16
11
  }
17
12
  function emitOsc52(text) {
@@ -22,119 +17,134 @@ function emitOsc52(text) {
22
17
  process.stdout.write(`\x1b]52;c;${encoded}\x07`);
23
18
  return true;
24
19
  }
25
- /** Read plain text from the system clipboard, if native clipboard access is available. */
20
+ /**
21
+ * WSL without WSLg has no Linux display, so the Windows clipboard is written through
22
+ * interop. PowerShell reads the text from a file because `clip.exe` and PowerShell stdin
23
+ * decode piped bytes with the console code page, which mangles non-ASCII UTF-8.
24
+ */
25
+ async function copyViaWindowsClipboard(text) {
26
+ const tmpFile = join(tmpdir(), `pi-wsl-clip-${randomUUID()}.txt`);
27
+ try {
28
+ writeFileSync(tmpFile, text, { encoding: "utf8", mode: 0o600 });
29
+ const winPath = (await runClipboardCommand("wslpath", ["-w", tmpFile], { timeoutMs: 1000 }))
30
+ ?.toString("utf8")
31
+ .trim();
32
+ if (!winPath)
33
+ return false;
34
+ const script = `Set-Clipboard -Value ([System.IO.File]::ReadAllText('${winPath.replaceAll("'", "''")}', [System.Text.Encoding]::UTF8))`;
35
+ const result = await runClipboardCommand("powershell.exe", ["-NoProfile", "-Command", script], {
36
+ timeoutMs: 5000,
37
+ });
38
+ return result !== undefined;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ finally {
44
+ try {
45
+ unlinkSync(tmpFile);
46
+ }
47
+ catch {
48
+ // The file may not have been created.
49
+ }
50
+ }
51
+ }
52
+ /** Read plain text from the system clipboard. */
26
53
  export async function readClipboardText() {
27
- if (!clipboard) {
28
- return null;
54
+ if (platform() === "linux") {
55
+ const commands = [];
56
+ if (process.env.TERMUX_VERSION)
57
+ commands.push(["termux-clipboard-get", []]);
58
+ if (process.env.WAYLAND_DISPLAY)
59
+ commands.push(["wl-paste", ["--no-newline", "--type", "text"]]);
60
+ if (process.env.DISPLAY) {
61
+ commands.push(["xclip", ["-selection", "clipboard", "-out"]], ["xsel", ["--clipboard", "--output"]]);
62
+ }
63
+ for (const [command, args] of commands) {
64
+ const bytes = await runClipboardCommand(command, args, { timeoutMs: 5000 });
65
+ if (bytes !== undefined)
66
+ return bytes.toString("utf8") || null;
67
+ }
29
68
  }
30
69
  try {
31
- const text = await clipboard.getText();
32
- return text || null;
70
+ return (await getNativeClipboard()?.getText()) || null;
33
71
  }
34
72
  catch {
35
73
  return null;
36
74
  }
37
75
  }
38
76
  export async function copyToClipboard(text) {
39
- let copied = false;
40
77
  const p = platform();
41
- // Prefer direct clipboard writes. Emitting OSC 52 first can make terminals
42
- // write the same native clipboard concurrently with the addon, and very large
43
- // OSC 52 payloads can desynchronize terminal rendering.
44
- //
45
- // On Linux, skip the native addon. The underlying `clipboard-rs` crate is
46
- // X11-only and does not retain selection ownership after `set_text`
47
- // resolves, so on Wayland-only compositors (Hyprland, Niri, ...) and even
48
- // some X11 sessions the call resolves successfully without populating the
49
- // clipboard. The platform tools below (wl-copy, xclip, xsel) properly
50
- // daemonize and keep ownership.
51
- try {
52
- if (clipboard && p !== "linux") {
53
- await clipboard.setText(text);
54
- copied = true;
78
+ const env = process.env;
79
+ let copied = false;
80
+ // Direct writes precede OSC 52 so the terminal cannot race the native writer.
81
+ // Linux tools retain clipboard selection ownership after this call returns.
82
+ if (p !== "linux") {
83
+ try {
84
+ const clipboard = getNativeClipboard();
85
+ if (clipboard?.setText) {
86
+ await clipboard.setText(text);
87
+ copied = true;
88
+ }
89
+ }
90
+ catch {
91
+ // Try platform commands next.
55
92
  }
56
93
  }
57
- catch {
58
- // Fall through to platform-specific clipboard tools.
59
- }
60
- const remote = isRemoteSession();
61
- if (copied && !remote) {
62
- return;
63
- }
64
- const options = { input: text, timeout: 5000, stdio: ["pipe", "ignore", "ignore"] };
65
94
  if (!copied) {
66
- try {
67
- if (p === "darwin") {
68
- execSync("pbcopy", options);
69
- copied = true;
95
+ const commands = [];
96
+ if (p === "darwin")
97
+ commands.push(["pbcopy", []]);
98
+ else if (p === "win32")
99
+ commands.push(["clip", []]);
100
+ else {
101
+ if (env.TERMUX_VERSION)
102
+ commands.push(["termux-clipboard-set", []]);
103
+ if (env.WAYLAND_DISPLAY)
104
+ commands.push(["wl-copy", []]);
105
+ if (env.DISPLAY) {
106
+ commands.push(["xclip", ["-selection", "clipboard"]], ["xsel", ["--clipboard", "--input"]]);
70
107
  }
71
- else if (p === "win32") {
72
- execSync("clip", options);
108
+ }
109
+ for (const [command, args] of commands) {
110
+ if ((await runClipboardCommand(command, args, { input: text, timeoutMs: 5000 })) !== undefined) {
73
111
  copied = true;
112
+ break;
74
113
  }
75
- else {
76
- // Linux. Try Termux, Wayland, or X11 clipboard tools.
77
- if (process.env.TERMUX_VERSION) {
78
- try {
79
- execSync("termux-clipboard-set", options);
80
- copied = true;
81
- }
82
- catch {
83
- // Fall back to Wayland or X11 tools.
84
- }
85
- }
86
- if (!copied) {
87
- const hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);
88
- const hasX11Display = Boolean(process.env.DISPLAY);
89
- const isWayland = isWaylandSession();
90
- if (isWayland && hasWaylandDisplay) {
91
- try {
92
- // Verify wl-copy exists (spawn errors are async and won't be caught)
93
- execSync("which wl-copy", { stdio: "ignore" });
94
- // wl-copy with execSync hangs due to fork behavior; use spawn instead.
95
- // Await the exit code and only claim success on a clean exit, so a
96
- // failed wl-copy falls through to the xclip/OSC 52 fallbacks.
97
- const wlCopyExit = await new Promise((resolve) => {
98
- const proc = spawn("wl-copy", [], { stdio: ["pipe", "ignore", "ignore"] });
99
- proc.on("error", () => resolve(1));
100
- proc.on("close", (code) => resolve(code ?? 1));
101
- proc.stdin.on("error", () => {
102
- // Ignore EPIPE errors if wl-copy exits early
103
- });
104
- proc.stdin.write(text);
105
- proc.stdin.end();
106
- });
107
- if (wlCopyExit === 0) {
108
- copied = true;
109
- }
110
- else if (hasX11Display) {
111
- copyToX11Clipboard(options);
112
- copied = true;
113
- }
114
- }
115
- catch {
116
- if (hasX11Display) {
117
- copyToX11Clipboard(options);
118
- copied = true;
119
- }
120
- }
121
- }
122
- else if (hasX11Display) {
123
- copyToX11Clipboard(options);
124
- copied = true;
125
- }
126
- }
127
- }
128
- }
129
- catch {
130
- // Fall through to OSC 52 fallback.
131
114
  }
132
115
  }
133
- if (remote || !copied) {
134
- const osc52Copied = emitOsc52(text);
135
- copied = copied || osc52Copied;
116
+ let osc52Emitted = false;
117
+ if (!copied && p === "linux" && isWSL(env)) {
118
+ // Windows Terminal supports OSC 52; prefer it over the slower PowerShell round trip.
119
+ if (env.WT_SESSION)
120
+ osc52Emitted = emitOsc52(text);
121
+ copied = osc52Emitted || (await copyViaWindowsClipboard(text));
136
122
  }
137
- if (!copied) {
138
- throw new Error("Failed to copy to clipboard");
123
+ // OSC 52 cannot be verified, so a desktop session with a display reports the failure
124
+ // instead (#9618). Without a display the terminal is the only clipboard route (containers,
125
+ // WSL without WSLg), and remote sessions always emit it to reach the client clipboard.
126
+ const headless = p === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY && !env.TERMUX_VERSION;
127
+ let oversized = false;
128
+ if (!osc52Emitted && (isRemoteSession(env) || (!copied && headless))) {
129
+ if (emitOsc52(text))
130
+ copied = true;
131
+ else
132
+ oversized = true;
133
+ }
134
+ if (copied)
135
+ return;
136
+ if (oversized)
137
+ throw new Error("Clipboard unavailable: text exceeds the OSC 52 size limit");
138
+ if (p === "linux") {
139
+ if (env.TERMUX_VERSION) {
140
+ throw new Error("Clipboard unavailable: install the Termux:API app and `termux-api` package");
141
+ }
142
+ if (env.WAYLAND_DISPLAY) {
143
+ throw new Error("Clipboard unavailable: install `wl-clipboard` (`wl-copy`) or check Wayland access");
144
+ }
145
+ if (env.DISPLAY) {
146
+ throw new Error("Clipboard unavailable: install `xclip` or `xsel`, or check X11 access");
147
+ }
139
148
  }
149
+ throw new Error("Clipboard unavailable");
140
150
  }
@@ -0,0 +1,2 @@
1
+ /** Windows Subsystem for Linux, where Windows executables are reachable through interop. */
2
+ export declare function isWSL(env?: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from "node:fs";
2
+ /** Windows Subsystem for Linux, where Windows executables are reachable through interop. */
3
+ export function isWSL(env = process.env) {
4
+ if (env.WSL_DISTRO_NAME || env.WSLENV) {
5
+ return true;
6
+ }
7
+ try {
8
+ const release = readFileSync("/proc/version", "utf-8");
9
+ return /microsoft|wsl/i.test(release);
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
@@ -0,0 +1,6 @@
1
+ interface ZipEntry {
2
+ name: string;
3
+ data: string | Uint8Array;
4
+ }
5
+ export declare function writeZipArchive(filePath: string, entries: readonly ZipEntry[]): Promise<void>;
6
+ export {};
@@ -0,0 +1,59 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { crc32, deflateRawSync } from "node:zlib";
3
+ function dosDateTime(date) {
4
+ return {
5
+ time: (date.getHours() << 11) | (date.getMinutes() << 5) | (date.getSeconds() >> 1),
6
+ day: ((Math.max(1980, date.getFullYear()) - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
7
+ };
8
+ }
9
+ /** Create the small, classic ZIP archives used by bug reports. */
10
+ function createZipArchive(entries) {
11
+ const files = [];
12
+ const directory = [];
13
+ const { time, day } = dosDateTime(new Date());
14
+ let offset = 0;
15
+ for (const entry of entries) {
16
+ const name = Buffer.from(entry.name);
17
+ const data = typeof entry.data === "string" ? Buffer.from(entry.data) : Buffer.from(entry.data);
18
+ const compressed = deflateRawSync(data);
19
+ const checksum = crc32(data);
20
+ const local = Buffer.alloc(30);
21
+ local.writeUInt32LE(0x04034b50, 0);
22
+ local.writeUInt16LE(20, 4);
23
+ local.writeUInt16LE(0x0800, 6);
24
+ local.writeUInt16LE(8, 8);
25
+ local.writeUInt16LE(time, 10);
26
+ local.writeUInt16LE(day, 12);
27
+ local.writeUInt32LE(checksum, 14);
28
+ local.writeUInt32LE(compressed.length, 18);
29
+ local.writeUInt32LE(data.length, 22);
30
+ local.writeUInt16LE(name.length, 26);
31
+ const central = Buffer.alloc(46);
32
+ central.writeUInt32LE(0x02014b50, 0);
33
+ central.writeUInt16LE(20, 4);
34
+ central.writeUInt16LE(20, 6);
35
+ central.writeUInt16LE(0x0800, 8);
36
+ central.writeUInt16LE(8, 10);
37
+ central.writeUInt16LE(time, 12);
38
+ central.writeUInt16LE(day, 14);
39
+ central.writeUInt32LE(checksum, 16);
40
+ central.writeUInt32LE(compressed.length, 20);
41
+ central.writeUInt32LE(data.length, 24);
42
+ central.writeUInt16LE(name.length, 28);
43
+ central.writeUInt32LE(offset, 42);
44
+ files.push(local, name, compressed);
45
+ directory.push(central, name);
46
+ offset += local.length + name.length + compressed.length;
47
+ }
48
+ const centralDirectory = Buffer.concat(directory);
49
+ const end = Buffer.alloc(22);
50
+ end.writeUInt32LE(0x06054b50, 0);
51
+ end.writeUInt16LE(entries.length, 8);
52
+ end.writeUInt16LE(entries.length, 10);
53
+ end.writeUInt32LE(centralDirectory.length, 12);
54
+ end.writeUInt32LE(offset, 16);
55
+ return Buffer.concat([...files, centralDirectory, end]);
56
+ }
57
+ export function writeZipArchive(filePath, entries) {
58
+ return writeFile(filePath, createZipArchive(entries));
59
+ }