@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/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 {
|
|
@@ -442,21 +524,44 @@ const kUnwrappedExecute = Symbol("OutputMeta.UnwrappedExecute");
|
|
|
442
524
|
|
|
443
525
|
/** Resolved artifact spill config sourced from the session settings (or schema defaults). */
|
|
444
526
|
function getSpillConfig(s: Settings | undefined) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
527
|
+
type Path =
|
|
528
|
+
| "tools.artifactSpillThreshold"
|
|
529
|
+
| "tools.artifactTailBytes"
|
|
530
|
+
| "tools.artifactTailLines"
|
|
531
|
+
| "tools.artifactHeadBytes";
|
|
532
|
+
const get = <P extends Path>(path: P) => s?.get(path) ?? getDefault(path);
|
|
448
533
|
return {
|
|
449
534
|
threshold: get("tools.artifactSpillThreshold") * 1024,
|
|
450
535
|
tailBytes: get("tools.artifactTailBytes") * 1024,
|
|
451
536
|
tailLines: get("tools.artifactTailLines"),
|
|
537
|
+
headBytes: get("tools.artifactHeadBytes") * 1024,
|
|
452
538
|
};
|
|
453
539
|
}
|
|
454
540
|
|
|
455
541
|
/**
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
|
|
542
|
+
* Resolve the OutputSink `headBytes` budget from session settings.
|
|
543
|
+
* Exposed so streaming executors (bash/python/ssh/eval) can opt into
|
|
544
|
+
* middle elision with the same per-user configuration.
|
|
545
|
+
*/
|
|
546
|
+
export function resolveOutputSinkHeadBytes(s: Settings | undefined): number {
|
|
547
|
+
return getSpillConfig(s).headBytes;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Resolve the per-line column cap from session settings. Shared by streaming
|
|
552
|
+
* executors (bash/python/ssh/eval via OutputSink) and the `read` tool's
|
|
553
|
+
* line-buffer post-processing, so one setting controls both surfaces.
|
|
554
|
+
*/
|
|
555
|
+
export function resolveOutputMaxColumns(s: Settings | undefined): number {
|
|
556
|
+
return s?.get("tools.outputMaxColumns") ?? getDefault("tools.outputMaxColumns");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* If the tool result text exceeds the spill threshold, save the full output
|
|
561
|
+
* as a session artifact and replace the content with a head+tail (middle
|
|
562
|
+
* elision) view plus an artifact reference. When `tools.artifactHeadBytes`
|
|
563
|
+
* is 0, falls back to tail-only truncation. Skips when the tool already
|
|
564
|
+
* saved its own artifact (e.g. bash/python via OutputSink).
|
|
460
565
|
*/
|
|
461
566
|
async function spillLargeResultToArtifact(
|
|
462
567
|
result: AgentToolResult,
|
|
@@ -466,7 +571,7 @@ async function spillLargeResultToArtifact(
|
|
|
466
571
|
const sessionManager = context?.sessionManager;
|
|
467
572
|
if (!sessionManager) return result;
|
|
468
573
|
if (toolName === "read") return result;
|
|
469
|
-
const { threshold, tailBytes, tailLines } = getSpillConfig(context?.settings);
|
|
574
|
+
const { threshold, tailBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
|
|
470
575
|
|
|
471
576
|
// Skip if tool already saved an artifact
|
|
472
577
|
const existingMeta: OutputMeta | undefined = result.details?.meta;
|
|
@@ -489,13 +594,21 @@ async function spillLargeResultToArtifact(
|
|
|
489
594
|
const artifactId = await sessionManager.saveArtifact(fullText, toolName);
|
|
490
595
|
if (!artifactId) return result;
|
|
491
596
|
|
|
492
|
-
// Truncate
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
597
|
+
// Truncate: middle elision when a head budget is configured, otherwise tail-only.
|
|
598
|
+
const useMiddle = headBytes > 0;
|
|
599
|
+
const truncated = useMiddle
|
|
600
|
+
? truncateMiddle(fullText, {
|
|
601
|
+
maxBytes: headBytes + tailBytes,
|
|
602
|
+
maxLines: tailLines * 2,
|
|
603
|
+
maxHeadBytes: headBytes,
|
|
604
|
+
maxHeadLines: tailLines,
|
|
605
|
+
})
|
|
606
|
+
: truncateTail(fullText, {
|
|
607
|
+
maxBytes: tailBytes,
|
|
608
|
+
maxLines: tailLines,
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Replace text blocks with single truncated block, keep images
|
|
499
612
|
const newContent: (TextContent | ImageContent)[] = [];
|
|
500
613
|
for (const block of result.content) {
|
|
501
614
|
if (block.type !== "text") {
|
|
@@ -507,18 +620,44 @@ async function spillLargeResultToArtifact(
|
|
|
507
620
|
// Build truncation meta
|
|
508
621
|
const outputLines = truncated.outputLines ?? truncated.totalLines;
|
|
509
622
|
const outputBytes = truncated.outputBytes ?? truncated.totalBytes;
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
623
|
+
let truncationMeta: TruncationMeta;
|
|
624
|
+
if (truncated.truncatedBy === "middle") {
|
|
625
|
+
const elidedLines = truncated.elidedLines ?? Math.max(0, truncated.totalLines - outputLines);
|
|
626
|
+
const elidedBytes = truncated.elidedBytes ?? Math.max(0, truncated.totalBytes - outputBytes);
|
|
627
|
+
const keptLines = Math.max(0, outputLines - 1); // -1 for marker line
|
|
628
|
+
const headLines = Math.ceil(keptLines / 2);
|
|
629
|
+
const tailLineCount = keptLines - headLines;
|
|
630
|
+
truncationMeta = {
|
|
631
|
+
direction: "middle",
|
|
632
|
+
truncatedBy: "middle",
|
|
633
|
+
totalLines: truncated.totalLines,
|
|
634
|
+
totalBytes: truncated.totalBytes,
|
|
635
|
+
outputLines,
|
|
636
|
+
outputBytes,
|
|
637
|
+
maxBytes: headBytes + tailBytes,
|
|
638
|
+
headRange: headLines > 0 ? { start: 1, end: headLines } : undefined,
|
|
639
|
+
tailRange:
|
|
640
|
+
tailLineCount > 0
|
|
641
|
+
? { start: truncated.totalLines - tailLineCount + 1, end: truncated.totalLines }
|
|
642
|
+
: undefined,
|
|
643
|
+
elidedLines,
|
|
644
|
+
elidedBytes,
|
|
645
|
+
artifactId,
|
|
646
|
+
};
|
|
647
|
+
} else {
|
|
648
|
+
const shownStart = truncated.totalLines - outputLines + 1;
|
|
649
|
+
truncationMeta = {
|
|
650
|
+
direction: "tail",
|
|
651
|
+
truncatedBy: truncated.truncatedBy ?? "bytes",
|
|
652
|
+
totalLines: truncated.totalLines,
|
|
653
|
+
totalBytes: truncated.totalBytes,
|
|
654
|
+
outputLines,
|
|
655
|
+
outputBytes,
|
|
656
|
+
maxBytes: tailBytes,
|
|
657
|
+
shownRange: { start: shownStart, end: truncated.totalLines },
|
|
658
|
+
artifactId,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
522
661
|
|
|
523
662
|
const newMeta: OutputMeta = { ...(existingMeta ?? {}), truncation: truncationMeta };
|
|
524
663
|
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
|
+
}
|