@oh-my-pi/pi-coding-agent 15.7.5 → 15.8.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/CHANGELOG.md +159 -182
- package/dist/types/async/job-manager.d.ts +3 -3
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/claude-trace-cli.d.ts +54 -0
- package/dist/types/cli/session-picker.d.ts +10 -3
- package/dist/types/cli/update-cli.d.ts +17 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +11 -2
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
- package/dist/types/eval/concurrency-bridge.d.ts +26 -0
- package/dist/types/eval/heartbeat.d.ts +45 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +24 -5
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
- package/dist/types/lsp/diagnostics-ledger.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +4 -0
- package/dist/types/main.d.ts +5 -0
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/custom-editor.d.ts +3 -2
- package/dist/types/modes/components/hook-selector.d.ts +10 -1
- package/dist/types/modes/components/session-selector.d.ts +32 -5
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -3
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -3
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +5 -3
- package/dist/types/registry/agent-registry.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +12 -3
- package/dist/types/session/history-storage.d.ts +16 -1
- package/dist/types/session/session-manager.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +6 -15
- package/dist/types/tools/ask.d.ts +8 -6
- package/dist/types/tools/eval-backends.d.ts +12 -0
- package/dist/types/tools/eval-render.d.ts +52 -0
- package/dist/types/tools/eval.d.ts +2 -35
- package/dist/types/tools/find.d.ts +0 -9
- package/dist/types/tools/index.d.ts +5 -12
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/sqlite-reader.d.ts +25 -8
- package/dist/types/tui/output-block.d.ts +4 -3
- package/dist/types/utils/clipboard.d.ts +4 -0
- package/dist/types/web/kagi.d.ts +76 -0
- package/dist/types/web/search/providers/exa.d.ts +7 -1
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/thinking-note.ts +13 -0
- package/package.json +9 -9
- package/src/async/job-manager.ts +3 -3
- package/src/cli/args.ts +6 -2
- package/src/cli/claude-trace-cli.ts +783 -0
- package/src/cli/session-picker.ts +36 -10
- package/src/cli/update-cli.ts +35 -2
- package/src/commands/launch.ts +3 -0
- package/src/config/keybindings.ts +6 -0
- package/src/config/model-registry.ts +33 -4
- package/src/config/settings-schema.ts +12 -2
- package/src/config/settings.ts +23 -0
- package/src/discovery/claude-plugins.ts +7 -9
- package/src/discovery/claude.ts +41 -22
- package/src/edit/index.ts +23 -3
- package/src/eval/__tests__/agent-bridge.test.ts +148 -4
- package/src/eval/__tests__/heartbeat.test.ts +84 -0
- package/src/eval/__tests__/llm-bridge.test.ts +30 -0
- package/src/eval/agent-bridge.ts +44 -38
- package/src/eval/concurrency-bridge.ts +34 -0
- package/src/eval/heartbeat.ts +74 -0
- package/src/eval/js/executor.ts +13 -9
- package/src/eval/js/shared/prelude.txt +20 -17
- package/src/eval/js/tool-bridge.ts +5 -0
- package/src/eval/llm-bridge.ts +20 -14
- package/src/eval/py/executor.ts +14 -18
- package/src/eval/py/prelude.py +23 -15
- package/src/exec/bash-executor.ts +31 -5
- package/src/extensibility/extensions/loader.ts +16 -18
- package/src/extensibility/extensions/runner.ts +22 -17
- package/src/extensibility/extensions/types.ts +39 -5
- package/src/extensibility/plugins/legacy-pi-compat.ts +115 -131
- package/src/extensibility/skills.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/index.ts +9 -22
- package/src/lsp/utils.ts +21 -0
- package/src/main.ts +92 -24
- package/src/modes/acp/acp-agent.ts +8 -4
- package/src/modes/acp/acp-event-mapper.ts +54 -4
- package/src/modes/components/assistant-message.ts +28 -1
- package/src/modes/components/custom-editor.ts +19 -7
- package/src/modes/components/hook-selector.ts +229 -44
- package/src/modes/components/oauth-selector.ts +12 -6
- package/src/modes/components/session-selector.ts +179 -24
- package/src/modes/components/tool-execution.ts +36 -7
- package/src/modes/controllers/command-controller.ts +2 -11
- package/src/modes/controllers/event-controller.ts +5 -2
- package/src/modes/controllers/extension-ui-controller.ts +6 -4
- package/src/modes/controllers/input-controller.ts +19 -16
- package/src/modes/controllers/selector-controller.ts +61 -21
- package/src/modes/interactive-mode.ts +127 -16
- package/src/modes/rpc/rpc-mode.ts +17 -6
- package/src/modes/theme/theme-schema.json +30 -0
- package/src/modes/theme/theme.ts +39 -2
- package/src/modes/types.ts +7 -3
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/system/orchestrate-notice.md +5 -3
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/ask.md +2 -1
- package/src/prompts/tools/eval.md +6 -6
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/irc.md +6 -6
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -1
- package/src/registry/agent-registry.ts +1 -1
- package/src/sdk.ts +85 -31
- package/src/session/agent-session.ts +127 -57
- package/src/session/history-storage.ts +56 -12
- package/src/session/session-manager.ts +34 -0
- package/src/task/output-manager.ts +40 -48
- package/src/task/render.ts +3 -8
- package/src/tools/ask.ts +74 -32
- package/src/tools/browser/tab-worker.ts +8 -5
- package/src/tools/eval-backends.ts +38 -0
- package/src/tools/eval-render.ts +750 -0
- package/src/tools/eval.ts +27 -754
- package/src/tools/find.ts +5 -29
- package/src/tools/index.ts +8 -38
- package/src/tools/path-utils.ts +144 -1
- package/src/tools/read.ts +47 -0
- package/src/tools/renderers.ts +1 -1
- package/src/tools/search.ts +2 -27
- package/src/tools/sqlite-reader.ts +92 -9
- package/src/tools/write.ts +9 -1
- package/src/tui/output-block.ts +5 -4
- package/src/utils/clipboard.ts +38 -1
- package/src/utils/open.ts +37 -2
- package/src/web/kagi.ts +168 -49
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/exa.ts +20 -86
- package/src/web/search/providers/kagi.ts +4 -0
package/src/tools/find.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { ToolSession } from ".";
|
|
|
16
16
|
import { applyListLimit } from "./list-limit";
|
|
17
17
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
18
18
|
import {
|
|
19
|
+
expandDelimitedPathEntries,
|
|
19
20
|
formatPathRelativeToCwd,
|
|
20
21
|
hasGlobPathChars,
|
|
21
22
|
normalizePathLikeInput,
|
|
@@ -52,33 +53,6 @@ const DEFAULT_GLOB_TIMEOUT_MS = 5000;
|
|
|
52
53
|
const MIN_GLOB_TIMEOUT_MS = 500;
|
|
53
54
|
const MAX_GLOB_TIMEOUT_MS = 60_000;
|
|
54
55
|
|
|
55
|
-
/**
|
|
56
|
-
* Reject comma-separated path lists packed into a single array element
|
|
57
|
-
* (`["a.py,b.py"]`). The schema is array-of-string; agents that pass a
|
|
58
|
-
* single comma-joined element get silent no-matches otherwise.
|
|
59
|
-
*
|
|
60
|
-
* Commas inside brace expansion (`{a,b}`) are legitimate glob syntax and
|
|
61
|
-
* must pass through.
|
|
62
|
-
*/
|
|
63
|
-
export function validateFindPathInputs(paths: readonly string[]): void {
|
|
64
|
-
for (const entry of paths) {
|
|
65
|
-
let braceDepth = 0;
|
|
66
|
-
for (let i = 0; i < entry.length; i++) {
|
|
67
|
-
const ch = entry.charCodeAt(i);
|
|
68
|
-
if (ch === 0x5c /* \ */ && i + 1 < entry.length) {
|
|
69
|
-
i++;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (ch === 0x7b /* { */) braceDepth++;
|
|
73
|
-
else if (ch === 0x7d /* } */) {
|
|
74
|
-
if (braceDepth > 0) braceDepth--;
|
|
75
|
-
} else if (ch === 0x2c /* , */ && braceDepth === 0) {
|
|
76
|
-
throw new ToolError(`paths is an array — pass ["a", "b"] not ["a,b"] (got ${JSON.stringify(entry)})`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
56
|
/**
|
|
83
57
|
* Group find matches by their directory so the model doesn't pay repeated
|
|
84
58
|
* tokens for shared path prefixes. Preserves the input order: groups appear in
|
|
@@ -180,8 +154,10 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
180
154
|
|
|
181
155
|
return untilAborted(signal, async () => {
|
|
182
156
|
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
183
|
-
|
|
184
|
-
|
|
157
|
+
const rawPatternInputs = this.#customOps
|
|
158
|
+
? paths
|
|
159
|
+
: await expandDelimitedPathEntries(paths, this.session.cwd, { splitter: parseFindPattern });
|
|
160
|
+
const rawPatterns = rawPatternInputs.map(input => normalizePathLikeInput(input).replace(/\\/g, "/"));
|
|
185
161
|
const internalRouter = InternalUrlRouter.instance();
|
|
186
162
|
const normalizedPatterns: string[] = [];
|
|
187
163
|
for (const rawPattern of rawPatterns) {
|
package/src/tools/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
2
2
|
import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { ToolChoice } from "@oh-my-pi/pi-ai";
|
|
4
|
-
import {
|
|
4
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
6
6
|
import type { Settings } from "../config/settings";
|
|
7
7
|
import { EditTool } from "../edit";
|
|
@@ -34,6 +34,7 @@ import { BrowserTool } from "./browser";
|
|
|
34
34
|
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
|
|
35
35
|
import { DebugTool } from "./debug";
|
|
36
36
|
import { EvalTool } from "./eval";
|
|
37
|
+
import { resolveEvalBackends } from "./eval-backends";
|
|
37
38
|
import { FindTool } from "./find";
|
|
38
39
|
import { GithubTool } from "./gh";
|
|
39
40
|
import { InspectImageTool } from "./inspect-image";
|
|
@@ -74,6 +75,7 @@ export * from "./browser";
|
|
|
74
75
|
export * from "./checkpoint";
|
|
75
76
|
export * from "./debug";
|
|
76
77
|
export * from "./eval";
|
|
78
|
+
export * from "./eval-backends";
|
|
77
79
|
export * from "./find";
|
|
78
80
|
export * from "./gh";
|
|
79
81
|
export * from "./image-gen";
|
|
@@ -157,7 +159,7 @@ export interface ToolSession {
|
|
|
157
159
|
getHindsightSessionState?: () => HindsightSessionState | undefined;
|
|
158
160
|
/** Get Mnemopi runtime state for this agent session. */
|
|
159
161
|
getMnemopiSessionState?: () => MnemopiSessionState | undefined;
|
|
160
|
-
/** Agent identity used for IRC routing. Returns the registry id (e.g. "
|
|
162
|
+
/** Agent identity used for IRC routing. Returns the registry id (e.g. "Main", "AuthLoader"). */
|
|
161
163
|
getAgentId?: () => string | null;
|
|
162
164
|
/** Look up a registered tool by name (used by the eval js backend's tool bridge). */
|
|
163
165
|
getToolByName?: (name: string) => AgentTool | undefined;
|
|
@@ -257,6 +259,10 @@ export interface ToolSession {
|
|
|
257
259
|
* by `getConflictHistory`. */
|
|
258
260
|
conflictHistory?: import("./conflict-detect").ConflictHistory;
|
|
259
261
|
|
|
262
|
+
/** Per-session ledger of post-edit LSP diagnostics already surfaced to the
|
|
263
|
+
* model for each file. Lazily initialized by `getDiagnosticsLedger`. */
|
|
264
|
+
diagnosticsLedger?: import("../lsp/diagnostics-ledger").DiagnosticsLedger;
|
|
265
|
+
|
|
260
266
|
/** Queue a hidden message to be injected at the next agent turn. */
|
|
261
267
|
queueDeferredMessage?(message: CustomMessage): void;
|
|
262
268
|
/** Get the active OpenTelemetry config so subagent dispatch can forward
|
|
@@ -331,42 +337,6 @@ export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
|
|
331
337
|
|
|
332
338
|
export type ToolName = keyof typeof BUILTIN_TOOLS;
|
|
333
339
|
|
|
334
|
-
export interface EvalBackendsAllowance {
|
|
335
|
-
python: boolean;
|
|
336
|
-
js: boolean;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Parse PI_PY / PI_JS environment variables. Each is a boolean flag; unset
|
|
341
|
-
* means "not specified, defer to settings". Returns null when neither is set
|
|
342
|
-
* so the caller can fall through to `readEvalBackendsAllowance` per key.
|
|
343
|
-
*/
|
|
344
|
-
function getEvalBackendsFromEnv(): EvalBackendsAllowance | null {
|
|
345
|
-
const pyEnv = $env.PI_PY;
|
|
346
|
-
const jsEnv = $env.PI_JS;
|
|
347
|
-
if (pyEnv === undefined && jsEnv === undefined) return null;
|
|
348
|
-
return {
|
|
349
|
-
python: pyEnv === undefined ? true : $flag("PI_PY"),
|
|
350
|
-
js: jsEnv === undefined ? true : $flag("PI_JS"),
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/** Read per-backend allowance from settings (defaults true). */
|
|
355
|
-
export function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAllowance {
|
|
356
|
-
return {
|
|
357
|
-
python: session.settings.get("eval.py") ?? true,
|
|
358
|
-
js: session.settings.get("eval.js") ?? true,
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Materialize the active eval backend allowance: PI_PY / PI_JS env flags
|
|
364
|
-
* override the per-key settings; otherwise settings (defaults true) win.
|
|
365
|
-
*/
|
|
366
|
-
export function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance {
|
|
367
|
-
return getEvalBackendsFromEnv() ?? readEvalBackendsAllowance(session);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
340
|
/**
|
|
371
341
|
* Create tools from BUILTIN_TOOLS registry.
|
|
372
342
|
*/
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -379,6 +379,145 @@ export function hasGlobPathChars(filePath: string): boolean {
|
|
|
379
379
|
return GLOB_PATH_CHARS.some(char => filePath.includes(char));
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
type PathEntrySplitter = (item: string) => { basePath: string };
|
|
383
|
+
|
|
384
|
+
const TOP_LEVEL_WHITESPACE_RE = /\s/;
|
|
385
|
+
|
|
386
|
+
type DelimitedPathSplitMode = "comma" | "semicolon" | "whitespace" | "mixed";
|
|
387
|
+
|
|
388
|
+
function isDelimitedPathSeparator(ch: string, mode: DelimitedPathSplitMode): boolean {
|
|
389
|
+
if (mode === "comma") return ch === ",";
|
|
390
|
+
if (mode === "semicolon") return ch === ";";
|
|
391
|
+
if (mode === "whitespace") return TOP_LEVEL_WHITESPACE_RE.test(ch);
|
|
392
|
+
return ch === "," || ch === ";" || TOP_LEVEL_WHITESPACE_RE.test(ch);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function hasTopLevelPathDelimiter(entry: string): boolean {
|
|
396
|
+
let braceDepth = 0;
|
|
397
|
+
for (let i = 0; i < entry.length; i++) {
|
|
398
|
+
const ch = entry[i];
|
|
399
|
+
if (ch === "\\" && i + 1 < entry.length) {
|
|
400
|
+
i++;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (ch === "{") {
|
|
404
|
+
braceDepth++;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (ch === "}") {
|
|
408
|
+
if (braceDepth > 0) braceDepth--;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (braceDepth === 0 && (ch === "," || ch === ";" || TOP_LEVEL_WHITESPACE_RE.test(ch))) {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function splitTopLevelDelimitedPath(entry: string, mode: DelimitedPathSplitMode): string[] {
|
|
419
|
+
const parts: string[] = [];
|
|
420
|
+
let braceDepth = 0;
|
|
421
|
+
let start = 0;
|
|
422
|
+
for (let i = 0; i < entry.length; i++) {
|
|
423
|
+
const ch = entry[i];
|
|
424
|
+
if (ch === "\\" && i + 1 < entry.length) {
|
|
425
|
+
i++;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (ch === "{") {
|
|
429
|
+
braceDepth++;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (ch === "}") {
|
|
433
|
+
if (braceDepth > 0) braceDepth--;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (braceDepth !== 0 || !isDelimitedPathSeparator(ch, mode)) continue;
|
|
437
|
+
parts.push(entry.slice(start, i));
|
|
438
|
+
start = i + 1;
|
|
439
|
+
}
|
|
440
|
+
parts.push(entry.slice(start));
|
|
441
|
+
return parts;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function delimitedPathPartResolves(entry: string, cwd: string, splitter: PathEntrySplitter): Promise<boolean> {
|
|
445
|
+
if (isInternalUrlPath(entry)) return true;
|
|
446
|
+
const peeled = splitPathAndSel(entry).path;
|
|
447
|
+
const { basePath } = splitter(peeled);
|
|
448
|
+
const absoluteBasePath = resolveToCwd(basePath, cwd);
|
|
449
|
+
try {
|
|
450
|
+
await fs.promises.stat(absoluteBasePath);
|
|
451
|
+
return true;
|
|
452
|
+
} catch (err) {
|
|
453
|
+
if (isEnoent(err)) return false;
|
|
454
|
+
throw err;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function tryDelimitedPathSplit(
|
|
459
|
+
entry: string,
|
|
460
|
+
cwd: string,
|
|
461
|
+
splitter: PathEntrySplitter,
|
|
462
|
+
mode: DelimitedPathSplitMode,
|
|
463
|
+
requireAllParts: boolean,
|
|
464
|
+
): Promise<string[] | null> {
|
|
465
|
+
const rawParts = splitTopLevelDelimitedPath(entry, mode);
|
|
466
|
+
if (rawParts.length < 2) return null;
|
|
467
|
+
|
|
468
|
+
const parts = rawParts.map(normalizePathLikeInput).filter(part => part.length > 0);
|
|
469
|
+
if (parts.length === 0) return null;
|
|
470
|
+
if (parts.length < 2 && rawParts.length === parts.length) return null;
|
|
471
|
+
|
|
472
|
+
const resolved = await Promise.all(parts.map(part => delimitedPathPartResolves(part, cwd, splitter)));
|
|
473
|
+
const valid = requireAllParts ? resolved.every(Boolean) : resolved.some(Boolean);
|
|
474
|
+
return valid ? parts : null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Split one path-like entry whose multiple targets were flattened into one
|
|
479
|
+
* string. Existing paths are kept intact, so real filenames containing spaces,
|
|
480
|
+
* commas, or semicolons win over delimiter recovery.
|
|
481
|
+
*/
|
|
482
|
+
export async function splitDelimitedPathEntry(
|
|
483
|
+
entry: string,
|
|
484
|
+
cwd: string,
|
|
485
|
+
options: { splitter?: PathEntrySplitter } = {},
|
|
486
|
+
): Promise<string[] | null> {
|
|
487
|
+
const normalizedEntry = normalizePathLikeInput(entry);
|
|
488
|
+
if (!hasTopLevelPathDelimiter(normalizedEntry)) return null;
|
|
489
|
+
if (isInternalUrlPath(normalizedEntry)) return null;
|
|
490
|
+
|
|
491
|
+
const splitter = options.splitter ?? parseSearchPath;
|
|
492
|
+
const peeledEntry = splitPathAndSel(normalizedEntry).path;
|
|
493
|
+
if (!hasGlobPathChars(peeledEntry) && (await delimitedPathPartResolves(normalizedEntry, cwd, splitter))) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return (
|
|
498
|
+
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "comma", false)) ??
|
|
499
|
+
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "semicolon", false)) ??
|
|
500
|
+
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "whitespace", true)) ??
|
|
501
|
+
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "mixed", true))
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Expand delimited entries in-place while preserving unsplit entries. */
|
|
506
|
+
export async function expandDelimitedPathEntries(
|
|
507
|
+
entries: readonly string[],
|
|
508
|
+
cwd: string,
|
|
509
|
+
options: { splitter?: PathEntrySplitter } = {},
|
|
510
|
+
): Promise<string[]> {
|
|
511
|
+
const expanded: string[] = [];
|
|
512
|
+
for (const entry of entries) {
|
|
513
|
+
const normalizedEntry = normalizePathLikeInput(entry);
|
|
514
|
+
const split = await splitDelimitedPathEntry(normalizedEntry, cwd, options);
|
|
515
|
+
if (split) expanded.push(...split);
|
|
516
|
+
else expanded.push(normalizedEntry);
|
|
517
|
+
}
|
|
518
|
+
return expanded;
|
|
519
|
+
}
|
|
520
|
+
|
|
382
521
|
export interface ParsedSearchPath {
|
|
383
522
|
basePath: string;
|
|
384
523
|
glob?: string;
|
|
@@ -769,7 +908,11 @@ export interface ToolScopeResolution {
|
|
|
769
908
|
*/
|
|
770
909
|
export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<ToolScopeResolution> {
|
|
771
910
|
const { rawPaths: inputs, cwd, internalUrlAction } = opts;
|
|
772
|
-
const
|
|
911
|
+
const normalizedRawPaths = inputs.map(normalizePathLikeInput);
|
|
912
|
+
if (normalizedRawPaths.some(rawPath => rawPath.length === 0)) {
|
|
913
|
+
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
914
|
+
}
|
|
915
|
+
const rawPaths = await expandDelimitedPathEntries(normalizedRawPaths, cwd);
|
|
773
916
|
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
774
917
|
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
775
918
|
}
|
package/src/tools/read.ts
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
type LineRange,
|
|
70
70
|
parseLineRanges,
|
|
71
71
|
resolveReadPath,
|
|
72
|
+
splitDelimitedPathEntry,
|
|
72
73
|
splitInternalUrlSel,
|
|
73
74
|
splitPathAndSel,
|
|
74
75
|
} from "./path-utils";
|
|
@@ -693,6 +694,50 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
693
694
|
});
|
|
694
695
|
}
|
|
695
696
|
|
|
697
|
+
async #tryReadDelimitedPaths(
|
|
698
|
+
readPath: string,
|
|
699
|
+
signal?: AbortSignal,
|
|
700
|
+
): Promise<AgentToolResult<ReadToolDetails> | null> {
|
|
701
|
+
const parts = await splitDelimitedPathEntry(readPath, this.session.cwd);
|
|
702
|
+
if (!parts) return null;
|
|
703
|
+
|
|
704
|
+
const notice = `Note: interpreted as ${parts.length} paths: ${parts.join(", ")}`;
|
|
705
|
+
const notes = [notice];
|
|
706
|
+
const content: Array<TextContent | ImageContent> = [];
|
|
707
|
+
let pendingText = notice;
|
|
708
|
+
const flushText = () => {
|
|
709
|
+
if (pendingText.length === 0) return;
|
|
710
|
+
content.push({ type: "text", text: pendingText });
|
|
711
|
+
pendingText = "";
|
|
712
|
+
};
|
|
713
|
+
const appendText = (text: string) => {
|
|
714
|
+
pendingText = pendingText.length > 0 ? `${pendingText}\n\n${text}` : text;
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
for (const part of parts) {
|
|
718
|
+
try {
|
|
719
|
+
const result = await this.execute("read-delimited-part", { path: part }, signal);
|
|
720
|
+
for (const block of result.content) {
|
|
721
|
+
if (block.type === "text") {
|
|
722
|
+
appendText(block.text);
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
flushText();
|
|
726
|
+
content.push(block);
|
|
727
|
+
}
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (error instanceof ToolAbortError || signal?.aborted) throw error;
|
|
730
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
731
|
+
const errorNote = `Could not read ${part}: ${message}`;
|
|
732
|
+
notes.push(errorNote);
|
|
733
|
+
appendText(`[${errorNote}]`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
flushText();
|
|
737
|
+
|
|
738
|
+
return toolResult<ReadToolDetails>({ notes }).content(content).done();
|
|
739
|
+
}
|
|
740
|
+
|
|
696
741
|
async #resolveArchiveReadPath(readPath: string, signal?: AbortSignal): Promise<ResolvedArchiveReadPath | null> {
|
|
697
742
|
const candidates = parseArchivePathCandidates(readPath);
|
|
698
743
|
for (const candidate of candidates) {
|
|
@@ -1596,6 +1641,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1596
1641
|
}
|
|
1597
1642
|
|
|
1598
1643
|
if (!suffixResolution) {
|
|
1644
|
+
const delimitedResult = await this.#tryReadDelimitedPaths(readPath, signal);
|
|
1645
|
+
if (delimitedResult) return delimitedResult;
|
|
1599
1646
|
throw new ToolError(`Path '${localReadPath}' not found`);
|
|
1600
1647
|
}
|
|
1601
1648
|
} else {
|
package/src/tools/renderers.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { astGrepToolRenderer } from "./ast-grep";
|
|
|
17
17
|
import { bashToolRenderer } from "./bash";
|
|
18
18
|
import { browserToolRenderer } from "./browser/render";
|
|
19
19
|
import { debugToolRenderer } from "./debug";
|
|
20
|
-
import { evalToolRenderer } from "./eval";
|
|
20
|
+
import { evalToolRenderer } from "./eval-render";
|
|
21
21
|
import { findToolRenderer } from "./find";
|
|
22
22
|
import { githubToolRenderer } from "./gh-renderer";
|
|
23
23
|
import { inspectImageToolRenderer } from "./inspect-image-renderer";
|
package/src/tools/search.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { formatGroupedFiles } from "./grouped-file-output";
|
|
|
30
30
|
import { formatMatchLine } from "./match-line-format";
|
|
31
31
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
32
32
|
import {
|
|
33
|
+
expandDelimitedPathEntries,
|
|
33
34
|
hasGlobPathChars,
|
|
34
35
|
isLineInRanges,
|
|
35
36
|
type LineRange,
|
|
@@ -93,29 +94,6 @@ export const SINGLE_FILE_MATCHES = 200;
|
|
|
93
94
|
* pagination headroom so the caller can see total file count. */
|
|
94
95
|
const INTERNAL_TOTAL_CAP = 2000;
|
|
95
96
|
|
|
96
|
-
/**
|
|
97
|
-
* Detect a `,` that is not inside a `{…}` brace expansion. Used to catch
|
|
98
|
-
* `paths: ["a,b"]` mistakes where the caller flattened multiple entries
|
|
99
|
-
* into a single string instead of passing a JSON array of strings.
|
|
100
|
-
*/
|
|
101
|
-
function containsTopLevelComma(entry: string): boolean {
|
|
102
|
-
let depth = 0;
|
|
103
|
-
for (let i = 0; i < entry.length; i++) {
|
|
104
|
-
const ch = entry[i];
|
|
105
|
-
if (ch === "\\" && i + 1 < entry.length) {
|
|
106
|
-
i++;
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (ch === "{") depth++;
|
|
110
|
-
else if (ch === "}") {
|
|
111
|
-
if (depth > 0) depth--;
|
|
112
|
-
} else if (ch === "," && depth === 0) {
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
97
|
/**
|
|
120
98
|
* Parsed `paths` entry — a path (possibly archive-shaped) plus an optional
|
|
121
99
|
* line-range selector peeled off the trailing `:N-M` (or `:N+K`, `:N,M`, …)
|
|
@@ -146,9 +124,6 @@ function parsePathSpecs(rawEntries: readonly string[]): SearchPathSpec[] {
|
|
|
146
124
|
clean = split.path;
|
|
147
125
|
ranges = parsed;
|
|
148
126
|
}
|
|
149
|
-
if (containsTopLevelComma(clean)) {
|
|
150
|
-
throw new ToolError('paths is an array — pass ["a", "b"] not ["a,b"]');
|
|
151
|
-
}
|
|
152
127
|
specs.push({ original: entry, clean, ranges });
|
|
153
128
|
}
|
|
154
129
|
return specs;
|
|
@@ -663,7 +638,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
663
638
|
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
|
|
664
639
|
throw new ToolError("Skip must be a non-negative number");
|
|
665
640
|
}
|
|
666
|
-
const rawEntries = toPathList(rawPaths);
|
|
641
|
+
const rawEntries = await expandDelimitedPathEntries(toPathList(rawPaths), this.session.cwd);
|
|
667
642
|
const pathSpecs = parsePathSpecs(rawEntries);
|
|
668
643
|
const paths = pathSpecs.map(spec => spec.clean);
|
|
669
644
|
const {
|
|
@@ -12,6 +12,15 @@ const MAX_QUERY_LIMIT = 500;
|
|
|
12
12
|
const MAX_RENDER_WIDTH = 120;
|
|
13
13
|
const MAX_COLUMN_WIDTH = 40;
|
|
14
14
|
const MIN_COLUMN_WIDTH = 1;
|
|
15
|
+
/**
|
|
16
|
+
* Upper bound on rows scanned when counting a table for the listing. SQLite has
|
|
17
|
+
* no stored row count, so `COUNT(*)` is a full b-tree scan — multi-second on a
|
|
18
|
+
* multi-GB database, and `bun:sqlite` runs it synchronously on the JS thread
|
|
19
|
+
* that also drives the TUI, freezing rendering and input. The listing instead
|
|
20
|
+
* trusts the planner's `sqlite_stat1` estimate for large tables and only counts
|
|
21
|
+
* exactly when a table is provably small, reading at most this many rows.
|
|
22
|
+
*/
|
|
23
|
+
const ROW_COUNT_PROBE_CAP = 50_000;
|
|
15
24
|
|
|
16
25
|
type SqliteBinding = Exclude<SQLQueryBindings, Record<string, unknown>>;
|
|
17
26
|
|
|
@@ -26,6 +35,11 @@ interface SqliteCountRow {
|
|
|
26
35
|
count: number;
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
interface SqliteStat1Row {
|
|
39
|
+
tbl: string;
|
|
40
|
+
stat: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
interface SqliteTableInfoRow {
|
|
30
44
|
cid: number;
|
|
31
45
|
name: string;
|
|
@@ -50,6 +64,23 @@ export type SqliteSelector =
|
|
|
50
64
|
|
|
51
65
|
export type SqliteRowLookup = { kind: "pk"; column: string; type: string } | { kind: "rowid" };
|
|
52
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Row count for a table in the listing.
|
|
69
|
+
* - `exact`: counted in full (the table is small enough to count cheaply).
|
|
70
|
+
* - `estimate`: the planner's `sqlite_stat1` figure; the table is too large to
|
|
71
|
+
* scan, so this may be stale.
|
|
72
|
+
* - `atLeast`: a lower bound; counting was capped before reaching the end.
|
|
73
|
+
*/
|
|
74
|
+
export type TableRowCount =
|
|
75
|
+
| { kind: "exact"; rows: number }
|
|
76
|
+
| { kind: "estimate"; rows: number }
|
|
77
|
+
| { kind: "atLeast"; rows: number };
|
|
78
|
+
|
|
79
|
+
export interface SqliteTableSummary {
|
|
80
|
+
name: string;
|
|
81
|
+
count: TableRowCount;
|
|
82
|
+
}
|
|
83
|
+
|
|
53
84
|
function splitSqliteRemainder(remainder: string): { subPath: string; queryString: string } {
|
|
54
85
|
const queryIndex = remainder.indexOf("?");
|
|
55
86
|
if (queryIndex === -1) {
|
|
@@ -495,20 +526,61 @@ export function parseSqliteSelector(subPath: string, queryString: string): Sqlit
|
|
|
495
526
|
return { kind: "schema", table, sampleLimit: DEFAULT_SCHEMA_SAMPLE_LIMIT };
|
|
496
527
|
}
|
|
497
528
|
|
|
498
|
-
|
|
529
|
+
/**
|
|
530
|
+
* Reads the planner's per-table row estimate from `sqlite_stat1` (populated by
|
|
531
|
+
* `ANALYZE`). The first integer of each `stat` string is the number of rows in
|
|
532
|
+
* that index; for a full (non-partial) index it equals the table's row count,
|
|
533
|
+
* so the max across a table's entries is the table estimate. Returns an empty
|
|
534
|
+
* map when the database was never analyzed. One small indexed read — no scan.
|
|
535
|
+
*/
|
|
536
|
+
function loadRowEstimates(db: Database): Map<string, number> {
|
|
537
|
+
const estimates = new Map<string, number>();
|
|
538
|
+
const hasStat1 = db
|
|
539
|
+
.prepare<Pick<SqliteMasterRow, "name">, []>(
|
|
540
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_stat1'",
|
|
541
|
+
)
|
|
542
|
+
.get();
|
|
543
|
+
if (!hasStat1) return estimates;
|
|
544
|
+
|
|
545
|
+
for (const { tbl, stat } of db.prepare<SqliteStat1Row, []>("SELECT tbl, stat FROM sqlite_stat1").all()) {
|
|
546
|
+
if (!stat) continue;
|
|
547
|
+
const rows = Number.parseInt(stat, 10);
|
|
548
|
+
if (!Number.isFinite(rows)) continue;
|
|
549
|
+
const prev = estimates.get(tbl);
|
|
550
|
+
if (prev === undefined || rows > prev) estimates.set(tbl, rows);
|
|
551
|
+
}
|
|
552
|
+
return estimates;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Counts a table while reading at most `cap + 1` rows. Returns an exact count
|
|
557
|
+
* when the table holds `cap` rows or fewer, otherwise a lower bound of `cap`.
|
|
558
|
+
* Bounds the worst-case scan so a stale or missing estimate can never trigger a
|
|
559
|
+
* full-table scan on the JS thread.
|
|
560
|
+
*/
|
|
561
|
+
function probeRowCount(db: Database, table: string, cap: number): TableRowCount {
|
|
562
|
+
const sql = `SELECT COUNT(*) AS count FROM (SELECT 1 FROM ${quoteSqliteIdentifier(table)} LIMIT ${cap + 1})`;
|
|
563
|
+
const counted = db.prepare<SqliteCountRow, []>(sql).get()?.count ?? 0;
|
|
564
|
+
return counted > cap ? { kind: "atLeast", rows: cap } : { kind: "exact", rows: counted };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function listTables(db: Database, options: { probeCap?: number } = {}): SqliteTableSummary[] {
|
|
568
|
+
const cap = options.probeCap ?? ROW_COUNT_PROBE_CAP;
|
|
499
569
|
const names = db
|
|
500
570
|
.prepare<Pick<SqliteMasterRow, "name">, []>(
|
|
501
571
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name COLLATE NOCASE",
|
|
502
572
|
)
|
|
503
573
|
.all();
|
|
574
|
+
const estimates = loadRowEstimates(db);
|
|
504
575
|
|
|
505
576
|
return names.map(({ name }) => {
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
577
|
+
const estimate = estimates.get(name);
|
|
578
|
+
// Trust the planner only when it says the table is too large to count
|
|
579
|
+
// cheaply; otherwise count exactly (bounded), which also corrects a
|
|
580
|
+
// stale-low estimate without ever scanning more than `cap` rows.
|
|
581
|
+
const count: TableRowCount =
|
|
582
|
+
estimate !== undefined && estimate > cap ? { kind: "estimate", rows: estimate } : probeRowCount(db, name, cap);
|
|
583
|
+
return { name, count };
|
|
512
584
|
});
|
|
513
585
|
}
|
|
514
586
|
|
|
@@ -679,13 +751,24 @@ export function deleteRowByRowId(db: Database, table: string, key: string): numb
|
|
|
679
751
|
return statement.run(binding).changes;
|
|
680
752
|
}
|
|
681
753
|
|
|
682
|
-
|
|
754
|
+
function formatRowCount(count: TableRowCount): string {
|
|
755
|
+
switch (count.kind) {
|
|
756
|
+
case "exact":
|
|
757
|
+
return `${count.rows} rows`;
|
|
758
|
+
case "estimate":
|
|
759
|
+
return `~${count.rows} rows`;
|
|
760
|
+
case "atLeast":
|
|
761
|
+
return `${count.rows}+ rows`;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export function renderTableList(tables: SqliteTableSummary[]): string {
|
|
683
766
|
if (tables.length === 0) {
|
|
684
767
|
return "(no tables)";
|
|
685
768
|
}
|
|
686
769
|
|
|
687
770
|
return tables
|
|
688
|
-
.map(table => truncateToWidth(replaceTabs(`${table.name} (${table.
|
|
771
|
+
.map(table => truncateToWidth(replaceTabs(`${table.name} (${formatRowCount(table.count)})`), MAX_RENDER_WIDTH))
|
|
689
772
|
.join("\n");
|
|
690
773
|
}
|
|
691
774
|
|
package/src/tools/write.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
15
15
|
import { InternalUrlRouter } from "../internal-urls";
|
|
16
16
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
17
17
|
import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
|
|
18
|
+
import { getDiagnosticsLedger } from "../lsp/diagnostics-ledger";
|
|
18
19
|
import { getLanguageFromPath, highlightCode, type Theme } from "../modes/theme/theme";
|
|
19
20
|
import writeDescription from "../prompts/tools/write.md" with { type: "text" };
|
|
20
21
|
import type { ToolSession } from "../sdk";
|
|
@@ -278,8 +279,15 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
278
279
|
const enableLsp = session.enableLsp ?? true;
|
|
279
280
|
const enableFormat = enableLsp && session.settings.get("lsp.formatOnWrite");
|
|
280
281
|
const enableDiagnostics = enableLsp && session.settings.get("lsp.diagnosticsOnWrite");
|
|
282
|
+
const dedup = enableDiagnostics && session.settings.get("lsp.diagnosticsDeduplicate");
|
|
281
283
|
this.#writethrough = enableLsp
|
|
282
|
-
? createLspWritethrough(session.cwd, {
|
|
284
|
+
? createLspWritethrough(session.cwd, {
|
|
285
|
+
enableFormat,
|
|
286
|
+
enableDiagnostics,
|
|
287
|
+
transformDiagnostics: dedup
|
|
288
|
+
? (path, result) => getDiagnosticsLedger(session).reduce(path, result)
|
|
289
|
+
: undefined,
|
|
290
|
+
})
|
|
283
291
|
: writethroughNoop;
|
|
284
292
|
this.description = prompt.render(writeDescription);
|
|
285
293
|
}
|
package/src/tui/output-block.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface OutputBlockOptions {
|
|
|
19
19
|
animate?: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const BORDER_SHIMMER_TICK_MS =
|
|
22
|
+
const BORDER_SHIMMER_TICK_MS = 16;
|
|
23
23
|
/** Duration of one full left↔right↔left bounce of the bottom-edge segment, in
|
|
24
24
|
* ms. Position is derived from the wall clock against this fixed cycle so a
|
|
25
25
|
* resize only nudges the segment proportionally instead of teleporting it. */
|
|
@@ -28,9 +28,10 @@ const BORDER_BOUNCE_MS = 3000;
|
|
|
28
28
|
const BORDER_SEGMENT_LEN = 8;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Monotonic frame counter for animated borders
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Monotonic frame counter for animated borders, quantized to the TUI's ~16ms
|
|
32
|
+
* render cap so the cache key advances once per ~60fps frame — fine enough for a
|
|
33
|
+
* smooth segment sweep, coarse enough to coalesce multiple render passes that
|
|
34
|
+
* land inside the same frame.
|
|
34
35
|
*/
|
|
35
36
|
export function borderShimmerTick(): number {
|
|
36
37
|
return Math.floor(Date.now() / BORDER_SHIMMER_TICK_MS);
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -119,7 +119,7 @@ async function readImageViaPowerShell(): Promise<ClipboardImage | null> {
|
|
|
119
119
|
if (!b64) return null;
|
|
120
120
|
const bytes = Buffer.from(b64, "base64");
|
|
121
121
|
if (bytes.byteLength === 0) return null;
|
|
122
|
-
return { data:
|
|
122
|
+
return { data: bytes, mimeType: "image/png" };
|
|
123
123
|
} catch {
|
|
124
124
|
return null;
|
|
125
125
|
}
|
|
@@ -154,3 +154,40 @@ export async function readImageFromClipboard(): Promise<ClipboardImage | null> {
|
|
|
154
154
|
|
|
155
155
|
return (await native.readImageFromClipboard()) ?? null;
|
|
156
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Read plain text from the system clipboard.
|
|
160
|
+
*/
|
|
161
|
+
export async function readTextFromClipboard(): Promise<string> {
|
|
162
|
+
try {
|
|
163
|
+
const p = process.platform;
|
|
164
|
+
if (p === "darwin") {
|
|
165
|
+
return execSync("pbpaste", { encoding: "utf8", timeout: 2000 }).toString();
|
|
166
|
+
}
|
|
167
|
+
if (p === "win32") {
|
|
168
|
+
return execSync('powershell.exe -NoProfile -Command "Get-Clipboard"', {
|
|
169
|
+
encoding: "utf8",
|
|
170
|
+
timeout: 2000,
|
|
171
|
+
}).toString();
|
|
172
|
+
}
|
|
173
|
+
if (process.env.TERMUX_VERSION) {
|
|
174
|
+
return execSync("termux-clipboard-get", { encoding: "utf8", timeout: 2000 }).toString();
|
|
175
|
+
}
|
|
176
|
+
const hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);
|
|
177
|
+
const hasX11Display = Boolean(process.env.DISPLAY);
|
|
178
|
+
if (hasWaylandDisplay) {
|
|
179
|
+
try {
|
|
180
|
+
return execSync("wl-paste --type text/plain --no-newline", { encoding: "utf8", timeout: 2000 }).toString();
|
|
181
|
+
} catch {
|
|
182
|
+
if (hasX11Display) {
|
|
183
|
+
return execSync("xclip -selection clipboard -o", { encoding: "utf8", timeout: 2000 }).toString();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else if (hasX11Display) {
|
|
187
|
+
return execSync("xclip -selection clipboard -o", { encoding: "utf8", timeout: 2000 }).toString();
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
logger.warn("clipboard: failed to read clipboard text", { error: String(error) });
|
|
191
|
+
}
|
|
192
|
+
return "";
|
|
193
|
+
}
|