@oh-my-pi/pi-coding-agent 15.1.9 → 15.2.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 +30 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/eval/py/kernel.d.ts +6 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/goals/tools/goal-tool.d.ts +4 -0
- package/dist/types/hashline/parser.d.ts +6 -2
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -0
- package/dist/types/modes/theme/shimmer.d.ts +27 -0
- package/dist/types/slash-commands/helpers/format.d.ts +4 -1
- package/dist/types/tools/ast-edit.d.ts +3 -0
- package/dist/types/tools/ast-grep.d.ts +3 -0
- package/dist/types/tools/find.d.ts +3 -0
- package/dist/types/tools/search.d.ts +3 -0
- package/dist/types/tui/file-list.d.ts +6 -0
- package/dist/types/tui/hyperlink.d.ts +42 -0
- package/dist/types/tui/index.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +2 -1
- package/package.json +7 -7
- package/src/config/settings-schema.ts +12 -0
- package/src/config/settings.ts +28 -5
- package/src/edit/renderer.ts +5 -3
- package/src/eval/py/executor.ts +12 -1
- package/src/eval/py/kernel.ts +24 -8
- package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
- package/src/goals/runtime.ts +9 -3
- package/src/goals/state.ts +1 -1
- package/src/goals/tools/goal-tool.ts +12 -2
- package/src/hashline/diff.ts +1 -1
- package/src/hashline/execute.ts +2 -2
- package/src/hashline/parser.ts +87 -12
- package/src/internal-urls/memory-protocol.ts +1 -1
- package/src/modes/interactive-mode.ts +29 -1
- package/src/modes/theme/shimmer.ts +79 -0
- package/src/prompts/tools/goal.md +7 -2
- package/src/session/agent-session.ts +12 -75
- package/src/slash-commands/helpers/format.ts +23 -3
- package/src/task/executor.ts +115 -19
- package/src/tools/ast-edit.ts +39 -6
- package/src/tools/ast-grep.ts +38 -6
- package/src/tools/find.ts +13 -2
- package/src/tools/read.ts +46 -6
- package/src/tools/search.ts +447 -265
- package/src/tui/file-list.ts +10 -2
- package/src/tui/hyperlink.ts +126 -0
- package/src/tui/index.ts +1 -0
- package/src/web/search/index.ts +13 -9
- package/src/web/search/providers/anthropic.ts +3 -1
- package/src/web/search/providers/brave.ts +3 -1
- package/src/web/search/providers/codex.ts +3 -1
- package/src/web/search/providers/exa.ts +3 -1
- package/src/web/search/providers/gemini.ts +3 -1
- package/src/web/search/providers/jina.ts +3 -1
- package/src/web/search/providers/kagi.ts +5 -1
- package/src/web/search/providers/kimi.ts +3 -1
- package/src/web/search/providers/parallel.ts +5 -1
- package/src/web/search/providers/perplexity.ts +5 -1
- package/src/web/search/providers/searxng.ts +3 -1
- package/src/web/search/providers/synthetic.ts +3 -1
- package/src/web/search/providers/tavily.ts +3 -1
- package/src/web/search/providers/utils.ts +33 -1
- package/src/web/search/providers/zai.ts +3 -1
package/src/task/executor.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
10
|
-
import {
|
|
10
|
+
import { type JsonSchemaValidationIssue, validateJsonSchemaValue } from "@oh-my-pi/pi-ai/utils/schema";
|
|
11
11
|
import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import { ModelRegistry } from "../config/model-registry";
|
|
13
13
|
import { resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
|
|
@@ -204,12 +204,59 @@ function parseStringifiedJson(value: unknown): unknown {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
|
|
207
|
+
interface OutputValidator {
|
|
208
|
+
validate: (value: unknown) => { ok: true } | { ok: false; message: string; missingRequired: string[] };
|
|
209
|
+
requiredFields: string[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildOutputValidator(schema: unknown): { validator?: OutputValidator; error?: string } {
|
|
208
213
|
const { normalized, error } = normalizeSchema(schema);
|
|
209
214
|
if (error) return { error };
|
|
210
215
|
if (normalized === undefined) return {};
|
|
211
216
|
const jsonSchema = jtdToJsonSchema(normalized);
|
|
212
|
-
|
|
217
|
+
const required = extractRequiredFields(jsonSchema);
|
|
218
|
+
return {
|
|
219
|
+
validator: {
|
|
220
|
+
requiredFields: required,
|
|
221
|
+
validate: value => {
|
|
222
|
+
const result = validateJsonSchemaValue(jsonSchema, value);
|
|
223
|
+
if (result.success) return { ok: true };
|
|
224
|
+
const missing = computeMissingRequired(required, value);
|
|
225
|
+
const message = formatValidationIssue(result.issues[0]) ?? "schema validation failed";
|
|
226
|
+
return { ok: false, message, missingRequired: missing };
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function extractRequiredFields(jsonSchema: unknown): string[] {
|
|
233
|
+
if (!jsonSchema || typeof jsonSchema !== "object") return [];
|
|
234
|
+
const required = (jsonSchema as { required?: unknown }).required;
|
|
235
|
+
return Array.isArray(required) ? required.filter((k): k is string => typeof k === "string") : [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function computeMissingRequired(required: readonly string[], value: unknown): string[] {
|
|
239
|
+
if (required.length === 0) return [];
|
|
240
|
+
if (value === null || value === undefined) return [...required];
|
|
241
|
+
if (typeof value !== "object" || Array.isArray(value)) return [];
|
|
242
|
+
const record = value as Record<string, unknown>;
|
|
243
|
+
return required.filter(key => !(key in record) || record[key] === undefined);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function formatValidationIssue(issue: JsonSchemaValidationIssue | undefined): string | undefined {
|
|
247
|
+
if (!issue) return undefined;
|
|
248
|
+
const path = issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)";
|
|
249
|
+
return `${path}: ${issue.message}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function previewOffendingData(value: unknown, maxLength = 500): string {
|
|
253
|
+
let serialized: string;
|
|
254
|
+
try {
|
|
255
|
+
serialized = JSON.stringify(value) ?? "null";
|
|
256
|
+
} catch {
|
|
257
|
+
serialized = String(value);
|
|
258
|
+
}
|
|
259
|
+
return serialized.length > maxLength ? `${serialized.slice(0, maxLength)}…` : serialized;
|
|
213
260
|
}
|
|
214
261
|
|
|
215
262
|
function tryParseJsonOutput(text: string): unknown | undefined {
|
|
@@ -253,9 +300,9 @@ function resolveFallbackCompletion(rawOutput: string, outputSchema: unknown): {
|
|
|
253
300
|
if (parsed === undefined) return null;
|
|
254
301
|
const candidate = parseStringifiedJson(extractCompletionData(parsed));
|
|
255
302
|
if (candidate === undefined) return null;
|
|
256
|
-
const {
|
|
303
|
+
const { validator, error } = buildOutputValidator(outputSchema);
|
|
257
304
|
if (error) return null;
|
|
258
|
-
if (
|
|
305
|
+
if (validator && !validator.validate(candidate).ok) return null;
|
|
259
306
|
return { data: candidate };
|
|
260
307
|
}
|
|
261
308
|
|
|
@@ -288,6 +335,31 @@ export const SUBAGENT_WARNING_NULL_YIELD = "SYSTEM WARNING: Subagent called yiel
|
|
|
288
335
|
export const SUBAGENT_WARNING_MISSING_YIELD =
|
|
289
336
|
"SYSTEM WARNING: Subagent exited without calling yield tool after 3 reminders.";
|
|
290
337
|
|
|
338
|
+
/** Build a schema_violation outcome — surfaced as a non-zero exit so callers treat it as a failure. */
|
|
339
|
+
function buildSchemaViolationOutcome(
|
|
340
|
+
failure: { message: string; missingRequired: string[] },
|
|
341
|
+
data: unknown,
|
|
342
|
+
): { rawOutput: string; stderr: string; exitCode: number } {
|
|
343
|
+
const missing = failure.missingRequired;
|
|
344
|
+
const headline =
|
|
345
|
+
missing.length > 0
|
|
346
|
+
? `schema_violation: missing required fields: ${missing.join(", ")}`
|
|
347
|
+
: `schema_violation: ${failure.message}`;
|
|
348
|
+
const payload = {
|
|
349
|
+
error: "schema_violation",
|
|
350
|
+
message: failure.message,
|
|
351
|
+
missingRequired: missing,
|
|
352
|
+
data: previewOffendingData(data),
|
|
353
|
+
};
|
|
354
|
+
let rawOutput: string;
|
|
355
|
+
try {
|
|
356
|
+
rawOutput = JSON.stringify(payload, null, 2);
|
|
357
|
+
} catch {
|
|
358
|
+
rawOutput = `{"error":"schema_violation","message":${JSON.stringify(headline)}}`;
|
|
359
|
+
}
|
|
360
|
+
return { rawOutput, stderr: headline, exitCode: 1 };
|
|
361
|
+
}
|
|
362
|
+
|
|
291
363
|
export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult {
|
|
292
364
|
let { rawOutput, exitCode, stderr } = args;
|
|
293
365
|
const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
|
|
@@ -311,14 +383,29 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
311
383
|
rawOutput = rawOutput ? `${SUBAGENT_WARNING_NULL_YIELD}\n\n${rawOutput}` : SUBAGENT_WARNING_NULL_YIELD;
|
|
312
384
|
} else {
|
|
313
385
|
const completeData = normalizeCompleteData(submitData, reportFindings);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
386
|
+
const { validator, error: schemaError } = buildOutputValidator(outputSchema);
|
|
387
|
+
if (schemaError) {
|
|
388
|
+
rawOutput = `{"error":"schema_violation","message":"invalid output schema: ${schemaError.replace(/"/g, '\\"')}"}`;
|
|
389
|
+
stderr = `schema_violation: invalid output schema: ${schemaError}`;
|
|
390
|
+
exitCode = 1;
|
|
391
|
+
} else {
|
|
392
|
+
const verdict = validator ? validator.validate(completeData) : { ok: true as const };
|
|
393
|
+
if (!verdict.ok) {
|
|
394
|
+
const outcome = buildSchemaViolationOutcome(verdict, completeData);
|
|
395
|
+
rawOutput = outcome.rawOutput;
|
|
396
|
+
stderr = outcome.stderr;
|
|
397
|
+
exitCode = outcome.exitCode;
|
|
398
|
+
} else {
|
|
399
|
+
try {
|
|
400
|
+
rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
|
|
401
|
+
} catch (err) {
|
|
402
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
403
|
+
rawOutput = `{"error":"Failed to serialize yield data: ${errorMessage}"}`;
|
|
404
|
+
}
|
|
405
|
+
exitCode = 0;
|
|
406
|
+
stderr = "";
|
|
407
|
+
}
|
|
319
408
|
}
|
|
320
|
-
exitCode = 0;
|
|
321
|
-
stderr = "";
|
|
322
409
|
}
|
|
323
410
|
}
|
|
324
411
|
} else {
|
|
@@ -328,14 +415,23 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
328
415
|
const fallback = allowFallback ? resolveFallbackCompletion(rawOutput, outputSchema) : null;
|
|
329
416
|
if (fallback) {
|
|
330
417
|
const completeData = normalizeCompleteData(fallback.data, reportFindings);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
rawOutput =
|
|
418
|
+
const { validator } = buildOutputValidator(outputSchema);
|
|
419
|
+
const verdict = validator ? validator.validate(completeData) : { ok: true as const };
|
|
420
|
+
if (!verdict.ok) {
|
|
421
|
+
const outcome = buildSchemaViolationOutcome(verdict, completeData);
|
|
422
|
+
rawOutput = outcome.rawOutput;
|
|
423
|
+
stderr = outcome.stderr;
|
|
424
|
+
exitCode = outcome.exitCode;
|
|
425
|
+
} else {
|
|
426
|
+
try {
|
|
427
|
+
rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
|
|
428
|
+
} catch (err) {
|
|
429
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
430
|
+
rawOutput = `{"error":"Failed to serialize fallback completion: ${errorMessage}"}`;
|
|
431
|
+
}
|
|
432
|
+
exitCode = 0;
|
|
433
|
+
stderr = "";
|
|
336
434
|
}
|
|
337
|
-
exitCode = 0;
|
|
338
|
-
stderr = "";
|
|
339
435
|
} else if (!hasOutputSchema && allowFallback && rawOutput.trim().length > 0) {
|
|
340
436
|
exitCode = 0;
|
|
341
437
|
stderr = "";
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
9
9
|
import { computeLineHash, HL_BODY_SEP } from "../hashline/hash";
|
|
10
10
|
import type { Theme } from "../modes/theme/theme";
|
|
11
11
|
import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
|
|
12
|
-
import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
12
|
+
import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
13
13
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
14
14
|
import type { ToolSession } from ".";
|
|
15
15
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
@@ -155,6 +155,9 @@ export interface AstEditToolDetails {
|
|
|
155
155
|
/** Pre-formatted text for the user-visible TUI render. Mirrors `result.text` lines but uses
|
|
156
156
|
* a `│` gutter (no model-only hashline anchors). The TUI uses this directly so it never parses model-facing text. */
|
|
157
157
|
displayContent?: string;
|
|
158
|
+
/** Absolute base directory used during the edit. Used by the renderer to resolve
|
|
159
|
+
* display-relative paths to absolute paths for OSC 8 hyperlinks. */
|
|
160
|
+
searchPath?: string;
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolDetails> {
|
|
@@ -241,6 +244,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
241
244
|
limitReached: result.limitReached,
|
|
242
245
|
...(cappedParseErrors.length > 0 ? { parseErrors: cappedParseErrors, parseErrorsTotal } : {}),
|
|
243
246
|
scopePath,
|
|
247
|
+
searchPath: resolvedSearchPath,
|
|
244
248
|
files: fileList,
|
|
245
249
|
fileReplacements: [],
|
|
246
250
|
};
|
|
@@ -483,6 +487,7 @@ export const astEditToolRenderer = {
|
|
|
483
487
|
return createCachedComponent(
|
|
484
488
|
() => options.expanded,
|
|
485
489
|
width => {
|
|
490
|
+
const searchBase = details?.searchPath;
|
|
486
491
|
const changeLines = renderTreeList(
|
|
487
492
|
{
|
|
488
493
|
items: changeGroups,
|
|
@@ -490,14 +495,42 @@ export const astEditToolRenderer = {
|
|
|
490
495
|
maxCollapsed: changeGroups.length,
|
|
491
496
|
maxCollapsedLines: COLLAPSED_CHANGE_LIMIT,
|
|
492
497
|
itemType: "change",
|
|
493
|
-
renderItem: group =>
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (line.startsWith("
|
|
498
|
+
renderItem: group => {
|
|
499
|
+
let contextDir = searchBase ?? "";
|
|
500
|
+
return group.map(line => {
|
|
501
|
+
if (line.startsWith("## ")) {
|
|
502
|
+
// Strip ` (3 replacements)` suffix attached by formatGroupedFiles.
|
|
503
|
+
const fileName = line
|
|
504
|
+
.slice(3)
|
|
505
|
+
.trimEnd()
|
|
506
|
+
.replace(/\s+\([^)]*\)\s*$/, "");
|
|
507
|
+
const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
|
|
508
|
+
const styled = uiTheme.fg("dim", line);
|
|
509
|
+
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
510
|
+
}
|
|
511
|
+
if (line.startsWith("# ")) {
|
|
512
|
+
const raw = line
|
|
513
|
+
.slice(2)
|
|
514
|
+
.trimEnd()
|
|
515
|
+
.replace(/\s+\([^)]*\)\s*$/, "");
|
|
516
|
+
const isDirectory = raw.endsWith("/");
|
|
517
|
+
const name = raw.replace(/\/$/, "");
|
|
518
|
+
if (isDirectory) {
|
|
519
|
+
if (searchBase) {
|
|
520
|
+
contextDir = name === "." ? searchBase : path.join(searchBase, name);
|
|
521
|
+
}
|
|
522
|
+
return uiTheme.fg("accent", line);
|
|
523
|
+
}
|
|
524
|
+
// Root-level file with optional suffix, e.g. `# foo.ts (3 replacements)`.
|
|
525
|
+
const absPath = searchBase && name ? path.join(searchBase, name) : undefined;
|
|
526
|
+
const styled = uiTheme.fg("accent", line);
|
|
527
|
+
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
528
|
+
}
|
|
497
529
|
if (line.startsWith("+")) return uiTheme.fg("toolDiffAdded", line);
|
|
498
530
|
if (line.startsWith("-")) return uiTheme.fg("toolDiffRemoved", line);
|
|
499
531
|
return uiTheme.fg("toolOutput", line);
|
|
500
|
-
})
|
|
532
|
+
});
|
|
533
|
+
},
|
|
501
534
|
},
|
|
502
535
|
uiTheme,
|
|
503
536
|
);
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -8,7 +8,7 @@ import * as z from "zod/v4";
|
|
|
8
8
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
9
9
|
import type { Theme } from "../modes/theme/theme";
|
|
10
10
|
import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
|
|
11
|
-
import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
11
|
+
import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
12
12
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
13
13
|
import type { ToolSession } from ".";
|
|
14
14
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
@@ -113,6 +113,9 @@ export interface AstGrepToolDetails {
|
|
|
113
113
|
/** Pre-formatted text for the user-visible TUI render. Mirrors `result.text` lines but uses
|
|
114
114
|
* a `│` gutter and `*` to mark match lines. The TUI uses this directly so it never parses model-facing text. */
|
|
115
115
|
displayContent?: string;
|
|
116
|
+
/** Absolute base directory used during search. Used by the renderer to resolve
|
|
117
|
+
* display-relative paths to absolute paths for OSC 8 hyperlinks. */
|
|
118
|
+
searchPath?: string;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolDetails> {
|
|
@@ -197,6 +200,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
197
200
|
limitReached: result.limitReached,
|
|
198
201
|
...(cappedParseErrors.length > 0 ? { parseErrors: cappedParseErrors, parseErrorsTotal } : {}),
|
|
199
202
|
scopePath,
|
|
203
|
+
searchPath: resolvedSearchPath,
|
|
200
204
|
files: fileList,
|
|
201
205
|
fileMatches: [],
|
|
202
206
|
};
|
|
@@ -366,6 +370,7 @@ export const astGrepToolRenderer = {
|
|
|
366
370
|
return createCachedComponent(
|
|
367
371
|
() => options.expanded,
|
|
368
372
|
width => {
|
|
373
|
+
const searchBase = details?.searchPath;
|
|
369
374
|
const matchLines = renderTreeList(
|
|
370
375
|
{
|
|
371
376
|
items: matchGroups,
|
|
@@ -373,13 +378,40 @@ export const astGrepToolRenderer = {
|
|
|
373
378
|
maxCollapsed: matchGroups.length,
|
|
374
379
|
maxCollapsedLines: COLLAPSED_MATCH_LIMIT,
|
|
375
380
|
itemType: "match",
|
|
376
|
-
renderItem: group =>
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (line.startsWith("
|
|
381
|
+
renderItem: group => {
|
|
382
|
+
let contextDir = searchBase ?? "";
|
|
383
|
+
return group.map(line => {
|
|
384
|
+
if (line.startsWith("## ")) {
|
|
385
|
+
const fileName = line
|
|
386
|
+
.slice(3)
|
|
387
|
+
.trimEnd()
|
|
388
|
+
.replace(/\s+\([^)]*\)\s*$/, "");
|
|
389
|
+
const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
|
|
390
|
+
const styled = uiTheme.fg("dim", line);
|
|
391
|
+
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
392
|
+
}
|
|
393
|
+
if (line.startsWith("# ")) {
|
|
394
|
+
const raw = line
|
|
395
|
+
.slice(2)
|
|
396
|
+
.trimEnd()
|
|
397
|
+
.replace(/\s+\([^)]*\)\s*$/, "");
|
|
398
|
+
const isDirectory = raw.endsWith("/");
|
|
399
|
+
const name = raw.replace(/\/$/, "");
|
|
400
|
+
if (isDirectory) {
|
|
401
|
+
if (searchBase) {
|
|
402
|
+
contextDir = name === "." ? searchBase : path.join(searchBase, name);
|
|
403
|
+
}
|
|
404
|
+
return uiTheme.fg("accent", line);
|
|
405
|
+
}
|
|
406
|
+
// Root-level file (single # without trailing slash) from formatGroupedFiles.
|
|
407
|
+
const absPath = searchBase && name ? path.join(searchBase, name) : undefined;
|
|
408
|
+
const styled = uiTheme.fg("accent", line);
|
|
409
|
+
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
410
|
+
}
|
|
380
411
|
if (line.startsWith(" meta:")) return uiTheme.fg("dim", line);
|
|
381
412
|
return uiTheme.fg("toolOutput", line);
|
|
382
|
-
})
|
|
413
|
+
});
|
|
414
|
+
},
|
|
383
415
|
},
|
|
384
416
|
uiTheme,
|
|
385
417
|
);
|
package/src/tools/find.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { InternalUrlRouter } from "../internal-urls";
|
|
|
11
11
|
import type { Theme } from "../modes/theme/theme";
|
|
12
12
|
import findDescription from "../prompts/tools/find.md" with { type: "text" };
|
|
13
13
|
import { type TruncationResult, truncateHead } from "../session/streaming-output";
|
|
14
|
-
import { Ellipsis, renderFileList, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
14
|
+
import { Ellipsis, fileHyperlink, renderFileList, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
15
15
|
import type { ToolSession } from ".";
|
|
16
16
|
import { applyListLimit } from "./list-limit";
|
|
17
17
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
@@ -84,6 +84,9 @@ export interface FindToolDetails {
|
|
|
84
84
|
files?: string[];
|
|
85
85
|
truncated?: boolean;
|
|
86
86
|
error?: string;
|
|
87
|
+
/** Working directory at search time. Used by the renderer to resolve relative
|
|
88
|
+
* file paths to absolute paths for OSC 8 hyperlinks. */
|
|
89
|
+
cwd?: string;
|
|
87
90
|
/** User-supplied paths whose base directory was missing on disk. The tool
|
|
88
91
|
* skipped these and continued with the surviving entries; surfaced as a
|
|
89
92
|
* non-fatal warning in the renderer and in the model-facing text. */
|
|
@@ -221,6 +224,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
221
224
|
fileCount: 0,
|
|
222
225
|
files: [],
|
|
223
226
|
truncated: forceTruncated,
|
|
227
|
+
cwd: this.session.cwd,
|
|
224
228
|
missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
|
|
225
229
|
};
|
|
226
230
|
const parts = ["No files found matching pattern"];
|
|
@@ -246,6 +250,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
246
250
|
truncated: Boolean(forceTruncated || limitMeta.resultLimit || truncation.truncated),
|
|
247
251
|
resultLimitReached: limitMeta.resultLimit?.reached,
|
|
248
252
|
truncation: truncation.truncated ? truncation : undefined,
|
|
253
|
+
cwd: this.session.cwd,
|
|
249
254
|
missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
|
|
250
255
|
};
|
|
251
256
|
|
|
@@ -513,11 +518,17 @@ export const findToolRenderer = {
|
|
|
513
518
|
return createCachedComponent(
|
|
514
519
|
() => options.expanded,
|
|
515
520
|
width => {
|
|
521
|
+
const cwd = details?.cwd;
|
|
516
522
|
const fileLines = renderFileList(
|
|
517
523
|
{
|
|
518
|
-
files: files.map(entry => ({
|
|
524
|
+
files: files.map(entry => ({
|
|
525
|
+
path: entry,
|
|
526
|
+
isDirectory: entry.endsWith("/"),
|
|
527
|
+
absPath: cwd && !entry.endsWith("/") ? path.resolve(cwd, entry) : undefined,
|
|
528
|
+
})),
|
|
519
529
|
expanded: options.expanded,
|
|
520
530
|
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
531
|
+
hyperlinkFn: fileHyperlink,
|
|
521
532
|
},
|
|
522
533
|
uiTheme,
|
|
523
534
|
);
|
package/src/tools/read.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
truncateHeadBytes,
|
|
28
28
|
truncateLine,
|
|
29
29
|
} from "../session/streaming-output";
|
|
30
|
-
import { renderCodeCell, renderMarkdownCell, renderStatusLine } from "../tui";
|
|
30
|
+
import { fileHyperlink, renderCodeCell, renderMarkdownCell, renderStatusLine, tryResolveInternalUrlSync } from "../tui";
|
|
31
31
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
32
32
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
33
33
|
import { ImageInputTooLargeError, loadImageInput, MAX_IMAGE_INPUT_BYTES } from "../utils/image-loading";
|
|
@@ -118,8 +118,22 @@ function formatTextWithMode(
|
|
|
118
118
|
startNum: number,
|
|
119
119
|
shouldAddHashLines: boolean,
|
|
120
120
|
shouldAddLineNumbers: boolean,
|
|
121
|
+
truncatedLines?: ReadonlySet<number>,
|
|
121
122
|
): string {
|
|
122
|
-
if (shouldAddHashLines)
|
|
123
|
+
if (shouldAddHashLines) {
|
|
124
|
+
if (!truncatedLines || truncatedLines.size === 0) return formatHashLines(text, startNum);
|
|
125
|
+
// Column-truncated lines hash differently from the on-disk line that the
|
|
126
|
+
// edit verifier reads back. Drop the anchor (`LINE|TEXT` instead of
|
|
127
|
+
// `LINE+HASH|TEXT`) so the model treats the line as un-anchorable rather
|
|
128
|
+
// than copying a hash that will be rejected as stale.
|
|
129
|
+
const lines = text.split("\n");
|
|
130
|
+
return lines
|
|
131
|
+
.map((line, i) => {
|
|
132
|
+
const ln = startNum + i;
|
|
133
|
+
return truncatedLines.has(ln) ? `${ln}${HL_BODY_SEP}${line}` : formatHashLine(ln, line);
|
|
134
|
+
})
|
|
135
|
+
.join("\n");
|
|
136
|
+
}
|
|
123
137
|
if (shouldAddLineNumbers) return prependLineNumbers(text, startNum);
|
|
124
138
|
return text;
|
|
125
139
|
}
|
|
@@ -1031,12 +1045,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1031
1045
|
}
|
|
1032
1046
|
|
|
1033
1047
|
const collectedLines = streamResult.lines;
|
|
1048
|
+
const truncatedLineNumbers = new Set<number>();
|
|
1034
1049
|
if (!rawSelector && maxColumns > 0) {
|
|
1035
1050
|
for (let i = 0; i < collectedLines.length; i++) {
|
|
1036
1051
|
const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
|
|
1037
1052
|
if (wasTruncated) {
|
|
1038
1053
|
collectedLines[i] = text;
|
|
1039
1054
|
columnTruncated = maxColumns;
|
|
1055
|
+
truncatedLineNumbers.add(range.startLine + i);
|
|
1040
1056
|
}
|
|
1041
1057
|
}
|
|
1042
1058
|
}
|
|
@@ -1046,7 +1062,15 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1046
1062
|
}
|
|
1047
1063
|
|
|
1048
1064
|
const blockText = collectedLines.join("\n");
|
|
1049
|
-
blocks.push(
|
|
1065
|
+
blocks.push(
|
|
1066
|
+
formatTextWithMode(
|
|
1067
|
+
blockText,
|
|
1068
|
+
range.startLine,
|
|
1069
|
+
shouldAddHashLines,
|
|
1070
|
+
shouldAddLineNumbers,
|
|
1071
|
+
truncatedLineNumbers,
|
|
1072
|
+
),
|
|
1073
|
+
);
|
|
1050
1074
|
}
|
|
1051
1075
|
|
|
1052
1076
|
let outputText = blocks.join("\n\n…\n\n");
|
|
@@ -1790,12 +1814,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1790
1814
|
// view — column truncation surfaces separately via `.limits()`.
|
|
1791
1815
|
const rawSelector = isRawSelector(parsed);
|
|
1792
1816
|
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
1817
|
+
const truncatedLineNumbers = new Set<number>();
|
|
1793
1818
|
if (!rawSelector && maxColumns > 0) {
|
|
1794
1819
|
for (let i = 0; i < collectedLines.length; i++) {
|
|
1795
1820
|
const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
|
|
1796
1821
|
if (wasTruncated) {
|
|
1797
1822
|
collectedLines[i] = text;
|
|
1798
1823
|
columnTruncated = maxColumns;
|
|
1824
|
+
truncatedLineNumbers.add(startLineDisplay + i);
|
|
1799
1825
|
}
|
|
1800
1826
|
}
|
|
1801
1827
|
}
|
|
@@ -1829,7 +1855,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1829
1855
|
let capturedDisplayContent: { text: string; startLine: number } | undefined;
|
|
1830
1856
|
const formatText = (text: string, startNum: number): string => {
|
|
1831
1857
|
capturedDisplayContent = { text, startLine: startNum };
|
|
1832
|
-
return formatTextWithMode(
|
|
1858
|
+
return formatTextWithMode(
|
|
1859
|
+
text,
|
|
1860
|
+
startNum,
|
|
1861
|
+
shouldAddHashLines,
|
|
1862
|
+
shouldAddLineNumbers,
|
|
1863
|
+
truncatedLineNumbers,
|
|
1864
|
+
);
|
|
1833
1865
|
};
|
|
1834
1866
|
|
|
1835
1867
|
let outputText: string;
|
|
@@ -2144,7 +2176,9 @@ export const readToolRenderer = {
|
|
|
2144
2176
|
}
|
|
2145
2177
|
|
|
2146
2178
|
const rawPath = args.file_path || args.path || "";
|
|
2147
|
-
const
|
|
2179
|
+
const shortPath = shortenPath(rawPath);
|
|
2180
|
+
const linkTarget = tryResolveInternalUrlSync(rawPath);
|
|
2181
|
+
const filePath = linkTarget ? fileHyperlink(linkTarget, shortPath) : shortPath;
|
|
2148
2182
|
const offset = args.offset;
|
|
2149
2183
|
const limit = args.limit;
|
|
2150
2184
|
|
|
@@ -2261,7 +2295,13 @@ export const readToolRenderer = {
|
|
|
2261
2295
|
}
|
|
2262
2296
|
|
|
2263
2297
|
const suffix = details?.suffixResolution;
|
|
2264
|
-
const
|
|
2298
|
+
const plainDisplayPath = suffix ? shortenPath(suffix.to) : filePath;
|
|
2299
|
+
// resolvedPath is the absolute fs path for fs-backed reads (regular files plus
|
|
2300
|
+
// local:// / memory:// / skill:// / artifact:// resources). Fall back to a sync
|
|
2301
|
+
// resolver for fs-backed internal URLs so the title is clickable even before the
|
|
2302
|
+
// result lands or if the handler didn't populate resolvedPath.
|
|
2303
|
+
const absForLink = details?.resolvedPath ?? tryResolveInternalUrlSync(rawPath);
|
|
2304
|
+
const displayPath = absForLink ? fileHyperlink(absForLink, plainDisplayPath) : plainDisplayPath;
|
|
2265
2305
|
const correction = suffix ? ` ${uiTheme.fg("dim", `(corrected from ${shortenPath(suffix.from)})`)}` : "";
|
|
2266
2306
|
let title = displayPath ? `Read ${displayPath}${correction}` : "Read";
|
|
2267
2307
|
if (args?.offset !== undefined || args?.limit !== undefined) {
|