@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.1
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 +41 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +10 -29
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +71 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/renderer.ts +7 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +2 -2
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +9 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +5 -6
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +79 -45
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- package/src/modes/controllers/input-controller.ts +13 -0
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +624 -52
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +14 -87
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +14 -3
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +10 -3
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +102 -114
- package/src/prompts/tools/read.md +1 -0
- package/src/prompts/tools/resolve.md +6 -5
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +428 -106
- package/src/session/blob-store.ts +36 -3
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +17 -7
- package/src/task/index.ts +3 -0
- package/src/task/render.ts +21 -15
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +27 -4
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +3 -1
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +7 -6
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +516 -233
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +44 -9
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
package/src/tools/ast-edit.ts
CHANGED
|
@@ -7,33 +7,27 @@ import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
8
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
9
9
|
import { computeLineHash, HL_BODY_SEP } from "../hashline/hash";
|
|
10
|
-
import { InternalUrlRouter } from "../internal-urls";
|
|
11
10
|
import type { Theme } from "../modes/theme/theme";
|
|
12
11
|
import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
|
|
13
|
-
import { Ellipsis,
|
|
12
|
+
import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
14
13
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
15
14
|
import type { ToolSession } from ".";
|
|
16
15
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
17
16
|
import { formatGroupedFiles } from "./grouped-file-output";
|
|
18
17
|
import type { OutputMeta } from "./output-meta";
|
|
18
|
+
import { resolveToolSearchScope } from "./path-utils";
|
|
19
19
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
normalizePathLikeInput,
|
|
23
|
-
parseSearchPath,
|
|
24
|
-
partitionExistingPaths,
|
|
25
|
-
resolveExplicitSearchPaths,
|
|
26
|
-
resolveToCwd,
|
|
27
|
-
} from "./path-utils";
|
|
28
|
-
import {
|
|
20
|
+
appendParseErrorsBulletList,
|
|
21
|
+
createCachedComponent,
|
|
29
22
|
dedupeParseErrors,
|
|
30
23
|
formatCodeFrameLine,
|
|
31
24
|
formatCount,
|
|
32
25
|
formatEmptyMessage,
|
|
33
26
|
formatErrorMessage,
|
|
34
27
|
formatParseErrors,
|
|
35
|
-
|
|
28
|
+
formatParseErrorsCountLabel,
|
|
36
29
|
PREVIEW_LIMITS,
|
|
30
|
+
splitGroupsByBlankLine,
|
|
37
31
|
} from "./render-utils";
|
|
38
32
|
import { queueResolveHandler } from "./resolve";
|
|
39
33
|
import { ToolError } from "./tool-errors";
|
|
@@ -205,63 +199,12 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
205
199
|
const normalizedRewrites = Object.fromEntries(ops);
|
|
206
200
|
const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
|
|
207
201
|
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
215
|
-
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
216
|
-
}
|
|
217
|
-
const internalRouter = InternalUrlRouter.instance();
|
|
218
|
-
const resolvedPathInputs: string[] = [];
|
|
219
|
-
for (const rawPath of rawPaths) {
|
|
220
|
-
if (!internalRouter.canHandle(rawPath)) {
|
|
221
|
-
resolvedPathInputs.push(rawPath);
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (hasGlobPathChars(rawPath)) {
|
|
225
|
-
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
226
|
-
}
|
|
227
|
-
const resource = await internalRouter.resolve(rawPath);
|
|
228
|
-
if (!resource.sourcePath) {
|
|
229
|
-
throw new ToolError(`Cannot rewrite internal URL without backing file: ${rawPath}`);
|
|
230
|
-
}
|
|
231
|
-
resolvedPathInputs.push(resource.sourcePath);
|
|
232
|
-
}
|
|
233
|
-
let effectivePathInputs = resolvedPathInputs;
|
|
234
|
-
if (resolvedPathInputs.length > 1) {
|
|
235
|
-
const partition = await partitionExistingPaths(resolvedPathInputs, this.session.cwd, parseSearchPath);
|
|
236
|
-
if (partition.valid.length === 0) {
|
|
237
|
-
throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
|
|
238
|
-
}
|
|
239
|
-
effectivePathInputs = partition.valid;
|
|
240
|
-
}
|
|
241
|
-
if (effectivePathInputs.length === 1) {
|
|
242
|
-
const parsedPath = parseSearchPath(effectivePathInputs[0] ?? ".");
|
|
243
|
-
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
244
|
-
globFilter = parsedPath.glob;
|
|
245
|
-
scopePath = formatScopePath(searchPath);
|
|
246
|
-
} else {
|
|
247
|
-
const multiSearchPath = await resolveExplicitSearchPaths(effectivePathInputs, this.session.cwd, globFilter);
|
|
248
|
-
if (!multiSearchPath) {
|
|
249
|
-
throw new ToolError("`paths` must contain at least one path or glob");
|
|
250
|
-
}
|
|
251
|
-
searchPath = multiSearchPath.basePath;
|
|
252
|
-
globFilter = multiSearchPath.targets ? undefined : multiSearchPath.glob;
|
|
253
|
-
multiTargets = multiSearchPath.targets;
|
|
254
|
-
scopePath = multiSearchPath.scopePath;
|
|
255
|
-
}
|
|
256
|
-
const resolvedSearchPath = searchPath;
|
|
257
|
-
scopePath = scopePath ?? formatScopePath(resolvedSearchPath);
|
|
258
|
-
let isDirectory: boolean;
|
|
259
|
-
try {
|
|
260
|
-
const stat = await Bun.file(resolvedSearchPath).stat();
|
|
261
|
-
isDirectory = stat.isDirectory();
|
|
262
|
-
} catch {
|
|
263
|
-
throw new ToolError(`Path not found: ${scopePath}`);
|
|
264
|
-
}
|
|
202
|
+
const scope = await resolveToolSearchScope({
|
|
203
|
+
rawPaths: params.paths,
|
|
204
|
+
cwd: this.session.cwd,
|
|
205
|
+
internalUrlAction: "rewrite",
|
|
206
|
+
});
|
|
207
|
+
const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
|
|
265
208
|
|
|
266
209
|
const result = await runAstEditOnce(multiTargets, resolvedSearchPath, globFilter, {
|
|
267
210
|
rewrites: normalizedRewrites,
|
|
@@ -502,15 +445,7 @@ export const astEditToolRenderer = {
|
|
|
502
445
|
if (filesSearched > 0) meta.push(`searched ${filesSearched}`);
|
|
503
446
|
const header = renderStatusLine({ icon: "warning", title: "AST Edit", description, meta }, uiTheme);
|
|
504
447
|
const lines = [header, formatEmptyMessage("No replacements made", uiTheme)];
|
|
505
|
-
|
|
506
|
-
const capped = details.parseErrors.slice(0, PARSE_ERRORS_LIMIT);
|
|
507
|
-
for (const err of capped) {
|
|
508
|
-
lines.push(uiTheme.fg("warning", ` - ${err}`));
|
|
509
|
-
}
|
|
510
|
-
if (details.parseErrors.length > PARSE_ERRORS_LIMIT) {
|
|
511
|
-
lines.push(uiTheme.fg("dim", ` … ${details.parseErrors.length - PARSE_ERRORS_LIMIT} more`));
|
|
512
|
-
}
|
|
513
|
-
}
|
|
448
|
+
appendParseErrorsBulletList(lines, details?.parseErrors, uiTheme);
|
|
514
449
|
return new Text(lines.join("\n"), 0, 0);
|
|
515
450
|
}
|
|
516
451
|
|
|
@@ -523,28 +458,7 @@ export const astEditToolRenderer = {
|
|
|
523
458
|
const description = rewriteCount === 1 ? args?.ops?.[0]?.pat : undefined;
|
|
524
459
|
|
|
525
460
|
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
526
|
-
const
|
|
527
|
-
const hasSeparators = rawLines.some(line => line.trim().length === 0);
|
|
528
|
-
const allGroups: string[][] = [];
|
|
529
|
-
if (hasSeparators) {
|
|
530
|
-
let current: string[] = [];
|
|
531
|
-
for (const line of rawLines) {
|
|
532
|
-
if (line.trim().length === 0) {
|
|
533
|
-
if (current.length > 0) {
|
|
534
|
-
allGroups.push(current);
|
|
535
|
-
current = [];
|
|
536
|
-
}
|
|
537
|
-
continue;
|
|
538
|
-
}
|
|
539
|
-
current.push(line);
|
|
540
|
-
}
|
|
541
|
-
if (current.length > 0) allGroups.push(current);
|
|
542
|
-
} else {
|
|
543
|
-
const nonEmpty = rawLines.filter(line => line.trim().length > 0);
|
|
544
|
-
if (nonEmpty.length > 0) {
|
|
545
|
-
allGroups.push(nonEmpty);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
461
|
+
const allGroups = splitGroupsByBlankLine(textContent.split("\n"));
|
|
548
462
|
const changeGroups = allGroups.filter(
|
|
549
463
|
group => !group[0]?.startsWith("Safety cap reached") && !group[0]?.startsWith("Parse issues:"),
|
|
550
464
|
);
|
|
@@ -560,23 +474,15 @@ export const astEditToolRenderer = {
|
|
|
560
474
|
extraLines.push(uiTheme.fg("warning", "limit reached; narrow path"));
|
|
561
475
|
}
|
|
562
476
|
if (details?.parseErrors?.length) {
|
|
563
|
-
|
|
564
|
-
const label =
|
|
565
|
-
total > PARSE_ERRORS_LIMIT
|
|
566
|
-
? `${PARSE_ERRORS_LIMIT} / ${total} parse issues`
|
|
567
|
-
: `${total} parse issue${total !== 1 ? "s" : ""}`;
|
|
568
|
-
extraLines.push(uiTheme.fg("warning", label));
|
|
477
|
+
extraLines.push(uiTheme.fg("warning", formatParseErrorsCountLabel(details.parseErrors)));
|
|
569
478
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const { expanded } = options;
|
|
574
|
-
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
575
|
-
if (cached?.key === key) return cached.lines;
|
|
479
|
+
return createCachedComponent(
|
|
480
|
+
() => options.expanded,
|
|
481
|
+
width => {
|
|
576
482
|
const changeLines = renderTreeList(
|
|
577
483
|
{
|
|
578
484
|
items: changeGroups,
|
|
579
|
-
expanded,
|
|
485
|
+
expanded: options.expanded,
|
|
580
486
|
maxCollapsed: changeGroups.length,
|
|
581
487
|
maxCollapsedLines: COLLAPSED_CHANGE_LIMIT,
|
|
582
488
|
itemType: "change",
|
|
@@ -591,14 +497,9 @@ export const astEditToolRenderer = {
|
|
|
591
497
|
},
|
|
592
498
|
uiTheme,
|
|
593
499
|
);
|
|
594
|
-
|
|
595
|
-
cached = { key, lines: rendered };
|
|
596
|
-
return rendered;
|
|
500
|
+
return [header, ...changeLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
597
501
|
},
|
|
598
|
-
|
|
599
|
-
cached = undefined;
|
|
600
|
-
},
|
|
601
|
-
};
|
|
502
|
+
);
|
|
602
503
|
},
|
|
603
504
|
mergeCallAndResult: true,
|
|
604
505
|
};
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -6,34 +6,28 @@ import { Text } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
8
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
9
|
-
import { InternalUrlRouter } from "../internal-urls";
|
|
10
9
|
import type { Theme } from "../modes/theme/theme";
|
|
11
10
|
import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
|
|
12
|
-
import { Ellipsis,
|
|
11
|
+
import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
13
12
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
14
13
|
import type { ToolSession } from ".";
|
|
15
14
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
16
15
|
import { formatGroupedFiles } from "./grouped-file-output";
|
|
17
16
|
import { formatMatchLine } from "./match-line-format";
|
|
18
17
|
import type { OutputMeta } from "./output-meta";
|
|
18
|
+
import { resolveToolSearchScope } from "./path-utils";
|
|
19
19
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
normalizePathLikeInput,
|
|
23
|
-
parseSearchPath,
|
|
24
|
-
partitionExistingPaths,
|
|
25
|
-
resolveExplicitSearchPaths,
|
|
26
|
-
resolveToCwd,
|
|
27
|
-
} from "./path-utils";
|
|
28
|
-
import {
|
|
20
|
+
appendParseErrorsBulletList,
|
|
21
|
+
createCachedComponent,
|
|
29
22
|
dedupeParseErrors,
|
|
30
23
|
formatCodeFrameLine,
|
|
31
24
|
formatCount,
|
|
32
25
|
formatEmptyMessage,
|
|
33
26
|
formatErrorMessage,
|
|
34
27
|
formatParseErrors,
|
|
35
|
-
|
|
28
|
+
formatParseErrorsCountLabel,
|
|
36
29
|
PREVIEW_LIMITS,
|
|
30
|
+
splitGroupsByBlankLine,
|
|
37
31
|
} from "./render-utils";
|
|
38
32
|
import { ToolError } from "./tool-errors";
|
|
39
33
|
import { toolResult } from "./tool-result";
|
|
@@ -150,64 +144,12 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
150
144
|
if (!Number.isFinite(skip) || skip < 0) {
|
|
151
145
|
throw new ToolError("skip must be a non-negative number");
|
|
152
146
|
}
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
160
|
-
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
161
|
-
}
|
|
162
|
-
const internalRouter = InternalUrlRouter.instance();
|
|
163
|
-
const resolvedPathInputs: string[] = [];
|
|
164
|
-
for (const rawPath of rawPaths) {
|
|
165
|
-
if (!internalRouter.canHandle(rawPath)) {
|
|
166
|
-
resolvedPathInputs.push(rawPath);
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (hasGlobPathChars(rawPath)) {
|
|
170
|
-
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
171
|
-
}
|
|
172
|
-
const resource = await internalRouter.resolve(rawPath);
|
|
173
|
-
if (!resource.sourcePath) {
|
|
174
|
-
throw new ToolError(`Cannot search internal URL without backing file: ${rawPath}`);
|
|
175
|
-
}
|
|
176
|
-
resolvedPathInputs.push(resource.sourcePath);
|
|
177
|
-
}
|
|
178
|
-
let effectivePathInputs = resolvedPathInputs;
|
|
179
|
-
if (resolvedPathInputs.length > 1) {
|
|
180
|
-
const partition = await partitionExistingPaths(resolvedPathInputs, this.session.cwd, parseSearchPath);
|
|
181
|
-
if (partition.valid.length === 0) {
|
|
182
|
-
throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
|
|
183
|
-
}
|
|
184
|
-
effectivePathInputs = partition.valid;
|
|
185
|
-
}
|
|
186
|
-
if (effectivePathInputs.length === 1) {
|
|
187
|
-
const parsedPath = parseSearchPath(effectivePathInputs[0] ?? ".");
|
|
188
|
-
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
189
|
-
globFilter = parsedPath.glob;
|
|
190
|
-
scopePath = formatScopePath(searchPath);
|
|
191
|
-
} else {
|
|
192
|
-
const multiSearchPath = await resolveExplicitSearchPaths(effectivePathInputs, this.session.cwd, globFilter);
|
|
193
|
-
if (!multiSearchPath) {
|
|
194
|
-
throw new ToolError("`paths` must contain at least one path or glob");
|
|
195
|
-
}
|
|
196
|
-
searchPath = multiSearchPath.basePath;
|
|
197
|
-
globFilter = multiSearchPath.targets ? undefined : multiSearchPath.glob;
|
|
198
|
-
multiTargets = multiSearchPath.targets;
|
|
199
|
-
scopePath = multiSearchPath.scopePath;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const resolvedSearchPath = searchPath;
|
|
203
|
-
scopePath = scopePath ?? formatScopePath(resolvedSearchPath);
|
|
204
|
-
let isDirectory: boolean;
|
|
205
|
-
try {
|
|
206
|
-
const stat = await Bun.file(resolvedSearchPath).stat();
|
|
207
|
-
isDirectory = stat.isDirectory();
|
|
208
|
-
} catch {
|
|
209
|
-
throw new ToolError(`Path not found: ${scopePath}`);
|
|
210
|
-
}
|
|
147
|
+
const scope = await resolveToolSearchScope({
|
|
148
|
+
rawPaths: params.paths,
|
|
149
|
+
cwd: this.session.cwd,
|
|
150
|
+
internalUrlAction: "search",
|
|
151
|
+
});
|
|
152
|
+
const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
|
|
211
153
|
|
|
212
154
|
const DEFAULT_AST_LIMIT = 50;
|
|
213
155
|
const result = multiTargets
|
|
@@ -388,13 +330,7 @@ export const astGrepToolRenderer = {
|
|
|
388
330
|
const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
|
|
389
331
|
if (details?.parseErrors?.length) {
|
|
390
332
|
lines.push(uiTheme.fg("warning", "Query may be mis-scoped; narrow `paths` before concluding absence"));
|
|
391
|
-
|
|
392
|
-
for (const err of capped) {
|
|
393
|
-
lines.push(uiTheme.fg("warning", ` - ${err}`));
|
|
394
|
-
}
|
|
395
|
-
if (details.parseErrors.length > PARSE_ERRORS_LIMIT) {
|
|
396
|
-
lines.push(uiTheme.fg("dim", ` … ${details.parseErrors.length - PARSE_ERRORS_LIMIT} more`));
|
|
397
|
-
}
|
|
333
|
+
appendParseErrorsBulletList(lines, details.parseErrors, uiTheme);
|
|
398
334
|
}
|
|
399
335
|
return new Text(lines.join("\n"), 0, 0);
|
|
400
336
|
}
|
|
@@ -411,28 +347,7 @@ export const astGrepToolRenderer = {
|
|
|
411
347
|
);
|
|
412
348
|
|
|
413
349
|
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
414
|
-
const
|
|
415
|
-
const hasSeparators = rawLines.some(line => line.trim().length === 0);
|
|
416
|
-
const allGroups: string[][] = [];
|
|
417
|
-
if (hasSeparators) {
|
|
418
|
-
let current: string[] = [];
|
|
419
|
-
for (const line of rawLines) {
|
|
420
|
-
if (line.trim().length === 0) {
|
|
421
|
-
if (current.length > 0) {
|
|
422
|
-
allGroups.push(current);
|
|
423
|
-
current = [];
|
|
424
|
-
}
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
current.push(line);
|
|
428
|
-
}
|
|
429
|
-
if (current.length > 0) allGroups.push(current);
|
|
430
|
-
} else {
|
|
431
|
-
const nonEmpty = rawLines.filter(line => line.trim().length > 0);
|
|
432
|
-
if (nonEmpty.length > 0) {
|
|
433
|
-
allGroups.push(nonEmpty);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
350
|
+
const allGroups = splitGroupsByBlankLine(textContent.split("\n"));
|
|
436
351
|
const matchGroups = allGroups.filter(
|
|
437
352
|
group => !group[0]?.startsWith("Result limit reached") && !group[0]?.startsWith("Parse issues:"),
|
|
438
353
|
);
|
|
@@ -442,24 +357,16 @@ export const astGrepToolRenderer = {
|
|
|
442
357
|
extraLines.push(uiTheme.fg("warning", "limit reached; narrow paths or increase limit"));
|
|
443
358
|
}
|
|
444
359
|
if (details?.parseErrors?.length) {
|
|
445
|
-
|
|
446
|
-
const label =
|
|
447
|
-
total > PARSE_ERRORS_LIMIT
|
|
448
|
-
? `${PARSE_ERRORS_LIMIT} / ${total} parse issues`
|
|
449
|
-
: `${total} parse issue${total !== 1 ? "s" : ""}`;
|
|
450
|
-
extraLines.push(uiTheme.fg("warning", label));
|
|
360
|
+
extraLines.push(uiTheme.fg("warning", formatParseErrorsCountLabel(details.parseErrors)));
|
|
451
361
|
}
|
|
452
362
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const { expanded } = options;
|
|
457
|
-
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
458
|
-
if (cached?.key === key) return cached.lines;
|
|
363
|
+
return createCachedComponent(
|
|
364
|
+
() => options.expanded,
|
|
365
|
+
width => {
|
|
459
366
|
const matchLines = renderTreeList(
|
|
460
367
|
{
|
|
461
368
|
items: matchGroups,
|
|
462
|
-
expanded,
|
|
369
|
+
expanded: options.expanded,
|
|
463
370
|
maxCollapsed: matchGroups.length,
|
|
464
371
|
maxCollapsedLines: COLLAPSED_MATCH_LIMIT,
|
|
465
372
|
itemType: "match",
|
|
@@ -473,14 +380,9 @@ export const astGrepToolRenderer = {
|
|
|
473
380
|
},
|
|
474
381
|
uiTheme,
|
|
475
382
|
);
|
|
476
|
-
|
|
477
|
-
cached = { key, lines: rendered };
|
|
478
|
-
return rendered;
|
|
383
|
+
return [header, ...matchLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
479
384
|
},
|
|
480
|
-
|
|
481
|
-
cached = undefined;
|
|
482
|
-
},
|
|
483
|
-
};
|
|
385
|
+
);
|
|
484
386
|
},
|
|
485
387
|
mergeCallAndResult: true,
|
|
486
388
|
};
|
|
@@ -12,10 +12,12 @@ import {
|
|
|
12
12
|
} from "@oh-my-pi/pi-tui";
|
|
13
13
|
import type { Terminal as XtermTerminalType } from "@xterm/headless";
|
|
14
14
|
import xterm from "@xterm/headless";
|
|
15
|
+
import { Settings } from "../config/settings";
|
|
15
16
|
import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
|
|
16
17
|
import type { Theme } from "../modes/theme/theme";
|
|
17
18
|
import { OutputSink, type OutputSummary } from "../session/streaming-output";
|
|
18
19
|
import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
|
|
20
|
+
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "./output-meta";
|
|
19
21
|
import { formatStatusIcon, replaceTabs } from "./render-utils";
|
|
20
22
|
|
|
21
23
|
export interface BashInteractiveResult extends OutputSummary {
|
|
@@ -294,7 +296,13 @@ export async function runInteractiveBashPty(
|
|
|
294
296
|
artifactId?: string;
|
|
295
297
|
},
|
|
296
298
|
): Promise<BashInteractiveResult> {
|
|
297
|
-
const
|
|
299
|
+
const settings = await Settings.init();
|
|
300
|
+
const sink = new OutputSink({
|
|
301
|
+
artifactPath: options.artifactPath,
|
|
302
|
+
artifactId: options.artifactId,
|
|
303
|
+
headBytes: resolveOutputSinkHeadBytes(settings),
|
|
304
|
+
maxColumns: resolveOutputMaxColumns(settings),
|
|
305
|
+
});
|
|
298
306
|
const result = await ui.custom<BashInteractiveResult>(
|
|
299
307
|
(tui, uiTheme, _keybindings, done) => {
|
|
300
308
|
const session = new PtySession();
|
package/src/tools/bash.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type BashResult, executeBash } from "../exec/bash-executor";
|
|
|
9
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
10
|
import { InternalUrlRouter } from "../internal-urls";
|
|
11
11
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
12
|
-
import type
|
|
12
|
+
import { highlightCode, type Theme } from "../modes/theme/theme";
|
|
13
13
|
import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
|
|
14
14
|
import type { ClientBridgeTerminalExitStatus, ClientBridgeTerminalOutput } from "../session/client-bridge";
|
|
15
15
|
import { DEFAULT_MAX_BYTES, streamTailUpdates, TailBuffer } from "../session/streaming-output";
|
|
@@ -484,8 +484,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
484
484
|
const env = normalizeBashEnv(rawEnv);
|
|
485
485
|
|
|
486
486
|
// Extract leading `cd <path> && ...` into cwd when the model ignores the cwd parameter.
|
|
487
|
+
// Constrained to a single line so a `&&` that sits on a later line of a multiline
|
|
488
|
+
// script can't pull the entire script into the "cwd" capture.
|
|
487
489
|
if (!cwd) {
|
|
488
|
-
const cdMatch = command.match(/^cd\
|
|
490
|
+
const cdMatch = command.match(/^cd[ \t]+((?:[^&\\\n\r]|\\.)+?)[ \t]*&&[ \t]*/);
|
|
489
491
|
if (cdMatch) {
|
|
490
492
|
cwd = cdMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
491
493
|
command = command.slice(cdMatch[0].length);
|
|
@@ -892,6 +894,27 @@ export function formatBashCommand(args: BashRenderArgs): string {
|
|
|
892
894
|
return displayWorkdir ? `${prompt} cd ${displayWorkdir} && ${renderedCommand}` : `${prompt} ${renderedCommand}`;
|
|
893
895
|
}
|
|
894
896
|
|
|
897
|
+
/**
|
|
898
|
+
* Returns the bash command formatted for the result body: the dim `$ cd … &&`
|
|
899
|
+
* prefix joined with syntax-highlighted command lines. The prefix is applied
|
|
900
|
+
* only to the first line so multi-line commands display cleanly — terminals
|
|
901
|
+
* reset SGR state at line boundaries, which made the previous single-string
|
|
902
|
+
* `theme.fg("dim", ...)` form render only the first line as dim.
|
|
903
|
+
*/
|
|
904
|
+
export function formatBashCommandLines(args: BashRenderArgs, uiTheme: Theme): string[] {
|
|
905
|
+
const command = replaceTabs(args.command || "…");
|
|
906
|
+
const cwd = getProjectDir();
|
|
907
|
+
const displayWorkdir = formatToolWorkingDirectory(args.cwd, cwd);
|
|
908
|
+
const envAssignments = formatBashEnvAssignments(getBashEnvForDisplay(args));
|
|
909
|
+
const prefixParts = ["$"];
|
|
910
|
+
if (displayWorkdir) prefixParts.push(`cd ${displayWorkdir} &&`);
|
|
911
|
+
if (envAssignments) prefixParts.push(envAssignments);
|
|
912
|
+
const prefix = uiTheme.fg("dim", `${prefixParts.join(" ")} `);
|
|
913
|
+
const highlightedLines = highlightCode(command, "bash");
|
|
914
|
+
if (highlightedLines.length === 0) return [prefix.trimEnd()];
|
|
915
|
+
return highlightedLines.map((line, i) => (i === 0 ? `${prefix}${line}` : line));
|
|
916
|
+
}
|
|
917
|
+
|
|
895
918
|
function toBashRenderArgs<TArgs>(args: TArgs | undefined, config: ShellRendererConfig<TArgs>): BashRenderArgs {
|
|
896
919
|
return {
|
|
897
920
|
command: config.resolveCommand?.(args),
|
|
@@ -922,7 +945,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
922
945
|
args?: TArgs,
|
|
923
946
|
): Component {
|
|
924
947
|
const renderArgs = toBashRenderArgs(args, config);
|
|
925
|
-
const
|
|
948
|
+
const cmdLines = args ? formatBashCommandLines(renderArgs, uiTheme) : undefined;
|
|
926
949
|
const isError = result.isError === true;
|
|
927
950
|
const icon = options.isPartial ? "pending" : isError ? "error" : "success";
|
|
928
951
|
const title = config.resolveTitle(args, options);
|
|
@@ -1000,7 +1023,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1000
1023
|
header,
|
|
1001
1024
|
state: options.isPartial ? "pending" : isError ? "error" : "success",
|
|
1002
1025
|
sections: [
|
|
1003
|
-
{ lines:
|
|
1026
|
+
{ lines: cmdLines ?? [] },
|
|
1004
1027
|
{ label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
|
|
1005
1028
|
],
|
|
1006
1029
|
width,
|
|
@@ -3,7 +3,7 @@ import { Process, ProcessStatus } from "@oh-my-pi/pi-natives";
|
|
|
3
3
|
import type { Browser, Page } from "puppeteer-core";
|
|
4
4
|
import { ToolError, throwIfAborted } from "../tool-errors";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const ATTACH_TARGET_SKIP_PATTERN =
|
|
7
7
|
/request[\s_-]?handler|devtools|background[\s_-]?(?:page|host)|service[\s_-]?worker/i;
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -62,7 +62,7 @@ export async function waitForCdp(cdpUrl: string, timeoutMs: number, signal?: Abo
|
|
|
62
62
|
* accepts both `--flag=value` and `--flag value`). Returns null if absent or
|
|
63
63
|
* malformed.
|
|
64
64
|
*/
|
|
65
|
-
|
|
65
|
+
function findCdpPortInArgs(args: string[]): number | null {
|
|
66
66
|
for (const arg of args) {
|
|
67
67
|
const m = /^--remote-debugging-port=(\d+)$/.exec(arg);
|
|
68
68
|
if (m) {
|
|
@@ -80,7 +80,7 @@ export function findCdpPortInArgs(args: string[]): number | null {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/** One-shot probe: returns true when `/json/version` answers 200 within the timeout. */
|
|
83
|
-
|
|
83
|
+
async function probeCdpAt(port: number, signal?: AbortSignal): Promise<boolean> {
|
|
84
84
|
const probeTimeout = AbortSignal.timeout(1500);
|
|
85
85
|
const probeSignal = signal ? AbortSignal.any([signal, probeTimeout]) : probeTimeout;
|
|
86
86
|
try {
|