@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/tools/output-meta.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
|
15
15
|
import { getDefault, type Settings } from "../config/settings";
|
|
16
16
|
import { formatGroupedDiagnosticMessages } from "../lsp/utils";
|
|
17
17
|
import type { Theme } from "../modes/theme/theme";
|
|
18
|
-
import { type OutputSummary, type TruncationResult, truncateTail } from "../session/streaming-output";
|
|
18
|
+
import { type OutputSummary, type TruncationResult, truncateMiddle, truncateTail } from "../session/streaming-output";
|
|
19
19
|
import { formatBytes, wrapBrackets } from "./render-utils";
|
|
20
20
|
import { renderError } from "./tool-errors";
|
|
21
21
|
|
|
@@ -23,15 +23,22 @@ import { renderError } from "./tool-errors";
|
|
|
23
23
|
* Truncation metadata for the output notice.
|
|
24
24
|
*/
|
|
25
25
|
export interface TruncationMeta {
|
|
26
|
-
direction: "head" | "tail";
|
|
27
|
-
truncatedBy: "lines" | "bytes";
|
|
26
|
+
direction: "head" | "tail" | "middle";
|
|
27
|
+
truncatedBy: "lines" | "bytes" | "middle";
|
|
28
28
|
totalLines: number;
|
|
29
29
|
totalBytes: number;
|
|
30
30
|
outputLines: number;
|
|
31
31
|
outputBytes: number;
|
|
32
32
|
maxBytes?: number;
|
|
33
|
-
/** Line range shown (1-indexed, inclusive) */
|
|
33
|
+
/** Line range shown (1-indexed, inclusive). Omitted for middle elision. */
|
|
34
34
|
shownRange?: { start: number; end: number };
|
|
35
|
+
/** Head/tail line ranges shown when direction === "middle". */
|
|
36
|
+
headRange?: { start: number; end: number };
|
|
37
|
+
tailRange?: { start: number; end: number };
|
|
38
|
+
/** Bytes elided from the middle. */
|
|
39
|
+
elidedBytes?: number;
|
|
40
|
+
/** Lines elided from the middle. */
|
|
41
|
+
elidedLines?: number;
|
|
35
42
|
/** Artifact ID if full output was saved */
|
|
36
43
|
artifactId?: string;
|
|
37
44
|
/** Next offset for pagination (head truncation only) */
|
|
@@ -79,20 +86,20 @@ export interface OutputMeta {
|
|
|
79
86
|
// =============================================================================
|
|
80
87
|
|
|
81
88
|
export interface TruncationOptions {
|
|
82
|
-
direction: "head" | "tail";
|
|
89
|
+
direction: "head" | "tail" | "middle";
|
|
83
90
|
startLine?: number;
|
|
84
91
|
totalFileLines?: number;
|
|
85
92
|
artifactId?: string;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
export interface TruncationSummaryOptions {
|
|
89
|
-
direction: "head" | "tail";
|
|
96
|
+
direction: "head" | "tail" | "middle";
|
|
90
97
|
startLine?: number;
|
|
91
98
|
totalFileLines?: number;
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
export interface TruncationTextOptions {
|
|
95
|
-
direction: "head" | "tail";
|
|
102
|
+
direction: "head" | "tail" | "middle";
|
|
96
103
|
totalLines?: number;
|
|
97
104
|
totalBytes?: number;
|
|
98
105
|
maxBytes?: number;
|
|
@@ -120,7 +127,40 @@ export class OutputMetaBuilder {
|
|
|
120
127
|
const { direction, startLine = 1, totalFileLines, artifactId } = options;
|
|
121
128
|
const outputLines = result.outputLines ?? result.totalLines;
|
|
122
129
|
const outputBytes = result.outputBytes ?? result.totalBytes;
|
|
123
|
-
const
|
|
130
|
+
const isMiddle = direction === "middle" || result.truncatedBy === "middle";
|
|
131
|
+
const truncatedBy: "lines" | "bytes" | "middle" = isMiddle
|
|
132
|
+
? "middle"
|
|
133
|
+
: result.truncatedBy === "lines"
|
|
134
|
+
? "lines"
|
|
135
|
+
: "bytes";
|
|
136
|
+
|
|
137
|
+
const effectiveTotalLines = totalFileLines ?? result.totalLines;
|
|
138
|
+
|
|
139
|
+
if (isMiddle) {
|
|
140
|
+
const elidedLines = result.elidedLines ?? Math.max(0, effectiveTotalLines - outputLines);
|
|
141
|
+
const elidedBytes = result.elidedBytes ?? Math.max(0, result.totalBytes - outputBytes);
|
|
142
|
+
// Reconstruct head/tail line ranges. The kept output spans the first
|
|
143
|
+
// `headLines` lines and the last `tailLines` lines of the source; lines
|
|
144
|
+
// in the middle (count == elidedLines) are dropped.
|
|
145
|
+
const keptLines = Math.max(0, outputLines - 1); // -1 for marker line
|
|
146
|
+
const headLines = Math.ceil(keptLines / 2);
|
|
147
|
+
const tailLines = keptLines - headLines;
|
|
148
|
+
this.#meta.truncation = {
|
|
149
|
+
direction: "middle",
|
|
150
|
+
truncatedBy: "middle",
|
|
151
|
+
totalLines: effectiveTotalLines,
|
|
152
|
+
totalBytes: result.totalBytes,
|
|
153
|
+
outputLines,
|
|
154
|
+
outputBytes,
|
|
155
|
+
headRange: headLines > 0 ? { start: 1, end: headLines } : undefined,
|
|
156
|
+
tailRange:
|
|
157
|
+
tailLines > 0 ? { start: effectiveTotalLines - tailLines + 1, end: effectiveTotalLines } : undefined,
|
|
158
|
+
elidedLines,
|
|
159
|
+
elidedBytes,
|
|
160
|
+
artifactId,
|
|
161
|
+
};
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
124
164
|
|
|
125
165
|
let shownStart: number;
|
|
126
166
|
let shownEnd: number;
|
|
@@ -136,7 +176,7 @@ export class OutputMetaBuilder {
|
|
|
136
176
|
this.#meta.truncation = {
|
|
137
177
|
direction,
|
|
138
178
|
truncatedBy,
|
|
139
|
-
totalLines:
|
|
179
|
+
totalLines: effectiveTotalLines,
|
|
140
180
|
totalBytes: result.totalBytes,
|
|
141
181
|
outputLines,
|
|
142
182
|
outputBytes,
|
|
@@ -154,6 +194,29 @@ export class OutputMetaBuilder {
|
|
|
154
194
|
|
|
155
195
|
const { direction, startLine = 1, totalFileLines } = options;
|
|
156
196
|
const totalLines = totalFileLines ?? summary.totalLines;
|
|
197
|
+
|
|
198
|
+
// Middle elision: the sink retained head + tail with an elision marker.
|
|
199
|
+
if (summary.elidedBytes != null && summary.elidedBytes > 0) {
|
|
200
|
+
const elidedLines = summary.elidedLines ?? Math.max(0, totalLines - summary.outputLines);
|
|
201
|
+
const keptLines = Math.max(0, summary.outputLines - 1); // -1 for marker line
|
|
202
|
+
const headLines = Math.ceil(keptLines / 2);
|
|
203
|
+
const tailLines = keptLines - headLines;
|
|
204
|
+
this.#meta.truncation = {
|
|
205
|
+
direction: "middle",
|
|
206
|
+
truncatedBy: "middle",
|
|
207
|
+
totalLines,
|
|
208
|
+
totalBytes: summary.totalBytes,
|
|
209
|
+
outputLines: summary.outputLines,
|
|
210
|
+
outputBytes: summary.outputBytes,
|
|
211
|
+
headRange: headLines > 0 ? { start: 1, end: headLines } : undefined,
|
|
212
|
+
tailRange: tailLines > 0 ? { start: totalLines - tailLines + 1, end: totalLines } : undefined,
|
|
213
|
+
elidedBytes: summary.elidedBytes,
|
|
214
|
+
elidedLines,
|
|
215
|
+
artifactId: summary.artifactId,
|
|
216
|
+
};
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
|
|
157
220
|
const truncatedBy: "lines" | "bytes" =
|
|
158
221
|
summary.outputBytes < summary.totalBytes
|
|
159
222
|
? "bytes"
|
|
@@ -322,9 +385,28 @@ export function formatFullOutputReference(artifactId: string): string {
|
|
|
322
385
|
}
|
|
323
386
|
|
|
324
387
|
export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
|
|
325
|
-
const range = truncation.shownRange;
|
|
326
388
|
let notice: string;
|
|
327
389
|
|
|
390
|
+
if (truncation.direction === "middle") {
|
|
391
|
+
const head = truncation.headRange;
|
|
392
|
+
const tail = truncation.tailRange;
|
|
393
|
+
const totalLines = truncation.totalLines;
|
|
394
|
+
const elidedBytes = truncation.elidedBytes ?? Math.max(0, truncation.totalBytes - truncation.outputBytes);
|
|
395
|
+
const elidedLines = truncation.elidedLines ?? Math.max(0, totalLines - truncation.outputLines);
|
|
396
|
+
const headPart = head ? `lines ${head.start}-${head.end}` : "";
|
|
397
|
+
const tailPart = tail ? `${tail.start}-${tail.end}` : "";
|
|
398
|
+
if (headPart && tailPart) {
|
|
399
|
+
notice = `Showing ${headPart} and ${tailPart} of ${totalLines}; ${elidedLines.toLocaleString()} middle line${elidedLines === 1 ? "" : "s"} (${formatBytes(elidedBytes)}) elided`;
|
|
400
|
+
} else {
|
|
401
|
+
notice = `Showing ${truncation.outputLines} of ${totalLines} lines; middle elided`;
|
|
402
|
+
}
|
|
403
|
+
if (truncation.artifactId != null) {
|
|
404
|
+
notice += `. ${formatFullOutputReference(truncation.artifactId)}`;
|
|
405
|
+
}
|
|
406
|
+
return notice;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const range = truncation.shownRange;
|
|
328
410
|
if (range && range.end >= range.start) {
|
|
329
411
|
notice = `Showing lines ${range.start}-${range.end} of ${truncation.totalLines}`;
|
|
330
412
|
} else {
|
|
@@ -407,6 +489,32 @@ export function formatStyledTruncationWarning(meta: OutputMeta | undefined, them
|
|
|
407
489
|
return theme.fg("warning", wrapBrackets(message, theme));
|
|
408
490
|
}
|
|
409
491
|
|
|
492
|
+
/**
|
|
493
|
+
* Strip the trailing notice that {@link appendOutputNotice} bakes into the
|
|
494
|
+
* LLM-facing content body. Renderers should call this before printing
|
|
495
|
+
* `result.content` text in the TUI, because they emit a styled warning line of
|
|
496
|
+
* their own; without this, users see the same `[Showing lines …]` string twice
|
|
497
|
+
* (once verbatim from the body, once as the styled `⟨…⟩` warning).
|
|
498
|
+
*
|
|
499
|
+
* Safe to call eagerly: returns the input unchanged when no notice is present
|
|
500
|
+
* (e.g. during streaming, before {@link wrappedExecute} runs).
|
|
501
|
+
*/
|
|
502
|
+
export function stripOutputNotice(text: string, meta: OutputMeta | undefined): string {
|
|
503
|
+
const notice = formatOutputNotice(meta);
|
|
504
|
+
if (!notice) return text;
|
|
505
|
+
// Trim trailing whitespace from `text` and from the notice itself so we
|
|
506
|
+
// match regardless of whether: (a) the caller already trimEnd()'d, (b)
|
|
507
|
+
// extra blank lines slipped in after the notice (diagnostics blocks add
|
|
508
|
+
// `\n\n` between sections, OutputSink may pad), or (c) neither. Returns
|
|
509
|
+
// the prefix before the notice so the caller can re-trim as needed.
|
|
510
|
+
const trimmedText = text.trimEnd();
|
|
511
|
+
const trimmedNotice = notice.trimEnd();
|
|
512
|
+
if (trimmedText.endsWith(trimmedNotice)) {
|
|
513
|
+
return trimmedText.slice(0, -trimmedNotice.length);
|
|
514
|
+
}
|
|
515
|
+
return text;
|
|
516
|
+
}
|
|
517
|
+
|
|
410
518
|
// =============================================================================
|
|
411
519
|
// Tool wrapper
|
|
412
520
|
// =============================================================================
|
|
@@ -442,21 +550,44 @@ const kUnwrappedExecute = Symbol("OutputMeta.UnwrappedExecute");
|
|
|
442
550
|
|
|
443
551
|
/** Resolved artifact spill config sourced from the session settings (or schema defaults). */
|
|
444
552
|
function getSpillConfig(s: Settings | undefined) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
553
|
+
type Path =
|
|
554
|
+
| "tools.artifactSpillThreshold"
|
|
555
|
+
| "tools.artifactTailBytes"
|
|
556
|
+
| "tools.artifactTailLines"
|
|
557
|
+
| "tools.artifactHeadBytes";
|
|
558
|
+
const get = <P extends Path>(path: P) => s?.get(path) ?? getDefault(path);
|
|
448
559
|
return {
|
|
449
560
|
threshold: get("tools.artifactSpillThreshold") * 1024,
|
|
450
561
|
tailBytes: get("tools.artifactTailBytes") * 1024,
|
|
451
562
|
tailLines: get("tools.artifactTailLines"),
|
|
563
|
+
headBytes: get("tools.artifactHeadBytes") * 1024,
|
|
452
564
|
};
|
|
453
565
|
}
|
|
454
566
|
|
|
455
567
|
/**
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
|
|
568
|
+
* Resolve the OutputSink `headBytes` budget from session settings.
|
|
569
|
+
* Exposed so streaming executors (bash/python/ssh/eval) can opt into
|
|
570
|
+
* middle elision with the same per-user configuration.
|
|
571
|
+
*/
|
|
572
|
+
export function resolveOutputSinkHeadBytes(s: Settings | undefined): number {
|
|
573
|
+
return getSpillConfig(s).headBytes;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Resolve the per-line column cap from session settings. Shared by streaming
|
|
578
|
+
* executors (bash/python/ssh/eval via OutputSink) and the `read` tool's
|
|
579
|
+
* line-buffer post-processing, so one setting controls both surfaces.
|
|
580
|
+
*/
|
|
581
|
+
export function resolveOutputMaxColumns(s: Settings | undefined): number {
|
|
582
|
+
return s?.get("tools.outputMaxColumns") ?? getDefault("tools.outputMaxColumns");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* If the tool result text exceeds the spill threshold, save the full output
|
|
587
|
+
* as a session artifact and replace the content with a head+tail (middle
|
|
588
|
+
* elision) view plus an artifact reference. When `tools.artifactHeadBytes`
|
|
589
|
+
* is 0, falls back to tail-only truncation. Skips when the tool already
|
|
590
|
+
* saved its own artifact (e.g. bash/python via OutputSink).
|
|
460
591
|
*/
|
|
461
592
|
async function spillLargeResultToArtifact(
|
|
462
593
|
result: AgentToolResult,
|
|
@@ -466,7 +597,7 @@ async function spillLargeResultToArtifact(
|
|
|
466
597
|
const sessionManager = context?.sessionManager;
|
|
467
598
|
if (!sessionManager) return result;
|
|
468
599
|
if (toolName === "read") return result;
|
|
469
|
-
const { threshold, tailBytes, tailLines } = getSpillConfig(context?.settings);
|
|
600
|
+
const { threshold, tailBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
|
|
470
601
|
|
|
471
602
|
// Skip if tool already saved an artifact
|
|
472
603
|
const existingMeta: OutputMeta | undefined = result.details?.meta;
|
|
@@ -489,13 +620,21 @@ async function spillLargeResultToArtifact(
|
|
|
489
620
|
const artifactId = await sessionManager.saveArtifact(fullText, toolName);
|
|
490
621
|
if (!artifactId) return result;
|
|
491
622
|
|
|
492
|
-
// Truncate
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
623
|
+
// Truncate: middle elision when a head budget is configured, otherwise tail-only.
|
|
624
|
+
const useMiddle = headBytes > 0;
|
|
625
|
+
const truncated = useMiddle
|
|
626
|
+
? truncateMiddle(fullText, {
|
|
627
|
+
maxBytes: headBytes + tailBytes,
|
|
628
|
+
maxLines: tailLines * 2,
|
|
629
|
+
maxHeadBytes: headBytes,
|
|
630
|
+
maxHeadLines: tailLines,
|
|
631
|
+
})
|
|
632
|
+
: truncateTail(fullText, {
|
|
633
|
+
maxBytes: tailBytes,
|
|
634
|
+
maxLines: tailLines,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Replace text blocks with single truncated block, keep images
|
|
499
638
|
const newContent: (TextContent | ImageContent)[] = [];
|
|
500
639
|
for (const block of result.content) {
|
|
501
640
|
if (block.type !== "text") {
|
|
@@ -507,18 +646,44 @@ async function spillLargeResultToArtifact(
|
|
|
507
646
|
// Build truncation meta
|
|
508
647
|
const outputLines = truncated.outputLines ?? truncated.totalLines;
|
|
509
648
|
const outputBytes = truncated.outputBytes ?? truncated.totalBytes;
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
649
|
+
let truncationMeta: TruncationMeta;
|
|
650
|
+
if (truncated.truncatedBy === "middle") {
|
|
651
|
+
const elidedLines = truncated.elidedLines ?? Math.max(0, truncated.totalLines - outputLines);
|
|
652
|
+
const elidedBytes = truncated.elidedBytes ?? Math.max(0, truncated.totalBytes - outputBytes);
|
|
653
|
+
const keptLines = Math.max(0, outputLines - 1); // -1 for marker line
|
|
654
|
+
const headLines = Math.ceil(keptLines / 2);
|
|
655
|
+
const tailLineCount = keptLines - headLines;
|
|
656
|
+
truncationMeta = {
|
|
657
|
+
direction: "middle",
|
|
658
|
+
truncatedBy: "middle",
|
|
659
|
+
totalLines: truncated.totalLines,
|
|
660
|
+
totalBytes: truncated.totalBytes,
|
|
661
|
+
outputLines,
|
|
662
|
+
outputBytes,
|
|
663
|
+
maxBytes: headBytes + tailBytes,
|
|
664
|
+
headRange: headLines > 0 ? { start: 1, end: headLines } : undefined,
|
|
665
|
+
tailRange:
|
|
666
|
+
tailLineCount > 0
|
|
667
|
+
? { start: truncated.totalLines - tailLineCount + 1, end: truncated.totalLines }
|
|
668
|
+
: undefined,
|
|
669
|
+
elidedLines,
|
|
670
|
+
elidedBytes,
|
|
671
|
+
artifactId,
|
|
672
|
+
};
|
|
673
|
+
} else {
|
|
674
|
+
const shownStart = truncated.totalLines - outputLines + 1;
|
|
675
|
+
truncationMeta = {
|
|
676
|
+
direction: "tail",
|
|
677
|
+
truncatedBy: truncated.truncatedBy ?? "bytes",
|
|
678
|
+
totalLines: truncated.totalLines,
|
|
679
|
+
totalBytes: truncated.totalBytes,
|
|
680
|
+
outputLines,
|
|
681
|
+
outputBytes,
|
|
682
|
+
maxBytes: tailBytes,
|
|
683
|
+
shownRange: { start: shownStart, end: truncated.totalLines },
|
|
684
|
+
artifactId,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
522
687
|
|
|
523
688
|
const newMeta: OutputMeta = { ...(existingMeta ?? {}), truncation: truncationMeta };
|
|
524
689
|
const newDetails = { ...(result.details ?? {}), meta: newMeta };
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -3,10 +3,12 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as url from "node:url";
|
|
5
5
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { InternalUrlRouter } from "../internal-urls";
|
|
7
|
+
import { ToolError } from "./tool-errors";
|
|
6
8
|
|
|
7
9
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
8
|
-
const FILE_LINE_RANGE_RE = /^(?:L?\d+(?:[-+]L?\d+)
|
|
9
|
-
const FILE_LINE_RANGE_ONLY_RE = /^L?\d+(?:[-+]L?\d+)
|
|
10
|
+
const FILE_LINE_RANGE_RE = /^(?:L?\d+(?:[-+]L?\d+)?(?:,L?\d+(?:[-+]L?\d+)?)*|raw|conflicts)$/i;
|
|
11
|
+
const FILE_LINE_RANGE_ONLY_RE = /^L?\d+(?:[-+]L?\d+)?(?:,L?\d+(?:[-+]L?\d+)?)*$/i;
|
|
10
12
|
const FILE_RAW_ONLY_RE = /^raw$/i;
|
|
11
13
|
const NARROW_NO_BREAK_SPACE = "\u202F";
|
|
12
14
|
const TOP_LEVEL_INTERNAL_URL_PREFIXES = [
|
|
@@ -567,3 +569,124 @@ export function resolveReadPath(filePath: string, cwd: string): string {
|
|
|
567
569
|
|
|
568
570
|
return resolved;
|
|
569
571
|
}
|
|
572
|
+
|
|
573
|
+
// =============================================================================
|
|
574
|
+
// Tool-scope resolution (search/ast tools)
|
|
575
|
+
// =============================================================================
|
|
576
|
+
|
|
577
|
+
export interface ToolScopeOptions {
|
|
578
|
+
rawPaths: string[];
|
|
579
|
+
cwd: string;
|
|
580
|
+
/** Verb used in the "Cannot {action} internal URL without a backing file: …" message. */
|
|
581
|
+
internalUrlAction: string;
|
|
582
|
+
/** Collect absolute paths flagged immutable by their internal-URL handler. */
|
|
583
|
+
trackImmutableSources?: boolean;
|
|
584
|
+
/** Honor `exactFilePaths` from {@link resolveExplicitSearchPaths} (search-only). */
|
|
585
|
+
surfaceExactFilePaths?: boolean;
|
|
586
|
+
/** Extra hint appended to "Path not found" when stat fails and the user supplied multiple paths. */
|
|
587
|
+
multipathStatHint?: string;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export interface ToolScopeResolution {
|
|
591
|
+
searchPath: string;
|
|
592
|
+
scopePath: string;
|
|
593
|
+
globFilter: string | undefined;
|
|
594
|
+
isDirectory: boolean;
|
|
595
|
+
multiTargets?: ResolvedSearchTarget[];
|
|
596
|
+
exactFilePaths?: string[];
|
|
597
|
+
missingPaths: string[];
|
|
598
|
+
immutableSourcePaths: Set<string>;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Shared path-input pipeline for `search`, `ast_grep`, and `ast_edit`:
|
|
603
|
+
* 1. normalize + reject empty paths,
|
|
604
|
+
* 2. resolve internal URLs through {@link InternalUrlRouter} to backing files,
|
|
605
|
+
* 3. partition existing vs missing when multiple paths are supplied,
|
|
606
|
+
* 4. derive a single search base path / glob, or a multi-target list,
|
|
607
|
+
* 5. stat the resolved base path so callers can branch on directory vs file scope.
|
|
608
|
+
*/
|
|
609
|
+
export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<ToolScopeResolution> {
|
|
610
|
+
const { rawPaths: inputs, cwd, internalUrlAction } = opts;
|
|
611
|
+
const rawPaths = inputs.map(normalizePathLikeInput);
|
|
612
|
+
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
613
|
+
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
614
|
+
}
|
|
615
|
+
const internalRouter = InternalUrlRouter.instance();
|
|
616
|
+
const resolvedPathInputs: string[] = [];
|
|
617
|
+
const immutableSourcePaths = new Set<string>();
|
|
618
|
+
for (const rawPath of rawPaths) {
|
|
619
|
+
if (!internalRouter.canHandle(rawPath)) {
|
|
620
|
+
resolvedPathInputs.push(rawPath);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (hasGlobPathChars(rawPath)) {
|
|
624
|
+
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
625
|
+
}
|
|
626
|
+
const resource = await internalRouter.resolve(rawPath);
|
|
627
|
+
if (!resource.sourcePath) {
|
|
628
|
+
throw new ToolError(`Cannot ${internalUrlAction} internal URL without a backing file: ${rawPath}`);
|
|
629
|
+
}
|
|
630
|
+
if (opts.trackImmutableSources && resource.immutable) {
|
|
631
|
+
immutableSourcePaths.add(path.resolve(resource.sourcePath));
|
|
632
|
+
}
|
|
633
|
+
resolvedPathInputs.push(resource.sourcePath);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
let missingPaths: string[] = [];
|
|
637
|
+
let effectivePaths = resolvedPathInputs;
|
|
638
|
+
if (resolvedPathInputs.length > 1) {
|
|
639
|
+
const partition = await partitionExistingPaths(resolvedPathInputs, cwd, parseSearchPath);
|
|
640
|
+
if (partition.valid.length === 0) {
|
|
641
|
+
throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
|
|
642
|
+
}
|
|
643
|
+
effectivePaths = partition.valid;
|
|
644
|
+
missingPaths = partition.missing;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
let searchPath: string;
|
|
648
|
+
let scopePath: string;
|
|
649
|
+
let globFilter: string | undefined;
|
|
650
|
+
let multiTargets: ResolvedSearchTarget[] | undefined;
|
|
651
|
+
let exactFilePaths: string[] | undefined;
|
|
652
|
+
if (effectivePaths.length === 1) {
|
|
653
|
+
const parsedPath = parseSearchPath(effectivePaths[0] ?? ".");
|
|
654
|
+
searchPath = resolveToCwd(parsedPath.basePath, cwd);
|
|
655
|
+
globFilter = parsedPath.glob;
|
|
656
|
+
scopePath = formatPathRelativeToCwd(searchPath, cwd);
|
|
657
|
+
} else {
|
|
658
|
+
const multiSearchPath = await resolveExplicitSearchPaths(effectivePaths, cwd);
|
|
659
|
+
if (!multiSearchPath) {
|
|
660
|
+
throw new ToolError("`paths` must contain at least one path or glob");
|
|
661
|
+
}
|
|
662
|
+
searchPath = multiSearchPath.basePath;
|
|
663
|
+
multiTargets = multiSearchPath.targets;
|
|
664
|
+
if (opts.surfaceExactFilePaths) {
|
|
665
|
+
exactFilePaths = multiSearchPath.exactFilePaths;
|
|
666
|
+
globFilter = exactFilePaths || multiTargets ? undefined : multiSearchPath.glob;
|
|
667
|
+
} else {
|
|
668
|
+
globFilter = multiTargets ? undefined : multiSearchPath.glob;
|
|
669
|
+
}
|
|
670
|
+
scopePath = multiSearchPath.scopePath;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
let isDirectory: boolean;
|
|
674
|
+
try {
|
|
675
|
+
const stat = await Bun.file(searchPath).stat();
|
|
676
|
+
isDirectory = stat.isDirectory();
|
|
677
|
+
} catch {
|
|
678
|
+
const hint = opts.multipathStatHint && rawPaths.length > 1 ? opts.multipathStatHint : "";
|
|
679
|
+
throw new ToolError(`Path not found: ${scopePath}${hint}`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
searchPath,
|
|
684
|
+
scopePath,
|
|
685
|
+
globFilter,
|
|
686
|
+
isDirectory,
|
|
687
|
+
multiTargets,
|
|
688
|
+
exactFilePaths,
|
|
689
|
+
missingPaths,
|
|
690
|
+
immutableSourcePaths,
|
|
691
|
+
};
|
|
692
|
+
}
|