@oh-my-pi/pi-coding-agent 15.3.1 → 15.4.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 +119 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +1 -1
- package/dist/types/cli/file-processor.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +45 -3
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +2 -0
- package/dist/types/edit/file-read-cache.d.ts +15 -4
- package/dist/types/edit/index.d.ts +3 -8
- package/dist/types/edit/renderer.d.ts +1 -2
- package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
- package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
- package/dist/types/eval/js/shared/runtime.d.ts +14 -8
- package/dist/types/eval/py/executor.d.ts +1 -2
- package/dist/types/eval/py/kernel.d.ts +6 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -5
- package/dist/types/eval/session-id.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +1 -3
- package/dist/types/hashline/anchors.d.ts +15 -9
- package/dist/types/hashline/constants.d.ts +0 -2
- package/dist/types/hashline/diff.d.ts +1 -2
- package/dist/types/hashline/executor.d.ts +52 -0
- package/dist/types/hashline/hash.d.ts +44 -93
- package/dist/types/hashline/index.d.ts +2 -1
- package/dist/types/hashline/input.d.ts +2 -9
- package/dist/types/hashline/recovery.d.ts +3 -9
- package/dist/types/hashline/tokenizer.d.ts +91 -0
- package/dist/types/hashline/types.d.ts +5 -7
- package/dist/types/modes/components/extensions/types.d.ts +0 -4
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +11 -15
- package/dist/types/session/agent-storage.d.ts +11 -10
- package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
- package/dist/types/slash-commands/types.d.ts +0 -5
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/types.d.ts +8 -0
- package/dist/types/tool-discovery/tool-index.d.ts +0 -50
- package/dist/types/tools/index.d.ts +2 -8
- package/dist/types/tools/match-line-format.d.ts +4 -4
- package/dist/types/tools/output-schema-validator.d.ts +64 -0
- package/dist/types/tools/review.d.ts +13 -0
- package/dist/types/tools/search-tool-bm25.d.ts +1 -1
- package/dist/types/tools/search.d.ts +4 -3
- package/dist/types/utils/edit-mode.d.ts +1 -1
- package/dist/types/web/kagi.d.ts +4 -2
- package/dist/types/web/parallel.d.ts +4 -3
- package/dist/types/web/scrapers/types.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +12 -4
- package/dist/types/web/search/provider.d.ts +2 -1
- package/dist/types/web/search/providers/anthropic.d.ts +9 -4
- package/dist/types/web/search/providers/base.d.ts +34 -2
- package/dist/types/web/search/providers/brave.d.ts +8 -1
- package/dist/types/web/search/providers/codex.d.ts +13 -9
- package/dist/types/web/search/providers/exa.d.ts +10 -1
- package/dist/types/web/search/providers/gemini.d.ts +20 -23
- package/dist/types/web/search/providers/jina.d.ts +2 -1
- package/dist/types/web/search/providers/kagi.d.ts +4 -1
- package/dist/types/web/search/providers/kimi.d.ts +10 -1
- package/dist/types/web/search/providers/parallel.d.ts +3 -2
- package/dist/types/web/search/providers/perplexity.d.ts +5 -2
- package/dist/types/web/search/providers/searxng.d.ts +2 -1
- package/dist/types/web/search/providers/synthetic.d.ts +5 -8
- package/dist/types/web/search/providers/tavily.d.ts +11 -4
- package/dist/types/web/search/providers/utils.d.ts +8 -6
- package/dist/types/web/search/providers/zai.d.ts +12 -3
- package/package.json +7 -7
- package/src/cli/auth-gateway-cli.ts +71 -2
- package/src/cli/file-processor.ts +12 -2
- package/src/cli.ts +0 -8
- package/src/commands/auth-gateway.ts +2 -0
- package/src/commands/commit.ts +8 -8
- package/src/config/prompt-templates.ts +6 -6
- package/src/config/settings-schema.ts +47 -3
- package/src/config/settings.ts +5 -5
- package/src/debug/raw-sse.ts +68 -3
- package/src/edit/file-read-cache.ts +68 -25
- package/src/edit/index.ts +6 -37
- package/src/edit/renderer.ts +9 -47
- package/src/edit/streaming.ts +43 -56
- package/src/eval/__tests__/shared-executors.test.ts +520 -0
- package/src/eval/js/context-manager.ts +64 -53
- package/src/eval/js/shared/local-module-loader.ts +265 -0
- package/src/eval/js/shared/prelude.txt +4 -0
- package/src/eval/js/shared/rewrite-imports.ts +85 -0
- package/src/eval/js/shared/runtime.ts +129 -86
- package/src/eval/js/worker-core.ts +23 -38
- package/src/eval/py/executor.ts +155 -84
- package/src/eval/py/kernel.ts +10 -1
- package/src/eval/py/prelude.py +22 -24
- package/src/eval/py/runner.py +203 -85
- package/src/eval/py/tool-bridge.ts +17 -10
- package/src/eval/session-id.ts +8 -0
- package/src/exec/bash-executor.ts +27 -16
- package/src/extensibility/extensions/runner.ts +0 -1
- package/src/extensibility/extensions/types.ts +1 -3
- package/src/extensibility/plugins/marketplace/manager.ts +20 -1
- package/src/hashline/anchors.ts +56 -65
- package/src/hashline/apply.ts +29 -31
- package/src/hashline/constants.ts +0 -3
- package/src/hashline/diff-preview.ts +4 -5
- package/src/hashline/diff.ts +30 -4
- package/src/hashline/execute.ts +91 -26
- package/src/hashline/executor.ts +239 -0
- package/src/hashline/grammar.lark +12 -10
- package/src/hashline/hash.ts +69 -114
- package/src/hashline/index.ts +2 -1
- package/src/hashline/input.ts +48 -41
- package/src/hashline/prefixes.ts +21 -11
- package/src/hashline/recovery.ts +63 -71
- package/src/hashline/stream.ts +2 -2
- package/src/hashline/tokenizer.ts +467 -0
- package/src/hashline/types.ts +6 -8
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/lsp/config.ts +87 -22
- package/src/modes/components/extensions/types.ts +0 -5
- package/src/modes/components/session-observer-overlay.ts +11 -2
- package/src/modes/components/tree-selector.ts +10 -2
- package/src/modes/controllers/command-controller.ts +1 -3
- package/src/modes/controllers/extension-ui-controller.ts +10 -11
- package/src/modes/controllers/selector-controller.ts +5 -5
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/ui-helpers.ts +4 -0
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/prompts/tools/hashline.md +73 -94
- package/src/prompts/tools/read.md +4 -4
- package/src/prompts/tools/search.md +3 -3
- package/src/sdk.ts +21 -24
- package/src/session/agent-session.ts +59 -66
- package/src/session/agent-storage.ts +13 -14
- package/src/slash-commands/acp-builtins.ts +3 -3
- package/src/slash-commands/types.ts +0 -6
- package/src/task/executor.ts +55 -57
- package/src/task/index.ts +8 -4
- package/src/task/render.ts +53 -1
- package/src/task/types.ts +8 -0
- package/src/tool-discovery/tool-index.ts +0 -134
- package/src/tools/ast-edit.ts +36 -13
- package/src/tools/ast-grep.ts +45 -4
- package/src/tools/browser/tab-worker.ts +3 -2
- package/src/tools/eval.ts +2 -1
- package/src/tools/fetch.ts +23 -14
- package/src/tools/index.ts +2 -8
- package/src/tools/irc.ts +59 -5
- package/src/tools/jtd-to-json-schema.ts +5 -1
- package/src/tools/match-line-format.ts +5 -7
- package/src/tools/output-schema-validator.ts +132 -0
- package/src/tools/read.ts +142 -63
- package/src/tools/review.ts +23 -0
- package/src/tools/search-tool-bm25.ts +3 -30
- package/src/tools/search.ts +48 -16
- package/src/tools/write.ts +3 -3
- package/src/tools/yield.ts +32 -41
- package/src/utils/edit-mode.ts +1 -2
- package/src/utils/file-mentions.ts +2 -2
- package/src/web/kagi.ts +15 -6
- package/src/web/parallel.ts +9 -6
- package/src/web/scrapers/types.ts +7 -1
- package/src/web/scrapers/youtube.ts +13 -7
- package/src/web/search/index.ts +37 -11
- package/src/web/search/provider.ts +5 -3
- package/src/web/search/providers/anthropic.ts +30 -21
- package/src/web/search/providers/base.ts +35 -2
- package/src/web/search/providers/brave.ts +4 -4
- package/src/web/search/providers/codex.ts +118 -89
- package/src/web/search/providers/exa.ts +3 -2
- package/src/web/search/providers/gemini.ts +58 -155
- package/src/web/search/providers/jina.ts +4 -4
- package/src/web/search/providers/kagi.ts +17 -11
- package/src/web/search/providers/kimi.ts +29 -13
- package/src/web/search/providers/parallel.ts +171 -23
- package/src/web/search/providers/perplexity.ts +38 -37
- package/src/web/search/providers/searxng.ts +3 -1
- package/src/web/search/providers/synthetic.ts +16 -19
- package/src/web/search/providers/tavily.ts +23 -18
- package/src/web/search/providers/utils.ts +11 -17
- package/src/web/search/providers/zai.ts +16 -8
- package/dist/types/hashline/parser.d.ts +0 -7
- package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
- package/dist/types/tools/vim.d.ts +0 -58
- package/dist/types/vim/buffer.d.ts +0 -41
- package/dist/types/vim/commands.d.ts +0 -6
- package/dist/types/vim/engine.d.ts +0 -47
- package/dist/types/vim/parser.d.ts +0 -3
- package/dist/types/vim/render.d.ts +0 -25
- package/dist/types/vim/types.d.ts +0 -182
- package/src/hashline/parser.ts +0 -212
- package/src/mcp/discoverable-tool-metadata.ts +0 -24
- package/src/prompts/tools/vim.md +0 -98
- package/src/tools/vim.ts +0 -949
- package/src/vim/buffer.ts +0 -309
- package/src/vim/commands.ts +0 -382
- package/src/vim/engine.ts +0 -2409
- package/src/vim/parser.ts +0 -134
- package/src/vim/render.ts +0 -252
- package/src/vim/types.ts +0 -197
package/src/tools/irc.ts
CHANGED
|
@@ -25,6 +25,7 @@ import ircDescription from "../prompts/tools/irc.md" with { type: "text" };
|
|
|
25
25
|
import type { AgentRef, AgentRegistry } from "../registry/agent-registry";
|
|
26
26
|
import type { ToolSession } from ".";
|
|
27
27
|
|
|
28
|
+
const DEFAULT_IRC_TIMEOUT_MS = 120_000;
|
|
28
29
|
const ircSchema = z.object({
|
|
29
30
|
op: z.enum(["send", "list"]).describe("irc operation"),
|
|
30
31
|
to: z.string().optional().describe('recipient agent id or "all"'),
|
|
@@ -159,6 +160,7 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
159
160
|
|
|
160
161
|
const awaitReply = params.awaitReply ?? !isBroadcast;
|
|
161
162
|
|
|
163
|
+
const timeoutMs = normalizeIrcTimeoutMs(this.session.settings.get("irc.timeoutMs"));
|
|
162
164
|
const delivered: string[] = [];
|
|
163
165
|
const replies: IrcReply[] = [];
|
|
164
166
|
const failed: Array<{ id: string; error: string }> = [];
|
|
@@ -174,12 +176,18 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
174
176
|
return;
|
|
175
177
|
}
|
|
176
178
|
try {
|
|
177
|
-
const result = await
|
|
178
|
-
|
|
179
|
-
message,
|
|
180
|
-
awaitReply,
|
|
179
|
+
const result = await runIrcDispatchWithTimeout(
|
|
180
|
+
timeoutMs,
|
|
181
181
|
signal,
|
|
182
|
-
|
|
182
|
+
timeoutSignal =>
|
|
183
|
+
targetSession.respondAsBackground({
|
|
184
|
+
from: senderId,
|
|
185
|
+
message,
|
|
186
|
+
awaitReply,
|
|
187
|
+
signal: timeoutSignal,
|
|
188
|
+
}),
|
|
189
|
+
target.id,
|
|
190
|
+
);
|
|
183
191
|
delivered.push(target.id);
|
|
184
192
|
if (awaitReply && result.replyText) {
|
|
185
193
|
replies.push({ from: target.id, text: result.replyText });
|
|
@@ -237,3 +245,49 @@ function errorResult(text: string, details: IrcDetails): AgentToolResult<IrcDeta
|
|
|
237
245
|
details,
|
|
238
246
|
};
|
|
239
247
|
}
|
|
248
|
+
|
|
249
|
+
function normalizeIrcTimeoutMs(value: number): number {
|
|
250
|
+
if (!Number.isFinite(value) || value === 0) return value === 0 ? 0 : DEFAULT_IRC_TIMEOUT_MS;
|
|
251
|
+
return Math.max(1, Math.trunc(value));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function runIrcDispatchWithTimeout<T>(
|
|
255
|
+
timeoutMs: number,
|
|
256
|
+
parentSignal: AbortSignal | undefined,
|
|
257
|
+
run: (signal?: AbortSignal) => Promise<T>,
|
|
258
|
+
targetId: string,
|
|
259
|
+
): Promise<T> {
|
|
260
|
+
if (timeoutMs <= 0) {
|
|
261
|
+
return await run(parentSignal);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const controller = new AbortController();
|
|
265
|
+
const timeoutError = new Error(`IRC timed out waiting for ${targetId} after ${timeoutMs} ms`);
|
|
266
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
267
|
+
let parentAbortListener: (() => void) | undefined;
|
|
268
|
+
|
|
269
|
+
const timeoutDeferred = Promise.withResolvers<never>();
|
|
270
|
+
if (parentSignal) {
|
|
271
|
+
if (parentSignal.aborted) {
|
|
272
|
+
throw parentSignal.reason instanceof Error ? parentSignal.reason : new Error("IRC aborted");
|
|
273
|
+
}
|
|
274
|
+
parentAbortListener = () => {
|
|
275
|
+
controller.abort(parentSignal.reason);
|
|
276
|
+
timeoutDeferred.reject(parentSignal.reason instanceof Error ? parentSignal.reason : new Error("IRC aborted"));
|
|
277
|
+
};
|
|
278
|
+
parentSignal.addEventListener("abort", parentAbortListener, { once: true });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
timeout = setTimeout(() => {
|
|
282
|
+
controller.abort(timeoutError);
|
|
283
|
+
timeoutDeferred.reject(timeoutError);
|
|
284
|
+
}, timeoutMs);
|
|
285
|
+
timeout.unref?.();
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
return await Promise.race([run(controller.signal), timeoutDeferred.promise]);
|
|
289
|
+
} finally {
|
|
290
|
+
if (timeout) clearTimeout(timeout);
|
|
291
|
+
if (parentSignal && parentAbortListener) parentSignal.removeEventListener("abort", parentAbortListener);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -180,7 +180,11 @@ function normalizeMixedSchemaNode(schema: unknown): unknown {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
if (isJTDSchema(schema)) {
|
|
183
|
-
|
|
183
|
+
// `convertSchema` is itself fully recursive and emits pure JSON Schema, so
|
|
184
|
+
// re-walking the result with `normalizeMixedSchemaNode` is unnecessary and
|
|
185
|
+
// unsafe: it would treat user-named properties whose keys happen to be JTD
|
|
186
|
+
// keywords (e.g. `ref`, `elements`) as nested JTD forms (#1345).
|
|
187
|
+
return convertSchema(schema);
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
const normalized: Record<string, unknown> = {};
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { computeLineHash } from "../hashline/hash";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Format a single line of match output for grep/ast-grep style results.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Matched lines are prefixed with `*`; context lines are prefixed with a single
|
|
5
|
+
* space so line numbers align in column. In hashline mode the line uses the
|
|
6
|
+
* editable `LINE:content` shape under a file-hash header; in plain mode it keeps
|
|
7
|
+
* the legacy `LINE|content` display-only shape. Line numbers are never padded.
|
|
10
8
|
*/
|
|
11
9
|
export function formatMatchLine(
|
|
12
10
|
lineNumber: number,
|
|
@@ -16,7 +14,7 @@ export function formatMatchLine(
|
|
|
16
14
|
): string {
|
|
17
15
|
const marker = isMatch ? "*" : " ";
|
|
18
16
|
if (options.useHashLines) {
|
|
19
|
-
return `${marker}${lineNumber}
|
|
17
|
+
return `${marker}${lineNumber}:${line}`;
|
|
20
18
|
}
|
|
21
19
|
return `${marker}${lineNumber}|${line}`;
|
|
22
20
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared output-schema validation for subagent yield + executor finalization.
|
|
3
|
+
*
|
|
4
|
+
* Both the in-process `yield` tool (subagent side) and the executor's post-mortem
|
|
5
|
+
* finalize path (parent side) need to validate yield payloads against the agent's
|
|
6
|
+
* declared output schema. This module is the single source of truth for that
|
|
7
|
+
* pipeline — keeping the two callsites in lockstep so a schema accepted in-tool
|
|
8
|
+
* cannot be rejected post-mortem (or vice versa).
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
isValidJsonSchema,
|
|
12
|
+
type JsonSchemaValidationIssue,
|
|
13
|
+
type JsonSchemaValidationResult,
|
|
14
|
+
validateJsonSchemaValue,
|
|
15
|
+
} from "@oh-my-pi/pi-ai/utils/schema";
|
|
16
|
+
import { jtdToJsonSchema, normalizeSchema } from "./jtd-to-json-schema";
|
|
17
|
+
|
|
18
|
+
/** A validator bound to a specific output schema. */
|
|
19
|
+
export interface OutputValidator {
|
|
20
|
+
/** Run JSON Schema validation; returns the raw `success`/`issues` shape so callers may inspect every failure. */
|
|
21
|
+
validate(value: unknown): JsonSchemaValidationResult;
|
|
22
|
+
/** Top-level required property names. Empty if the schema has no `required` array at root. */
|
|
23
|
+
readonly requiredFields: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BuildOutputValidatorResult {
|
|
27
|
+
/** Present when the schema produced a usable validator (i.e. constraining schemas). Absent for missing/unconstrained schemas. */
|
|
28
|
+
validator?: OutputValidator;
|
|
29
|
+
/** Raw JSON Schema produced by `jtdToJsonSchema`. Available alongside the validator so callers can derive related artifacts (strict-mode probe, dereference, hint text). */
|
|
30
|
+
jsonSchema?: Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Normalized schema (post-`normalizeSchema`). Surfaced so callers can distinguish
|
|
33
|
+
* "no schema provided" (`undefined`) from "intentionally unconstrained" (`true`)
|
|
34
|
+
* when both produce no validator.
|
|
35
|
+
*/
|
|
36
|
+
normalized?: unknown;
|
|
37
|
+
/** Set when the schema cannot be used. Callers should treat this as a "no validation" case (loose acceptance) and surface the message in diagnostics. */
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build the canonical validator for a JTD-or-JSON-Schema output declaration.
|
|
43
|
+
*
|
|
44
|
+
* Returns:
|
|
45
|
+
* - `{ validator, jsonSchema, normalized }` for constraining schemas — both callers use this path.
|
|
46
|
+
* - `{ normalized: true }` for an intentionally unconstrained schema (the JSON Schema literal `true`).
|
|
47
|
+
* No validator, but distinguishable from "no schema provided".
|
|
48
|
+
* - `{}` for an absent schema (`undefined`).
|
|
49
|
+
* - `{ error, normalized? }` when the schema cannot be honored (invalid syntax, `false`, malformed JTD).
|
|
50
|
+
*/
|
|
51
|
+
export function buildOutputValidator(schema: unknown): BuildOutputValidatorResult {
|
|
52
|
+
const { normalized, error: normalizeError } = normalizeSchema(schema);
|
|
53
|
+
if (normalizeError) return { error: normalizeError, normalized };
|
|
54
|
+
if (normalized === undefined) return {};
|
|
55
|
+
if (normalized === false) return { error: "boolean false schema rejects all outputs", normalized };
|
|
56
|
+
if (normalized === true) return { normalized };
|
|
57
|
+
|
|
58
|
+
const jsonSchema = jtdToJsonSchema(normalized);
|
|
59
|
+
if (jsonSchema === undefined) return { normalized };
|
|
60
|
+
if (jsonSchema === false) return { error: "boolean false schema rejects all outputs", normalized };
|
|
61
|
+
if (jsonSchema === true) return { normalized };
|
|
62
|
+
if (typeof jsonSchema !== "object" || Array.isArray(jsonSchema)) {
|
|
63
|
+
return { error: "invalid JSON schema", normalized };
|
|
64
|
+
}
|
|
65
|
+
if (!isValidJsonSchema(jsonSchema)) return { error: "invalid JSON schema", normalized };
|
|
66
|
+
|
|
67
|
+
const jsonSchemaRecord = jsonSchema as Record<string, unknown>;
|
|
68
|
+
const required = extractRequiredFields(jsonSchemaRecord);
|
|
69
|
+
return {
|
|
70
|
+
normalized,
|
|
71
|
+
jsonSchema: jsonSchemaRecord,
|
|
72
|
+
validator: {
|
|
73
|
+
requiredFields: required,
|
|
74
|
+
validate: value => validateJsonSchemaValue(jsonSchemaRecord, value),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Produce the executor's headline+missing-required summary from a failed validation. */
|
|
80
|
+
export function summarizeValidationFailure(
|
|
81
|
+
result: JsonSchemaValidationResult,
|
|
82
|
+
value: unknown,
|
|
83
|
+
requiredFields: readonly string[],
|
|
84
|
+
): { message: string; missingRequired: string[] } {
|
|
85
|
+
if (result.success) return { message: "", missingRequired: [] };
|
|
86
|
+
const missing = computeMissingRequired(requiredFields, value);
|
|
87
|
+
const message = formatValidationIssueHeadline(result.issues[0]) ?? "schema validation failed";
|
|
88
|
+
return { message, missingRequired: missing };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function extractRequiredFields(jsonSchema: unknown): string[] {
|
|
92
|
+
if (!jsonSchema || typeof jsonSchema !== "object") return [];
|
|
93
|
+
const required = (jsonSchema as { required?: unknown }).required;
|
|
94
|
+
return Array.isArray(required) ? required.filter((k): k is string => typeof k === "string") : [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function computeMissingRequired(required: readonly string[], value: unknown): string[] {
|
|
98
|
+
if (required.length === 0) return [];
|
|
99
|
+
if (value === null || value === undefined) return [...required];
|
|
100
|
+
if (typeof value !== "object" || Array.isArray(value)) return [];
|
|
101
|
+
const record = value as Record<string, unknown>;
|
|
102
|
+
return required.filter(key => !(key in record) || record[key] === undefined);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Format a single validation issue as `path.with.dots: message`.
|
|
107
|
+
*
|
|
108
|
+
* Used by the executor's post-mortem `schema_violation` headline — one line, dot-separated path,
|
|
109
|
+
* since the executor's error format already lists missing-required fields separately.
|
|
110
|
+
*/
|
|
111
|
+
export function formatValidationIssueHeadline(issue: JsonSchemaValidationIssue | undefined): string | undefined {
|
|
112
|
+
if (!issue) return undefined;
|
|
113
|
+
const path = issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)";
|
|
114
|
+
return `${path}: ${issue.message}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Format every validation issue as `path/with/slashes: message; ...`.
|
|
119
|
+
*
|
|
120
|
+
* Used by the yield tool's model-facing retry feedback — the model gets every problem at once so it
|
|
121
|
+
* can fix the entire output in one retry instead of iterating issue-by-issue. The slash separator
|
|
122
|
+
* mirrors JSON Pointer convention and disambiguates against fields whose names contain dots.
|
|
123
|
+
*/
|
|
124
|
+
export function formatAllValidationIssues(issues: ReadonlyArray<JsonSchemaValidationIssue> | undefined): string {
|
|
125
|
+
if (!issues || issues.length === 0) return "Unknown schema validation error.";
|
|
126
|
+
return issues
|
|
127
|
+
.map(issue => {
|
|
128
|
+
const path = issue.path.length === 0 ? "" : `${issue.path.map(seg => String(seg)).join("/")}: `;
|
|
129
|
+
return `${path}${issue.message}`;
|
|
130
|
+
})
|
|
131
|
+
.join("; ");
|
|
132
|
+
}
|
package/src/tools/read.ts
CHANGED
|
@@ -9,9 +9,10 @@ import { Text } from "@oh-my-pi/pi-tui";
|
|
|
9
9
|
import { getRemoteDir, logger, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import * as z from "zod/v4";
|
|
11
11
|
import { getFileReadCache } from "../edit/file-read-cache";
|
|
12
|
+
import { normalizeToLF } from "../edit/normalize";
|
|
12
13
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
13
14
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
14
|
-
import {
|
|
15
|
+
import { computeFileHash, formatHashlineHeader, formatNumberedLine, formatNumberedLines } from "../hashline/hash";
|
|
15
16
|
import { InternalUrlRouter } from "../internal-urls";
|
|
16
17
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
17
18
|
import type { InternalUrl } from "../internal-urls/types";
|
|
@@ -113,27 +114,50 @@ function prependLineNumbers(text: string, startNum: number): string {
|
|
|
113
114
|
return textLines.map((line, i) => `${startNum + i}|${line}`).join("\n");
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
interface HashlineHeaderContext {
|
|
118
|
+
header: string;
|
|
119
|
+
fileHash: string;
|
|
120
|
+
fullText: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildHashlineHeaderContext(displayPath: string, fullText: string): HashlineHeaderContext {
|
|
124
|
+
const normalized = normalizeToLF(fullText);
|
|
125
|
+
const fileHash = computeFileHash(normalized);
|
|
126
|
+
return {
|
|
127
|
+
header: formatHashlineHeader(displayPath, fileHash),
|
|
128
|
+
fileHash,
|
|
129
|
+
fullText: normalized,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function readHashlineHeaderContext(absolutePath: string, cwd: string): Promise<HashlineHeaderContext> {
|
|
134
|
+
const fullText = await Bun.file(absolutePath).text();
|
|
135
|
+
return buildHashlineHeaderContext(formatPathRelativeToCwd(absolutePath, cwd), fullText);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function prependHashlineHeader(text: string, context: HashlineHeaderContext | undefined): string {
|
|
139
|
+
return context ? `${context.header}\n${text}` : text;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function recordHashlineSnapshot(
|
|
143
|
+
session: ToolSession,
|
|
144
|
+
absolutePath: string | undefined,
|
|
145
|
+
context: HashlineHeaderContext | undefined,
|
|
146
|
+
): void {
|
|
147
|
+
if (!context || !absolutePath || !path.isAbsolute(absolutePath)) return;
|
|
148
|
+
getFileReadCache(session).recordContiguous(absolutePath, 1, context.fullText.split("\n"), {
|
|
149
|
+
fullText: context.fullText,
|
|
150
|
+
fileHash: context.fileHash,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
116
154
|
function formatTextWithMode(
|
|
117
155
|
text: string,
|
|
118
156
|
startNum: number,
|
|
119
157
|
shouldAddHashLines: boolean,
|
|
120
158
|
shouldAddLineNumbers: boolean,
|
|
121
|
-
truncatedLines?: ReadonlySet<number>,
|
|
122
159
|
): string {
|
|
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
|
-
}
|
|
160
|
+
if (shouldAddHashLines) return formatNumberedLines(text, startNum);
|
|
137
161
|
if (shouldAddLineNumbers) return prependLineNumbers(text, startNum);
|
|
138
162
|
return text;
|
|
139
163
|
}
|
|
@@ -164,7 +188,7 @@ function formatSingleLine(
|
|
|
164
188
|
shouldAddHashLines: boolean,
|
|
165
189
|
shouldAddLineNumbers: boolean,
|
|
166
190
|
): string {
|
|
167
|
-
if (shouldAddHashLines) return
|
|
191
|
+
if (shouldAddHashLines) return formatNumberedLine(line, text);
|
|
168
192
|
if (shouldAddLineNumbers) return `${line}|${text}`;
|
|
169
193
|
return text;
|
|
170
194
|
}
|
|
@@ -179,9 +203,7 @@ function formatMergedBraceLine(
|
|
|
179
203
|
): { model: string; display: string } {
|
|
180
204
|
const merged = `${headText.trimEnd()} .. ${tailText.trim()}`;
|
|
181
205
|
if (shouldAddHashLines) {
|
|
182
|
-
|
|
183
|
-
const end = formatLineHash(endLine, tailText);
|
|
184
|
-
return { model: `${start}-${end}${HL_BODY_SEP}${merged}`, display: merged };
|
|
206
|
+
return { model: `${startLine}-${endLine}:${merged}`, display: merged };
|
|
185
207
|
}
|
|
186
208
|
if (shouldAddLineNumbers) {
|
|
187
209
|
return { model: `${startLine}-${endLine}|${merged}`, display: merged };
|
|
@@ -194,17 +216,38 @@ function countTextLines(text: string): number {
|
|
|
194
216
|
return text.split("\n").length;
|
|
195
217
|
}
|
|
196
218
|
|
|
219
|
+
/** Inclusive line range describing one elided span in a structural summary. */
|
|
220
|
+
interface ElidedRange {
|
|
221
|
+
start: number;
|
|
222
|
+
end: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Sample ranges shown in the footer to demonstrate the multi-range syntax. */
|
|
226
|
+
const FOOTER_RANGE_SAMPLES = 2;
|
|
227
|
+
|
|
197
228
|
/**
|
|
198
229
|
* Footer appended to summarized reads telling the model how to recover the
|
|
199
230
|
* elided body. Without this hint, agents either ignore the `...`/`{ .. }`
|
|
200
|
-
* markers or burn a turn guessing the right selector (see issue #1046).
|
|
231
|
+
* markers or burn a turn guessing the right selector (see issue #1046). The
|
|
232
|
+
* footer demonstrates the multi-range selector syntax with concrete sample
|
|
233
|
+
* ranges drawn from the actual elision so the model re-reads only what it
|
|
234
|
+
* needs instead of falling back to `:raw` or whole-file reads.
|
|
201
235
|
*/
|
|
202
|
-
function formatSummaryElisionFooter(
|
|
203
|
-
|
|
204
|
-
|
|
236
|
+
function formatSummaryElisionFooter(
|
|
237
|
+
readPath: string,
|
|
238
|
+
elidedRanges: ReadonlyArray<ElidedRange>,
|
|
239
|
+
elidedLines: number,
|
|
240
|
+
): string {
|
|
241
|
+
if (elidedRanges.length === 0) return "";
|
|
205
242
|
const lineWord = elidedLines === 1 ? "line" : "lines";
|
|
206
|
-
const
|
|
207
|
-
|
|
243
|
+
const sampleCount = Math.min(elidedRanges.length, FOOTER_RANGE_SAMPLES);
|
|
244
|
+
const selector = elidedRanges
|
|
245
|
+
.slice(0, sampleCount)
|
|
246
|
+
.map(r => `${r.start}-${r.end}`)
|
|
247
|
+
.join(",");
|
|
248
|
+
const example = `${readPath}:${selector}`;
|
|
249
|
+
const tail = elidedRanges.length > sampleCount ? `, e.g. ${example}` : ` with ${example}`;
|
|
250
|
+
return `[${elidedLines} ${lineWord} elided; re-read needed ranges${tail}]`;
|
|
208
251
|
}
|
|
209
252
|
const READ_CHUNK_SIZE = 8 * 1024;
|
|
210
253
|
|
|
@@ -858,9 +901,18 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
858
901
|
|
|
859
902
|
const shouldAddHashLines = displayMode.hashLines;
|
|
860
903
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
904
|
+
const hashContext =
|
|
905
|
+
shouldAddHashLines && options.sourcePath
|
|
906
|
+
? buildHashlineHeaderContext(formatPathRelativeToCwd(options.sourcePath, this.session.cwd), text)
|
|
907
|
+
: undefined;
|
|
908
|
+
recordHashlineSnapshot(this.session, options.sourcePath, hashContext);
|
|
909
|
+
let emittedHashlineHeader = false;
|
|
861
910
|
const formatText = (content: string, startNum: number): string => {
|
|
862
911
|
details.displayContent = { text: content, startLine: startNum };
|
|
863
|
-
|
|
912
|
+
const formatted = formatTextWithMode(content, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
913
|
+
if (!hashContext || emittedHashlineHeader) return formatted;
|
|
914
|
+
emittedHashlineHeader = true;
|
|
915
|
+
return prependHashlineHeader(formatted, hashContext);
|
|
864
916
|
};
|
|
865
917
|
|
|
866
918
|
let outputText: string;
|
|
@@ -876,7 +928,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
876
928
|
if (shouldAddHashLines) {
|
|
877
929
|
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
878
930
|
firstLineBytes,
|
|
879
|
-
)}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot
|
|
931
|
+
)}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot emit an editable numbered preview for a truncated line.]`;
|
|
880
932
|
} else {
|
|
881
933
|
outputText = formatText(snippet.text, startLineDisplay);
|
|
882
934
|
}
|
|
@@ -942,6 +994,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
942
994
|
const totalLines = allLines.length;
|
|
943
995
|
const shouldAddHashLines = displayMode.hashLines;
|
|
944
996
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
997
|
+
const hashContext =
|
|
998
|
+
shouldAddHashLines && options.sourcePath
|
|
999
|
+
? buildHashlineHeaderContext(formatPathRelativeToCwd(options.sourcePath, this.session.cwd), text)
|
|
1000
|
+
: undefined;
|
|
1001
|
+
recordHashlineSnapshot(this.session, options.sourcePath, hashContext);
|
|
1002
|
+
let emittedHashlineHeader = false;
|
|
945
1003
|
|
|
946
1004
|
const resultBuilder = toolResult(details);
|
|
947
1005
|
if (options.sourcePath) resultBuilder.sourcePath(options.sourcePath);
|
|
@@ -957,7 +1015,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
957
1015
|
}
|
|
958
1016
|
const effectiveEnd = Math.min(range.endLine ?? totalLines, totalLines);
|
|
959
1017
|
const sliced = allLines.slice(range.startLine - 1, effectiveEnd).join("\n");
|
|
960
|
-
|
|
1018
|
+
const formatted = formatTextWithMode(sliced, range.startLine, shouldAddHashLines, shouldAddLineNumbers);
|
|
1019
|
+
parts.push(hashContext && !emittedHashlineHeader ? prependHashlineHeader(formatted, hashContext) : formatted);
|
|
1020
|
+
if (hashContext) emittedHashlineHeader = true;
|
|
961
1021
|
}
|
|
962
1022
|
|
|
963
1023
|
const outputText = parts.length > 0 ? parts.join("\n\n…\n\n") : "";
|
|
@@ -1016,6 +1076,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1016
1076
|
|
|
1017
1077
|
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1018
1078
|
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1079
|
+
const hashContext = shouldAddHashLines
|
|
1080
|
+
? await readHashlineHeaderContext(absolutePath, this.session.cwd)
|
|
1081
|
+
: undefined;
|
|
1082
|
+
recordHashlineSnapshot(this.session, absolutePath, hashContext);
|
|
1083
|
+
let emittedHashlineHeader = false;
|
|
1019
1084
|
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
1020
1085
|
|
|
1021
1086
|
const blocks: string[] = [];
|
|
@@ -1045,32 +1110,29 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1045
1110
|
}
|
|
1046
1111
|
|
|
1047
1112
|
const collectedLines = streamResult.lines;
|
|
1048
|
-
const truncatedLineNumbers = new Set<number>();
|
|
1049
1113
|
if (!rawSelector && maxColumns > 0) {
|
|
1050
1114
|
for (let i = 0; i < collectedLines.length; i++) {
|
|
1051
1115
|
const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
|
|
1052
1116
|
if (wasTruncated) {
|
|
1053
1117
|
collectedLines[i] = text;
|
|
1054
1118
|
columnTruncated = maxColumns;
|
|
1055
|
-
truncatedLineNumbers.add(range.startLine + i);
|
|
1056
1119
|
}
|
|
1057
1120
|
}
|
|
1058
1121
|
}
|
|
1059
1122
|
|
|
1060
1123
|
if (collectedLines.length > 0) {
|
|
1061
|
-
getFileReadCache(this.session).recordContiguous(
|
|
1124
|
+
getFileReadCache(this.session).recordContiguous(
|
|
1125
|
+
absolutePath,
|
|
1126
|
+
range.startLine,
|
|
1127
|
+
collectedLines,
|
|
1128
|
+
hashContext ? { fullText: hashContext.fullText, fileHash: hashContext.fileHash } : {},
|
|
1129
|
+
);
|
|
1062
1130
|
}
|
|
1063
1131
|
|
|
1064
1132
|
const blockText = collectedLines.join("\n");
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
range.startLine,
|
|
1069
|
-
shouldAddHashLines,
|
|
1070
|
-
shouldAddLineNumbers,
|
|
1071
|
-
truncatedLineNumbers,
|
|
1072
|
-
),
|
|
1073
|
-
);
|
|
1133
|
+
const formatted = formatTextWithMode(blockText, range.startLine, shouldAddHashLines, shouldAddLineNumbers);
|
|
1134
|
+
blocks.push(hashContext && !emittedHashlineHeader ? prependHashlineHeader(formatted, hashContext) : formatted);
|
|
1135
|
+
if (hashContext) emittedHashlineHeader = true;
|
|
1074
1136
|
}
|
|
1075
1137
|
|
|
1076
1138
|
let outputText = blocks.join("\n\n…\n\n");
|
|
@@ -1359,7 +1421,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1359
1421
|
#renderSummary(summary: SummaryResult): {
|
|
1360
1422
|
text: string;
|
|
1361
1423
|
displayText: string;
|
|
1362
|
-
|
|
1424
|
+
elidedRanges: ElidedRange[];
|
|
1363
1425
|
elidedLines: number;
|
|
1364
1426
|
} {
|
|
1365
1427
|
const displayMode = resolveFileDisplayMode(this.session);
|
|
@@ -1420,13 +1482,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1420
1482
|
|
|
1421
1483
|
const modelParts: string[] = [];
|
|
1422
1484
|
const displayParts: string[] = [];
|
|
1423
|
-
|
|
1485
|
+
const elidedRanges: ElidedRange[] = [];
|
|
1424
1486
|
let elidedLines = 0;
|
|
1425
1487
|
for (const unit of units) {
|
|
1426
1488
|
if (unit.kind === "elided") {
|
|
1427
1489
|
modelParts.push("...");
|
|
1428
1490
|
displayParts.push("...");
|
|
1429
|
-
|
|
1491
|
+
elidedRanges.push({ start: unit.startLine, end: unit.endLine });
|
|
1430
1492
|
elidedLines += unit.endLine - unit.startLine + 1;
|
|
1431
1493
|
continue;
|
|
1432
1494
|
}
|
|
@@ -1441,7 +1503,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1441
1503
|
);
|
|
1442
1504
|
modelParts.push(formatted.model);
|
|
1443
1505
|
displayParts.push(formatted.display);
|
|
1444
|
-
|
|
1506
|
+
// Suggest the full brace range so re-reading shows both braces
|
|
1507
|
+
// plus the elided body in one shot.
|
|
1508
|
+
elidedRanges.push({ start: unit.startLine, end: unit.endLine });
|
|
1445
1509
|
// Merged brace pair encloses (start+1)..(end-1) as elided.
|
|
1446
1510
|
elidedLines += Math.max(0, unit.endLine - unit.startLine - 1);
|
|
1447
1511
|
continue;
|
|
@@ -1450,7 +1514,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1450
1514
|
displayParts.push(unit.text);
|
|
1451
1515
|
}
|
|
1452
1516
|
|
|
1453
|
-
return { text: modelParts.join("\n"), displayText: displayParts.join("\n"),
|
|
1517
|
+
return { text: modelParts.join("\n"), displayText: displayParts.join("\n"), elidedRanges, elidedLines };
|
|
1454
1518
|
}
|
|
1455
1519
|
|
|
1456
1520
|
async execute(
|
|
@@ -1698,15 +1762,20 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1698
1762
|
const renderedSummary = this.#renderSummary(summary);
|
|
1699
1763
|
const footer = formatSummaryElisionFooter(
|
|
1700
1764
|
localReadPath,
|
|
1701
|
-
renderedSummary.
|
|
1765
|
+
renderedSummary.elidedRanges,
|
|
1702
1766
|
renderedSummary.elidedLines,
|
|
1703
1767
|
);
|
|
1704
|
-
const
|
|
1768
|
+
const summaryHashContext = displayMode.hashLines
|
|
1769
|
+
? await readHashlineHeaderContext(absolutePath, this.session.cwd)
|
|
1770
|
+
: undefined;
|
|
1771
|
+
recordHashlineSnapshot(this.session, absolutePath, summaryHashContext);
|
|
1772
|
+
const bodyText = footer ? `${renderedSummary.text}\n\n${footer}` : renderedSummary.text;
|
|
1773
|
+
const modelText = prependHashlineHeader(bodyText, summaryHashContext);
|
|
1705
1774
|
details = {
|
|
1706
1775
|
displayContent: { text: renderedSummary.displayText, startLine: 1 },
|
|
1707
1776
|
summary: {
|
|
1708
1777
|
lines: countTextLines(renderedSummary.text),
|
|
1709
|
-
elidedSpans: renderedSummary.
|
|
1778
|
+
elidedSpans: renderedSummary.elidedRanges.length,
|
|
1710
1779
|
elidedLines: renderedSummary.elidedLines,
|
|
1711
1780
|
},
|
|
1712
1781
|
};
|
|
@@ -1814,14 +1883,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1814
1883
|
// view — column truncation surfaces separately via `.limits()`.
|
|
1815
1884
|
const rawSelector = isRawSelector(parsed);
|
|
1816
1885
|
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
1817
|
-
const truncatedLineNumbers = new Set<number>();
|
|
1818
1886
|
if (!rawSelector && maxColumns > 0) {
|
|
1819
1887
|
for (let i = 0; i < collectedLines.length; i++) {
|
|
1820
1888
|
const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
|
|
1821
1889
|
if (wasTruncated) {
|
|
1822
1890
|
collectedLines[i] = text;
|
|
1823
1891
|
columnTruncated = maxColumns;
|
|
1824
|
-
truncatedLineNumbers.add(startLineDisplay + i);
|
|
1825
1892
|
}
|
|
1826
1893
|
}
|
|
1827
1894
|
}
|
|
@@ -1846,22 +1913,29 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1846
1913
|
firstLineExceedsLimit,
|
|
1847
1914
|
};
|
|
1848
1915
|
|
|
1916
|
+
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1917
|
+
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1918
|
+
const hashContext = shouldAddHashLines
|
|
1919
|
+
? await readHashlineHeaderContext(absolutePath, this.session.cwd)
|
|
1920
|
+
: undefined;
|
|
1921
|
+
|
|
1849
1922
|
if (collectedLines.length > 0 && !firstLineExceedsLimit) {
|
|
1850
|
-
getFileReadCache(this.session).recordContiguous(
|
|
1923
|
+
getFileReadCache(this.session).recordContiguous(
|
|
1924
|
+
absolutePath,
|
|
1925
|
+
startLineDisplay,
|
|
1926
|
+
collectedLines,
|
|
1927
|
+
hashContext ? { fullText: hashContext.fullText, fileHash: hashContext.fileHash } : {},
|
|
1928
|
+
);
|
|
1851
1929
|
}
|
|
1852
1930
|
|
|
1853
|
-
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1854
|
-
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1855
1931
|
let capturedDisplayContent: { text: string; startLine: number } | undefined;
|
|
1932
|
+
let emittedHashlineHeader = false;
|
|
1856
1933
|
const formatText = (text: string, startNum: number): string => {
|
|
1857
1934
|
capturedDisplayContent = { text, startLine: startNum };
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
shouldAddLineNumbers,
|
|
1863
|
-
truncatedLineNumbers,
|
|
1864
|
-
);
|
|
1935
|
+
const formatted = formatTextWithMode(text, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
1936
|
+
if (!hashContext || emittedHashlineHeader) return formatted;
|
|
1937
|
+
emittedHashlineHeader = true;
|
|
1938
|
+
return prependHashlineHeader(formatted, hashContext);
|
|
1865
1939
|
};
|
|
1866
1940
|
|
|
1867
1941
|
let outputText: string;
|
|
@@ -1873,7 +1947,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1873
1947
|
if (shouldAddHashLines) {
|
|
1874
1948
|
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
1875
1949
|
firstLineBytes,
|
|
1876
|
-
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Hashline output requires full lines; cannot
|
|
1950
|
+
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Hashline output requires full lines; cannot emit an editable numbered preview for a truncated line.]`;
|
|
1877
1951
|
} else {
|
|
1878
1952
|
outputText = formatText(snippet.text, startLineDisplay);
|
|
1879
1953
|
}
|
|
@@ -1996,7 +2070,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1996
2070
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1997
2071
|
|
|
1998
2072
|
const rawText = region.lines.join("\n");
|
|
1999
|
-
const
|
|
2073
|
+
const hashContext = shouldAddHashLines
|
|
2074
|
+
? await readHashlineHeaderContext(entry.absolutePath, this.session.cwd)
|
|
2075
|
+
: undefined;
|
|
2076
|
+
recordHashlineSnapshot(this.session, entry.absolutePath, hashContext);
|
|
2077
|
+
const formattedBody = formatTextWithMode(rawText, region.startLine, shouldAddHashLines, shouldAddLineNumbers);
|
|
2078
|
+
const formattedText = prependHashlineHeader(formattedBody, hashContext);
|
|
2000
2079
|
|
|
2001
2080
|
const details: ReadToolDetails = {
|
|
2002
2081
|
resolvedPath: entry.absolutePath,
|