@oh-my-pi/pi-coding-agent 16.3.0 → 16.3.3
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 +91 -0
- package/dist/cli.js +3855 -3798
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/cli/update-cli.test.d.ts +1 -0
- package/dist/types/commands/update.d.ts +5 -0
- package/dist/types/config/config-file.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/dap/client.d.ts +9 -1
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/edit/file-snapshot-store.d.ts +9 -1
- package/dist/types/edit/hashline/execute.d.ts +1 -1
- package/dist/types/edit/hashline/params.d.ts +3 -6
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/extensibility/plugins/parser.d.ts +2 -1
- package/dist/types/hindsight/client.d.ts +15 -11
- package/dist/types/hindsight/client.test.d.ts +1 -0
- package/dist/types/irc/bus.d.ts +5 -4
- package/dist/types/mcp/smithery-registry.d.ts +1 -0
- package/dist/types/mcp/smithery-registry.test.d.ts +1 -0
- package/dist/types/modes/components/advisor-message.d.ts +4 -2
- package/dist/types/modes/components/tool-execution.d.ts +9 -6
- package/dist/types/modes/components/transcript-container.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +13 -13
- package/dist/types/session/indexed-session-storage.d.ts +2 -2
- package/dist/types/session/session-storage.d.ts +31 -3
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +1 -0
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +1 -0
- package/dist/types/ssh/connection-manager.d.ts +19 -0
- package/dist/types/ssh/sshfs-mount.d.ts +10 -1
- package/dist/types/subprocess/worker-client.d.ts +20 -6
- package/dist/types/task/discovery.d.ts +5 -2
- package/dist/types/task/renderer.d.ts +1 -0
- package/dist/types/tools/ast-grep.d.ts +4 -2
- package/dist/types/tools/bash.d.ts +27 -0
- package/dist/types/tools/browser/render.d.ts +2 -0
- package/dist/types/tools/debug.d.ts +1 -0
- package/dist/types/tools/eval-render.d.ts +2 -0
- package/dist/types/tools/glob.d.ts +4 -2
- package/dist/types/tools/grep.d.ts +4 -3
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tools/renderers.d.ts +24 -0
- package/dist/types/tools/ssh.d.ts +4 -1
- package/dist/types/tts/index.d.ts +2 -0
- package/dist/types/tts/speakable.d.ts +47 -0
- package/dist/types/tts/speech-enhancer.d.ts +46 -0
- package/dist/types/tts/streaming-player.d.ts +1 -2
- package/dist/types/tts/tts-client.d.ts +11 -10
- package/dist/types/tts/tts-protocol.d.ts +7 -0
- package/dist/types/tts/vocalizer.d.ts +15 -8
- package/dist/types/utils/fetch-timeout.d.ts +4 -0
- package/dist/types/utils/git.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +1 -0
- package/dist/types/web/search/providers/gemini.d.ts +1 -0
- package/package.json +12 -12
- package/scripts/generate-legacy-pi-bundled-registry.ts +8 -2
- package/src/advisor/__tests__/advisor.test.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +2 -2
- package/src/cli/gallery-fixtures/search.ts +2 -2
- package/src/cli/models-cli.ts +19 -0
- package/src/cli/update-cli.test.ts +28 -0
- package/src/cli/update-cli.ts +35 -8
- package/src/cli.ts +27 -5
- package/src/commands/update.ts +8 -2
- package/src/config/config-file.ts +6 -6
- package/src/config/model-resolver.ts +31 -10
- package/src/config/settings-schema.ts +21 -0
- package/src/cursor.ts +1 -1
- package/src/dap/client.ts +134 -36
- package/src/discovery/helpers.ts +8 -0
- package/src/edit/file-snapshot-store.ts +12 -1
- package/src/edit/hashline/diff.ts +4 -13
- package/src/edit/hashline/execute.ts +1 -1
- package/src/edit/hashline/params.ts +5 -12
- package/src/edit/renderer.ts +4 -2
- package/src/edit/streaming.ts +15 -5
- package/src/export/html/tool-views.generated.js +34 -34
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/extensions/loader.ts +10 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
- package/src/extensibility/plugins/installer.ts +12 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +87 -11
- package/src/extensibility/plugins/loader.ts +30 -1
- package/src/extensibility/plugins/manager.ts +32 -8
- package/src/extensibility/plugins/parser.ts +7 -5
- package/src/extensibility/tool-event-input.ts +1 -1
- package/src/hindsight/client.test.ts +33 -0
- package/src/hindsight/client.ts +42 -22
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/irc/bus.ts +5 -4
- package/src/main.ts +7 -1
- package/src/mcp/oauth-flow.ts +93 -4
- package/src/mcp/smithery-auth.ts +3 -0
- package/src/mcp/smithery-connect.ts +9 -0
- package/src/mcp/smithery-registry.test.ts +51 -0
- package/src/mcp/smithery-registry.ts +27 -4
- package/src/modes/components/__tests__/move-overlay.test.ts +72 -1
- package/src/modes/components/advisor-message.ts +13 -10
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/move-overlay.ts +35 -23
- package/src/modes/components/status-line/component.ts +11 -4
- package/src/modes/components/tool-execution.ts +119 -20
- package/src/modes/components/transcript-container.ts +26 -0
- package/src/modes/components/tree-selector.ts +10 -3
- package/src/modes/controllers/event-controller.ts +23 -3
- package/src/modes/controllers/tool-args-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/rpc/rpc-client.ts +29 -16
- package/src/modes/theme/shimmer.ts +49 -15
- package/src/modes/theme/theme.ts +7 -0
- package/src/prompts/goals/goal-mode-context.md +4 -0
- package/src/prompts/goals/goal-todo-context.md +10 -2
- package/src/prompts/system/speech-rewrite.md +15 -0
- package/src/prompts/system/tiny-title-system.md +1 -1
- package/src/prompts/system/title-system-marker.md +2 -1
- package/src/prompts/system/title-system.md +2 -1
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/glob.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/sdk.ts +8 -5
- package/src/session/agent-session.ts +329 -79
- package/src/session/indexed-session-storage.ts +40 -3
- package/src/session/session-history-format.ts +6 -2
- package/src/session/session-manager.ts +83 -14
- package/src/session/session-storage.ts +112 -26
- package/src/slash-commands/helpers/usage-report.ts +6 -1
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +61 -0
- package/src/ssh/__tests__/sshfs-mount.test.ts +13 -0
- package/src/ssh/connection-manager.ts +44 -11
- package/src/ssh/sshfs-mount.ts +27 -4
- package/src/subprocess/worker-client.ts +161 -10
- package/src/task/discovery.ts +25 -2
- package/src/task/executor.ts +24 -4
- package/src/task/render.ts +26 -12
- package/src/task/renderer.ts +1 -0
- package/src/task/worktree.ts +144 -16
- package/src/tiny/text.ts +109 -17
- package/src/tools/ast-grep.ts +20 -17
- package/src/tools/bash.ts +46 -9
- package/src/tools/browser/render.ts +2 -0
- package/src/tools/debug.ts +1 -0
- package/src/tools/eval-render.ts +5 -2
- package/src/tools/gh-renderer.ts +3 -0
- package/src/tools/glob.ts +24 -20
- package/src/tools/grep.ts +39 -37
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +23 -6
- package/src/tools/renderers.ts +24 -0
- package/src/tools/ssh.ts +25 -7
- package/src/tts/index.ts +2 -0
- package/src/tts/speakable.ts +382 -0
- package/src/tts/speech-enhancer.ts +204 -0
- package/src/tts/streaming-player.ts +71 -16
- package/src/tts/tts-client.ts +11 -10
- package/src/tts/tts-protocol.ts +14 -5
- package/src/tts/tts-worker.ts +52 -49
- package/src/tts/vocalizer.ts +277 -46
- package/src/utils/clipboard.ts +49 -12
- package/src/utils/fetch-timeout.ts +10 -0
- package/src/utils/git.ts +8 -0
- package/src/web/search/index.ts +8 -0
- package/src/web/search/providers/base.ts +1 -0
- package/src/web/search/providers/gemini.ts +23 -6
package/src/tools/grep.ts
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
selectorLineRanges,
|
|
55
55
|
splitInternalUrlSel,
|
|
56
56
|
splitPathAndSel,
|
|
57
|
+
toPathList,
|
|
57
58
|
} from "./path-utils";
|
|
58
59
|
import {
|
|
59
60
|
createCachedComponent,
|
|
@@ -69,15 +70,13 @@ import { ToolError } from "./tool-errors";
|
|
|
69
70
|
import { toolResult } from "./tool-result";
|
|
70
71
|
|
|
71
72
|
const searchPathEntry = type("string").describe(
|
|
72
|
-
'file, directory, glob, internal URL, or "<file>:<lines>" selector (e.g. "src/foo.ts:50-100", "src/foo.ts:50+10", "src/foo.ts:50-100,200-300")',
|
|
73
|
+
'file, directory, glob, internal URL, or "<file>:<lines>" selector to search (e.g. "src/foo.ts:50-100", "src/foo.ts:50+10", "src/foo.ts:50-100,200-300")',
|
|
73
74
|
);
|
|
74
75
|
const searchSchema = type({
|
|
75
76
|
pattern: type("string").describe("regex pattern"),
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
'file, directory, glob, internal URL, or array of those to search; append `:<lines>` to scope a file to specific line ranges. Omitted or empty -> searches the workspace root (".")',
|
|
80
|
-
),
|
|
77
|
+
"path?": searchPathEntry.describe(
|
|
78
|
+
'file, directory, glob, internal URL, or "<file>:<lines>" selector to search; pass several as a semicolon-delimited list ("src; tests"). Omitted -> searches the workspace root (".")',
|
|
79
|
+
),
|
|
81
80
|
"case?": type("boolean").describe("case-sensitive search"),
|
|
82
81
|
"gitignore?": type("boolean").describe("respect gitignore"),
|
|
83
82
|
"skip?": type("number")
|
|
@@ -86,27 +85,6 @@ const searchSchema = type({
|
|
|
86
85
|
});
|
|
87
86
|
|
|
88
87
|
export type GrepToolInput = typeof searchSchema.infer;
|
|
89
|
-
function parseStringEncodedPathArray(input: string): string[] | null {
|
|
90
|
-
const trimmed = input.trim();
|
|
91
|
-
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
|
|
92
|
-
|
|
93
|
-
let parsed: unknown;
|
|
94
|
-
try {
|
|
95
|
-
parsed = JSON.parse(trimmed);
|
|
96
|
-
} catch {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (!Array.isArray(parsed) || parsed.some(entry => typeof entry !== "string")) {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
return parsed;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function toPathList(input: string | string[] | undefined): string[] {
|
|
107
|
-
if (typeof input === "string") return parseStringEncodedPathArray(input) ?? [input];
|
|
108
|
-
return input ?? [];
|
|
109
|
-
}
|
|
110
88
|
|
|
111
89
|
/** Maximum number of distinct files surfaced in a single response. The
|
|
112
90
|
* agent paginates further pages via `skip`. */
|
|
@@ -184,7 +162,7 @@ function parsePathSpecs(rawEntries: readonly string[]): GrepPathSpec[] {
|
|
|
184
162
|
// `:conflicts:1-1`) instead of silently widening the search or dropping a chunk.
|
|
185
163
|
if (!isReadSelectorGrammar(internalSplit.sel)) {
|
|
186
164
|
throw new ToolError(
|
|
187
|
-
`
|
|
165
|
+
`path entry "${entry}" has an invalid selector ":${internalSplit.sel}" — use ":N-M" line ranges, ":raw"/":conflicts", a range plus ":raw", or percent-encode a literal ":" as %3A`,
|
|
188
166
|
);
|
|
189
167
|
}
|
|
190
168
|
specs.push({ original: entry, clean: internalSplit.path, ranges: selectorLineRanges(internalSplit.sel) });
|
|
@@ -197,7 +175,7 @@ function parsePathSpecs(rawEntries: readonly string[]): GrepPathSpec[] {
|
|
|
197
175
|
const parsed = parseLineRanges(split.sel);
|
|
198
176
|
if (!parsed) {
|
|
199
177
|
throw new ToolError(
|
|
200
|
-
`
|
|
178
|
+
`path entry "${entry}" — only line-range selectors like ":50-100" are supported (no ":raw"/":conflicts")`,
|
|
201
179
|
);
|
|
202
180
|
}
|
|
203
181
|
if (hasGlobPathChars(split.path)) {
|
|
@@ -858,8 +836,10 @@ type SearchParams = typeof searchSchema.infer;
|
|
|
858
836
|
|
|
859
837
|
export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails> {
|
|
860
838
|
readonly name = "grep";
|
|
861
|
-
readonly approval = (args: unknown): ToolTier =>
|
|
862
|
-
|
|
839
|
+
readonly approval = (args: unknown): ToolTier => {
|
|
840
|
+
const a = args as { path?: string | string[]; paths?: string | string[] };
|
|
841
|
+
return toPathList(a.path ?? a.paths).some(pathTargetsSsh) ? "exec" : "read";
|
|
842
|
+
};
|
|
863
843
|
readonly label = "Grep";
|
|
864
844
|
readonly loadMode = "discoverable";
|
|
865
845
|
readonly summary = "Grep file contents using ripgrep (fast regex search)";
|
|
@@ -882,7 +862,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
882
862
|
_onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
|
|
883
863
|
_toolContext?: AgentToolContext,
|
|
884
864
|
): Promise<AgentToolResult<GrepToolDetails>> {
|
|
885
|
-
const { pattern,
|
|
865
|
+
const { pattern, path: rawPath, case: caseSensitive, gitignore, skip } = params;
|
|
886
866
|
|
|
887
867
|
return untilAborted(signal, async () => {
|
|
888
868
|
// Preserve the pattern verbatim — leading/trailing whitespace is
|
|
@@ -897,7 +877,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
897
877
|
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
|
|
898
878
|
throw new ToolError("Skip must be a non-negative number");
|
|
899
879
|
}
|
|
900
|
-
const scopedPaths = toPathList(
|
|
880
|
+
const scopedPaths = toPathList(rawPath);
|
|
901
881
|
const effectivePaths = scopedPaths.length > 0 ? scopedPaths : ["."];
|
|
902
882
|
const rawEntries = await expandDelimitedPathEntries(effectivePaths, this.session.cwd);
|
|
903
883
|
const pathSpecs = parsePathSpecs(rawEntries);
|
|
@@ -972,7 +952,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
972
952
|
trackImmutableSources: true,
|
|
973
953
|
surfaceExactFilePaths: true,
|
|
974
954
|
fanOutFileTargets: true,
|
|
975
|
-
multipathStatHint: " (`
|
|
955
|
+
multipathStatHint: " (`path` list entries must each exist relative to cwd)",
|
|
976
956
|
settings: this.session.settings,
|
|
977
957
|
signal,
|
|
978
958
|
localProtocolOptions: this.session.localProtocolOptions,
|
|
@@ -1047,7 +1027,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1047
1027
|
? ` (archive members were not searchable: ${archiveUnreadable.join(", ")})`
|
|
1048
1028
|
: "";
|
|
1049
1029
|
throw new ToolError(
|
|
1050
|
-
`Path not found: ${missingPaths.join(", ")};
|
|
1030
|
+
`Path not found: ${missingPaths.join(", ")}; list each target in the semicolon-delimited \`path\`${archiveHint}`,
|
|
1051
1031
|
);
|
|
1052
1032
|
}
|
|
1053
1033
|
const baseDisplayMode = resolveFileDisplayMode(this.session);
|
|
@@ -1391,6 +1371,17 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1391
1371
|
}, 0);
|
|
1392
1372
|
let lastEmittedLine: number | undefined;
|
|
1393
1373
|
const gutterPad = " ".repeat(lineNumberWidth + 1);
|
|
1374
|
+
// Track match/context lines whose displayed text was
|
|
1375
|
+
// column-truncated by the native (see `crates/pi-natives/src/grep.rs`
|
|
1376
|
+
// `truncate_line`, marker `...` at max_columns). Excluded from
|
|
1377
|
+
// seenLines so a follow-up edit anchored at that line still
|
|
1378
|
+
// requires a full-width re-read — the model saw only the
|
|
1379
|
+
// prefix. The native currently propagates `truncated` only on
|
|
1380
|
+
// the match line; context lines fall back to a length check
|
|
1381
|
+
// against `DEFAULT_MAX_COLUMN` as a conservative heuristic.
|
|
1382
|
+
const clippedLines = new Set<number>();
|
|
1383
|
+
const isNativeTruncated = (line: string): boolean =>
|
|
1384
|
+
line.length >= DEFAULT_MAX_COLUMN && line.endsWith("...");
|
|
1394
1385
|
for (const match of fileMatches) {
|
|
1395
1386
|
const pushLine = (lineNumber: number, line: string, isMatch: boolean) => {
|
|
1396
1387
|
if (lastEmittedLine !== undefined && lineNumber > lastEmittedLine + 1) {
|
|
@@ -1404,22 +1395,31 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1404
1395
|
if (match.contextBefore) {
|
|
1405
1396
|
for (const ctx of match.contextBefore) {
|
|
1406
1397
|
pushLine(ctx.lineNumber, ctx.line, false);
|
|
1398
|
+
if (isNativeTruncated(ctx.line)) clippedLines.add(ctx.lineNumber);
|
|
1407
1399
|
}
|
|
1408
1400
|
}
|
|
1409
1401
|
pushLine(match.lineNumber, match.line, true);
|
|
1410
1402
|
if (match.truncated) {
|
|
1411
1403
|
linesTruncated = true;
|
|
1404
|
+
clippedLines.add(match.lineNumber);
|
|
1412
1405
|
}
|
|
1413
1406
|
if (match.contextAfter) {
|
|
1414
1407
|
for (const ctx of match.contextAfter) {
|
|
1415
1408
|
pushLine(ctx.lineNumber, ctx.line, false);
|
|
1409
|
+
if (isNativeTruncated(ctx.line)) clippedLines.add(ctx.lineNumber);
|
|
1416
1410
|
}
|
|
1417
1411
|
}
|
|
1418
1412
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
1419
1413
|
}
|
|
1420
1414
|
if (hashContext?.tag) {
|
|
1421
1415
|
const absoluteFilePath = path.resolve(this.session.cwd, relativePath);
|
|
1422
|
-
recordSeenLinesFromBody(
|
|
1416
|
+
recordSeenLinesFromBody(
|
|
1417
|
+
this.session,
|
|
1418
|
+
absoluteFilePath,
|
|
1419
|
+
hashContext.tag,
|
|
1420
|
+
modelOut.join("\n"),
|
|
1421
|
+
clippedLines,
|
|
1422
|
+
);
|
|
1423
1423
|
}
|
|
1424
1424
|
return { model: modelOut, display: displayOut };
|
|
1425
1425
|
};
|
|
@@ -1505,6 +1505,8 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1505
1505
|
|
|
1506
1506
|
interface GrepRenderArgs {
|
|
1507
1507
|
pattern: string;
|
|
1508
|
+
path?: string | string[];
|
|
1509
|
+
/** Legacy pre-`path` argument name; kept so historical transcripts still render a scope. */
|
|
1508
1510
|
paths?: string | string[];
|
|
1509
1511
|
case?: boolean;
|
|
1510
1512
|
gitignore?: boolean;
|
|
@@ -1674,7 +1676,7 @@ function grepStatusIcon(uiTheme: Theme): string {
|
|
|
1674
1676
|
export const grepToolRenderer = {
|
|
1675
1677
|
inline: true,
|
|
1676
1678
|
renderCall(args: GrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
1677
|
-
const paths = toPathList(args.paths);
|
|
1679
|
+
const paths = toPathList(args.path ?? args.paths);
|
|
1678
1680
|
const meta: string[] = [];
|
|
1679
1681
|
if (paths.length) meta.push(`in ${paths.join(", ")}`);
|
|
1680
1682
|
if (args.case === false) meta.push("case:insensitive");
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -510,6 +510,39 @@ export function normalizePathLikeInput(input: string): string {
|
|
|
510
510
|
return stripOuterDoubleQuotes(input.trim());
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Parse a JSON-encoded array of path strings (e.g. `'["a.ts","b.ts"]'`).
|
|
515
|
+
* Returns `null` when the input is not a bracketed JSON string array, so the
|
|
516
|
+
* caller can fall back to treating the input as a single literal path.
|
|
517
|
+
*/
|
|
518
|
+
function parseStringEncodedPathArray(input: string): string[] | null {
|
|
519
|
+
const trimmed = input.trim();
|
|
520
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
|
|
521
|
+
|
|
522
|
+
let parsed: unknown;
|
|
523
|
+
try {
|
|
524
|
+
parsed = JSON.parse(trimmed);
|
|
525
|
+
} catch {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!Array.isArray(parsed) || parsed.some(entry => typeof entry !== "string")) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
return parsed;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Normalize a path argument that may arrive as a single string, a JSON-encoded
|
|
537
|
+
* string array (`'["a.ts"]'`), or an actual array into a flat `string[]`.
|
|
538
|
+
* Delimited single strings (`"a.ts b.ts"`) are left for
|
|
539
|
+
* {@link expandDelimitedPathEntries} to split.
|
|
540
|
+
*/
|
|
541
|
+
export function toPathList(input: string | string[] | undefined): string[] {
|
|
542
|
+
if (typeof input === "string") return parseStringEncodedPathArray(input) ?? [input];
|
|
543
|
+
return input ?? [];
|
|
544
|
+
}
|
|
545
|
+
|
|
513
546
|
const GLOB_PATH_CHARS = ["*", "?", "[", "{"] as const;
|
|
514
547
|
|
|
515
548
|
export function hasGlobPathChars(filePath: string): boolean {
|
|
@@ -592,12 +625,21 @@ async function delimitedPathPartResolves(entry: string, cwd: string, splitter: P
|
|
|
592
625
|
}
|
|
593
626
|
}
|
|
594
627
|
|
|
628
|
+
/**
|
|
629
|
+
* How many split parts must resolve to an existing path for the split to win.
|
|
630
|
+
* Semicolon is the documented list delimiter, so it splits unconditionally
|
|
631
|
+
* (`"none"`) — an all-missing list must still fan out so multi-path missing
|
|
632
|
+
* semantics can name every entry. Comma is legacy recovery (`"some"`), and
|
|
633
|
+
* whitespace/mixed are aggressive heuristics gated on every part existing.
|
|
634
|
+
*/
|
|
635
|
+
type DelimitedResolveRequirement = "all" | "some" | "none";
|
|
636
|
+
|
|
595
637
|
async function tryDelimitedPathSplit(
|
|
596
638
|
entry: string,
|
|
597
639
|
cwd: string,
|
|
598
640
|
splitter: PathEntrySplitter,
|
|
599
641
|
mode: DelimitedPathSplitMode,
|
|
600
|
-
|
|
642
|
+
requirement: DelimitedResolveRequirement,
|
|
601
643
|
): Promise<string[] | null> {
|
|
602
644
|
const rawParts = splitTopLevelDelimitedPath(entry, mode);
|
|
603
645
|
if (rawParts.length < 2) return null;
|
|
@@ -606,9 +648,12 @@ async function tryDelimitedPathSplit(
|
|
|
606
648
|
if (parts.length === 0) return null;
|
|
607
649
|
if (parts.length < 2 && rawParts.length === parts.length) return null;
|
|
608
650
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
651
|
+
if (requirement !== "none") {
|
|
652
|
+
const resolved = await Promise.all(parts.map(part => delimitedPathPartResolves(part, cwd, splitter)));
|
|
653
|
+
const valid = requirement === "all" ? resolved.every(Boolean) : resolved.some(Boolean);
|
|
654
|
+
if (!valid) return null;
|
|
655
|
+
}
|
|
656
|
+
return parts;
|
|
612
657
|
}
|
|
613
658
|
|
|
614
659
|
/**
|
|
@@ -632,10 +677,10 @@ export async function splitDelimitedPathEntry(
|
|
|
632
677
|
}
|
|
633
678
|
|
|
634
679
|
return (
|
|
635
|
-
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "semicolon",
|
|
636
|
-
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "comma",
|
|
637
|
-
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "whitespace",
|
|
638
|
-
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "mixed",
|
|
680
|
+
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "semicolon", "none")) ??
|
|
681
|
+
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "comma", "some")) ??
|
|
682
|
+
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "whitespace", "all")) ??
|
|
683
|
+
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "mixed", "all"))
|
|
639
684
|
);
|
|
640
685
|
}
|
|
641
686
|
|
|
@@ -1094,11 +1139,11 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
1094
1139
|
const { rawPaths: inputs, cwd, internalUrlAction } = opts;
|
|
1095
1140
|
const normalizedRawPaths = inputs.map(normalizePathLikeInput);
|
|
1096
1141
|
if (normalizedRawPaths.some(rawPath => rawPath.length === 0)) {
|
|
1097
|
-
throw new ToolError("
|
|
1142
|
+
throw new ToolError("Search scope entries must be non-empty paths or globs");
|
|
1098
1143
|
}
|
|
1099
1144
|
const rawPaths = await expandDelimitedPathEntries(normalizedRawPaths, cwd);
|
|
1100
1145
|
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
1101
|
-
throw new ToolError("
|
|
1146
|
+
throw new ToolError("Search scope entries must be non-empty paths or globs");
|
|
1102
1147
|
}
|
|
1103
1148
|
// Strict external-URL schemes. `file://` is intentionally absent: it has
|
|
1104
1149
|
// local-path semantics (expandPath strips it downstream), so it flows through
|
package/src/tools/read.ts
CHANGED
|
@@ -316,7 +316,14 @@ function formatMergedBraceLine(
|
|
|
316
316
|
|
|
317
317
|
function countTextLines(text: string): number {
|
|
318
318
|
if (text.length === 0) return 0;
|
|
319
|
-
|
|
319
|
+
// Count newlines directly instead of allocating an array via split("\n").
|
|
320
|
+
// Called on every read of file content; the result is identical (N newlines
|
|
321
|
+
// ⇒ N+1 lines for non-empty text).
|
|
322
|
+
let lines = 1;
|
|
323
|
+
for (let i = 0; i < text.length; i++) {
|
|
324
|
+
if (text.charCodeAt(i) === 10) lines++;
|
|
325
|
+
}
|
|
326
|
+
return lines;
|
|
320
327
|
}
|
|
321
328
|
|
|
322
329
|
function contiguousLineNumbers(startLine: number, count: number): number[] {
|
|
@@ -1317,7 +1324,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1317
1324
|
startLine: startNum,
|
|
1318
1325
|
lineNumbers: Array.from({ length: lineCount }, (_, i) => startNum + i),
|
|
1319
1326
|
};
|
|
1320
|
-
if (shouldAddHashLines) seenLines = contiguousLineNumbers(startNum,
|
|
1327
|
+
if (shouldAddHashLines) seenLines = contiguousLineNumbers(startNum, lineCount);
|
|
1321
1328
|
const formatted = formatTextWithMode(content, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
1322
1329
|
if (!hashContext || emittedHashlineHeader) return formatted;
|
|
1323
1330
|
emittedHashlineHeader = true;
|
|
@@ -1552,6 +1559,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1552
1559
|
const displayLineByNumber = new Map<number, string>();
|
|
1553
1560
|
const fullLines = rawSelector ? undefined : await readBracketContextFullLines(absolutePath, fileSize);
|
|
1554
1561
|
let columnTruncated = 0;
|
|
1562
|
+
const clippedLines = new Set<number>();
|
|
1555
1563
|
let displayContent: { text: string; startLine: number; lineNumbers?: Array<number | null> } | undefined;
|
|
1556
1564
|
|
|
1557
1565
|
for (const range of ranges) {
|
|
@@ -1599,6 +1607,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1599
1607
|
if (!cloned) cloned = collectedLines.slice();
|
|
1600
1608
|
cloned[i] = text;
|
|
1601
1609
|
columnTruncated = maxColumns;
|
|
1610
|
+
clippedLines.add(range.startLine + i);
|
|
1602
1611
|
}
|
|
1603
1612
|
}
|
|
1604
1613
|
if (cloned) displayLines = cloned;
|
|
@@ -1626,7 +1635,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1626
1635
|
if (visibleText !== undefined) return visibleText;
|
|
1627
1636
|
if (maxColumns <= 0) return sourceText;
|
|
1628
1637
|
const truncated = truncateLine(sourceText, maxColumns);
|
|
1629
|
-
if (truncated.wasTruncated)
|
|
1638
|
+
if (truncated.wasTruncated) {
|
|
1639
|
+
columnTruncated = maxColumns;
|
|
1640
|
+
clippedLines.add(lineNumber);
|
|
1641
|
+
}
|
|
1630
1642
|
return truncated.text;
|
|
1631
1643
|
},
|
|
1632
1644
|
},
|
|
@@ -1644,7 +1656,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1644
1656
|
if (shouldAddHashLines && outputText) {
|
|
1645
1657
|
const tag = await recordFileSnapshot(this.session, absolutePath);
|
|
1646
1658
|
if (tag) {
|
|
1647
|
-
recordSeenLinesFromBody(this.session, absolutePath, tag, outputText);
|
|
1659
|
+
recordSeenLinesFromBody(this.session, absolutePath, tag, outputText, clippedLines);
|
|
1648
1660
|
outputText = `${formatReadHashlineHeader(formatPathRelativeToCwd(absolutePath, this.session.cwd), tag)}\n${outputText}`;
|
|
1649
1661
|
}
|
|
1650
1662
|
}
|
|
@@ -2483,6 +2495,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2483
2495
|
// ellipsis-truncated text made every long-line file uneditable on
|
|
2484
2496
|
// the next edit attempt.
|
|
2485
2497
|
let displayLines: string[] = collectedLines;
|
|
2498
|
+
const clippedLines = new Set<number>();
|
|
2486
2499
|
if (!rawSelector && maxColumns > 0) {
|
|
2487
2500
|
let cloned: string[] | undefined;
|
|
2488
2501
|
for (let i = 0; i < collectedLines.length; i++) {
|
|
@@ -2491,6 +2504,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2491
2504
|
if (!cloned) cloned = collectedLines.slice();
|
|
2492
2505
|
cloned[i] = text;
|
|
2493
2506
|
columnTruncated = maxColumns;
|
|
2507
|
+
clippedLines.add(startLineDisplay + i);
|
|
2494
2508
|
}
|
|
2495
2509
|
}
|
|
2496
2510
|
if (cloned) displayLines = cloned;
|
|
@@ -2573,7 +2587,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2573
2587
|
if (visibleText !== undefined) return visibleText;
|
|
2574
2588
|
if (maxColumns <= 0) return sourceText;
|
|
2575
2589
|
const truncated = truncateLine(sourceText, maxColumns);
|
|
2576
|
-
if (truncated.wasTruncated)
|
|
2590
|
+
if (truncated.wasTruncated) {
|
|
2591
|
+
columnTruncated = maxColumns;
|
|
2592
|
+
clippedLines.add(lineNumber);
|
|
2593
|
+
}
|
|
2577
2594
|
return truncated.text;
|
|
2578
2595
|
},
|
|
2579
2596
|
},
|
|
@@ -2647,7 +2664,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2647
2664
|
}
|
|
2648
2665
|
|
|
2649
2666
|
if (hashContext?.tag) {
|
|
2650
|
-
recordSeenLinesFromBody(this.session, absolutePath, hashContext.tag, outputText);
|
|
2667
|
+
recordSeenLinesFromBody(this.session, absolutePath, hashContext.tag, outputText, clippedLines);
|
|
2651
2668
|
}
|
|
2652
2669
|
|
|
2653
2670
|
if (capturedDisplayContent) {
|
package/src/tools/renderers.ts
CHANGED
|
@@ -63,6 +63,30 @@ export type ToolRenderer = {
|
|
|
63
63
|
* with the final render and may commit like any settled stream.
|
|
64
64
|
*/
|
|
65
65
|
provisionalPartialResult?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Whether the renderer's pending-call path visibly consumes
|
|
68
|
+
* `options.spinnerFrame`. Used to avoid scheduling repaint ticks for live
|
|
69
|
+
* partial calls whose bytes cannot change between spinner frames.
|
|
70
|
+
*/
|
|
71
|
+
animatedPendingPreview?: boolean | ((args: unknown) => boolean);
|
|
72
|
+
/**
|
|
73
|
+
* Whether the renderer's partial-result path visibly consumes
|
|
74
|
+
* `options.spinnerFrame`.
|
|
75
|
+
*/
|
|
76
|
+
animatedPartialResult?: boolean | ((args: unknown) => boolean);
|
|
77
|
+
/**
|
|
78
|
+
* Whether replacing a streamed pending placeholder with the first result
|
|
79
|
+
* requires a full viewport repaint. Use for merged renderers whose pending
|
|
80
|
+
* streamed args may have committed placeholder rows that the result render
|
|
81
|
+
* re-anchors instead of preserving.
|
|
82
|
+
*/
|
|
83
|
+
forceFirstResultViewportRepaint?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Whether settling a provisional partial result into the final render requires
|
|
86
|
+
* a full viewport repaint. Use when the result renderer changes chrome or
|
|
87
|
+
* frame topology at `options.isPartial: true -> false`.
|
|
88
|
+
*/
|
|
89
|
+
forceResultViewportRepaintOnSettle?: boolean;
|
|
66
90
|
};
|
|
67
91
|
|
|
68
92
|
export const toolRenderers: Record<string, ToolRenderer> = {
|
package/src/tools/ssh.ts
CHANGED
|
@@ -261,23 +261,33 @@ function formatSshCommandLines(command: string, uiTheme: Theme): string[] {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
export const sshToolRenderer = {
|
|
264
|
-
|
|
264
|
+
animatedPendingPreview: true,
|
|
265
|
+
renderCall(args: SshRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
265
266
|
const host = args.host || "…";
|
|
266
267
|
const command = args.command ?? "";
|
|
267
|
-
const header = renderStatusLine({ icon: "pending", title: "SSH", description: `[${host}]` }, uiTheme);
|
|
268
268
|
const cmdLines = formatSshCommandLines(command, uiTheme);
|
|
269
269
|
const outputBlock = new CachedOutputBlock();
|
|
270
270
|
return markFramedBlockComponent({
|
|
271
|
-
render: (width: number): readonly string[] =>
|
|
272
|
-
|
|
271
|
+
render: (width: number): readonly string[] => {
|
|
272
|
+
const header = renderStatusLine(
|
|
273
|
+
{
|
|
274
|
+
icon: options.spinnerFrame !== undefined ? "running" : "pending",
|
|
275
|
+
spinnerFrame: options.spinnerFrame,
|
|
276
|
+
title: "SSH",
|
|
277
|
+
description: `[${host}]`,
|
|
278
|
+
},
|
|
279
|
+
uiTheme,
|
|
280
|
+
);
|
|
281
|
+
return outputBlock.render(
|
|
273
282
|
{
|
|
274
283
|
header,
|
|
275
|
-
state: "pending",
|
|
276
|
-
sections: [{ lines: capPreviewLines(cmdLines, uiTheme, { expanded:
|
|
284
|
+
state: options.spinnerFrame !== undefined ? "running" : "pending",
|
|
285
|
+
sections: [{ lines: capPreviewLines(cmdLines, uiTheme, { expanded: options.expanded }) }],
|
|
277
286
|
width,
|
|
278
287
|
},
|
|
279
288
|
uiTheme,
|
|
280
|
-
)
|
|
289
|
+
);
|
|
290
|
+
},
|
|
281
291
|
invalidate: () => {
|
|
282
292
|
outputBlock.invalidate();
|
|
283
293
|
},
|
|
@@ -392,4 +402,12 @@ export const sshToolRenderer = {
|
|
|
392
402
|
// land below and strand a duplicate pending header above the final frame
|
|
393
403
|
// ([#3177](https://github.com/can1357/oh-my-pi/issues/3177)).
|
|
394
404
|
provisionalPartialResult: true,
|
|
405
|
+
// Streamed args can initially render the SSH placeholder (`⏳ SSH: […]` /
|
|
406
|
+
// `$ …`), then the first partial result inserts the `Output` section and
|
|
407
|
+
// re-anchors the frame. Force a full repaint at that seam so placeholder rows
|
|
408
|
+
// do not survive in viewport/native scrollback.
|
|
409
|
+
forceFirstResultViewportRepaint: true,
|
|
410
|
+
// The provisional pending-result frame settles into the final `⇄ SSH: [host]`
|
|
411
|
+
// frame, so clear/replay the viewport at that topology flip too.
|
|
412
|
+
forceResultViewportRepaintOnSettle: true,
|
|
395
413
|
};
|
package/src/tts/index.ts
CHANGED