@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.2
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 +79 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +10 -10
- 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/commands/commit.ts +10 -0
- 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 +44 -3
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +82 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/discovery/claude-plugins.ts +19 -7
- 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/eval/py/runner.py +42 -11
- package/src/eval/py/runtime.ts +1 -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/get-commands-handler.ts +77 -0
- 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/plugins/legacy-pi-compat.ts +48 -31
- 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/hashline/input.ts +2 -1
- package/src/hashline/parser.ts +27 -3
- 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 +11 -12
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +8 -0
- package/src/internal-urls/types.ts +21 -0
- package/src/lsp/config.ts +15 -6
- package/src/lsp/defaults.json +6 -2
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +327 -95
- 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 +93 -8
- 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/extension-ui-controller.ts +3 -2
- 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/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-mode.ts +41 -88
- package/src/modes/rpc/rpc-types.ts +57 -0
- package/src/modes/runtime-init.ts +116 -0
- package/src/modes/theme/defaults/dark-poimandres.json +3 -0
- package/src/modes/theme/defaults/light-poimandres.json +3 -0
- package/src/modes/theme/theme.ts +24 -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/github.md +4 -4
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +101 -117
- package/src/prompts/tools/read.md +55 -36
- 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/discovery.ts +5 -2
- package/src/task/executor.ts +19 -8
- 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-command-fixup.ts +47 -0
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +66 -19
- 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/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +12 -2
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +44 -10
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/job.ts +16 -7
- package/src/tools/output-meta.ts +202 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +548 -237
- 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/ssh.ts +3 -2
- package/src/tools/write.ts +64 -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/anthropic.ts +5 -0
- package/src/web/search/providers/exa.ts +3 -0
- package/src/web/search/providers/gemini.ts +40 -95
- package/src/web/search/providers/jina.ts +5 -2
- package/src/web/search/providers/zai.ts +5 -2
- 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/task/render.ts
CHANGED
|
@@ -50,6 +50,24 @@ function getStatusIcon(status: AgentProgress["status"], theme: Theme, spinnerFra
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** Append tool-count, token, and cost stats to a status line string. */
|
|
54
|
+
function appendAgentStats(
|
|
55
|
+
line: string,
|
|
56
|
+
opts: { toolCount?: number; tokens: number; cost: number },
|
|
57
|
+
theme: Theme,
|
|
58
|
+
): string {
|
|
59
|
+
if (opts.toolCount) {
|
|
60
|
+
line += `${theme.sep.dot}${theme.fg("dim", `${opts.toolCount} tools`)}`;
|
|
61
|
+
}
|
|
62
|
+
if (opts.tokens > 0) {
|
|
63
|
+
line += `${theme.sep.dot}${theme.fg("dim", `${formatNumber(opts.tokens)} tokens`)}`;
|
|
64
|
+
}
|
|
65
|
+
if (opts.cost > 0) {
|
|
66
|
+
line += `${theme.sep.dot}${theme.fg("statusLineCost", `$${opts.cost.toFixed(2)}`)}`;
|
|
67
|
+
}
|
|
68
|
+
return line;
|
|
69
|
+
}
|
|
70
|
+
|
|
53
71
|
function formatFindingSummary(findings: ReportFindingDetails[], theme: Theme): string {
|
|
54
72
|
if (findings.length === 0) return theme.fg("dim", "Findings: none");
|
|
55
73
|
|
|
@@ -526,19 +544,9 @@ function renderAgentProgress(
|
|
|
526
544
|
const taskPreview = truncateToWidth(progress.assignment ?? progress.task, 40);
|
|
527
545
|
statusLine += ` ${theme.fg("muted", taskPreview)}`;
|
|
528
546
|
}
|
|
529
|
-
|
|
530
|
-
statusLine += `${theme.sep.dot}${theme.fg("dim", `${progress.toolCount} tools`)}`;
|
|
531
|
-
}
|
|
532
|
-
if (progress.tokens > 0) {
|
|
533
|
-
statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatNumber(progress.tokens)} tokens`)}`;
|
|
534
|
-
}
|
|
547
|
+
statusLine = appendAgentStats(statusLine, progress, theme);
|
|
535
548
|
} else if (progress.status === "completed") {
|
|
536
|
-
|
|
537
|
-
statusLine += `${theme.sep.dot}${theme.fg("dim", `${progress.toolCount} tools`)}`;
|
|
538
|
-
}
|
|
539
|
-
if (progress.tokens > 0) {
|
|
540
|
-
statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatNumber(progress.tokens)} tokens`)}`;
|
|
541
|
-
}
|
|
549
|
+
statusLine = appendAgentStats(statusLine, progress, theme);
|
|
542
550
|
}
|
|
543
551
|
|
|
544
552
|
lines.push(statusLine);
|
|
@@ -768,9 +776,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
768
776
|
iconColor,
|
|
769
777
|
theme,
|
|
770
778
|
)}`;
|
|
771
|
-
|
|
772
|
-
statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatNumber(result.tokens)} tokens`)}`;
|
|
773
|
-
}
|
|
779
|
+
statusLine = appendAgentStats(statusLine, { tokens: result.tokens, cost: result.usage?.cost.total ?? 0 }, theme);
|
|
774
780
|
statusLine += `${theme.sep.dot}${theme.fg("dim", formatDuration(result.durationMs))}`;
|
|
775
781
|
|
|
776
782
|
if (result.truncated) {
|
package/src/task/types.ts
CHANGED
|
@@ -217,7 +217,10 @@ export interface AgentProgress {
|
|
|
217
217
|
recentTools: Array<{ tool: string; args: string; endMs: number }>;
|
|
218
218
|
recentOutput: string[];
|
|
219
219
|
toolCount: number;
|
|
220
|
+
/** Cumulative input + output + cacheWrite tokens across all turns. Excludes cacheRead (re-reads cached context every turn, making cumulative sum misleading). */
|
|
220
221
|
tokens: number;
|
|
222
|
+
/** Cumulative billing cost in USD, accumulated incrementally from message_end events. */
|
|
223
|
+
cost: number;
|
|
221
224
|
durationMs: number;
|
|
222
225
|
modelOverride?: string | string[];
|
|
223
226
|
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|
|
@@ -239,6 +242,7 @@ export interface SingleResult {
|
|
|
239
242
|
stderr: string;
|
|
240
243
|
truncated: boolean;
|
|
241
244
|
durationMs: number;
|
|
245
|
+
/** Cumulative input + output + cacheWrite tokens across all turns. Excludes cacheRead (re-reads cached context every turn, making cumulative sum misleading). */
|
|
242
246
|
tokens: number;
|
|
243
247
|
modelOverride?: string | string[];
|
|
244
248
|
error?: string;
|
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
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative transforms applied to a bash command before execution.
|
|
3
|
+
*
|
|
4
|
+
* Two fixups are applied, each anchored to the end of a top-level segment
|
|
5
|
+
* (segments split on `;`, `&&`, `||`, and background `&`):
|
|
6
|
+
*
|
|
7
|
+
* 1. Trailing `| head [args]` / `| tail [args]` (and the `|&` variant) — these
|
|
8
|
+
* pipes exist purely to limit output length. The harness already truncates
|
|
9
|
+
* bash output and exposes the full result via an artifact, so they only
|
|
10
|
+
* hide content the agent wanted.
|
|
11
|
+
*
|
|
12
|
+
* 2. A redundant trailing `2>&1` left on a segment that has no remaining pipe
|
|
13
|
+
* or other redirect. The harness already merges stderr into stdout, so the
|
|
14
|
+
* duplication is purely cosmetic — and often a leftover after fixup (1)
|
|
15
|
+
* drops a downstream pipe.
|
|
16
|
+
*
|
|
17
|
+
* The heavy lifting (tokenization, quoting, heredoc handling, command
|
|
18
|
+
* substitution, nested compound commands) lives in Rust under
|
|
19
|
+
* `pi_shell::fixup`, driven by the real `brush-parser` AST. This module is a
|
|
20
|
+
* thin sync wrapper plus user-facing notice formatting.
|
|
21
|
+
*/
|
|
22
|
+
import { applyBashFixups as nativeApplyBashFixups } from "@oh-my-pi/pi-natives";
|
|
23
|
+
|
|
24
|
+
export interface BashFixupResult {
|
|
25
|
+
/** Possibly-rewritten command. */
|
|
26
|
+
command: string;
|
|
27
|
+
/** Substrings that were stripped, in the order they were removed. */
|
|
28
|
+
stripped: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Apply both fixups to a bash command. On any parse failure, multi-line input,
|
|
33
|
+
* or no-op transform, returns the input verbatim with `stripped: []`.
|
|
34
|
+
*/
|
|
35
|
+
export function applyBashFixups(command: string): BashFixupResult {
|
|
36
|
+
return nativeApplyBashFixups(command);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Human-readable notice for the fixups that fired. Mirrors the shape of
|
|
41
|
+
* `formatTimeoutClampNotice` so it can ride alongside the other bash notices.
|
|
42
|
+
*/
|
|
43
|
+
export function formatBashFixupNotice(stripped: readonly string[]): string | undefined {
|
|
44
|
+
if (!stripped.length) return undefined;
|
|
45
|
+
const quoted = stripped.map(s => `\`${s}\``).join(", ");
|
|
46
|
+
return `<system-warning>Stripped redundant ${quoted} — bash output is already truncated and stderr is already merged into stdout. NEVER use these patterns.</system-warning>`;
|
|
47
|
+
}
|
|
@@ -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();
|