@oh-my-pi/pi-coding-agent 15.5.12 → 15.5.15
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 +46 -0
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +1 -10
- package/dist/types/edit/file-snapshot-store.d.ts +19 -0
- package/dist/types/eval/__tests__/llm-bridge.test.d.ts +1 -0
- package/dist/types/eval/llm-bridge.d.ts +25 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +15 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/tools/index.d.ts +0 -1
- package/package.json +8 -8
- package/src/config/model-registry.ts +89 -5
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +1 -10
- package/src/edit/file-snapshot-store.ts +34 -0
- package/src/edit/hashline/diff.ts +3 -8
- package/src/edit/renderer.ts +1 -1
- package/src/eval/__tests__/llm-bridge.test.ts +297 -0
- package/src/eval/js/shared/prelude.txt +8 -0
- package/src/eval/js/tool-bridge.ts +4 -0
- package/src/eval/llm-bridge.ts +181 -0
- package/src/eval/py/prelude.py +52 -31
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -13
- package/src/extensibility/plugins/legacy-pi-compat.ts +60 -23
- package/src/internal-urls/docs-index.generated.ts +4 -5
- package/src/main.ts +4 -0
- package/src/modes/components/model-selector.ts +119 -22
- package/src/modes/components/status-line/presets.ts +1 -0
- package/src/modes/components/status-line/segments.ts +23 -0
- package/src/modes/interactive-mode.ts +22 -87
- package/src/modes/theme/theme.ts +7 -0
- package/src/prompts/tools/eval.md +2 -0
- package/src/session/agent-session.ts +19 -0
- package/src/session/session-manager.ts +47 -0
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/ast-grep.ts +6 -17
- package/src/tools/eval.ts +24 -48
- package/src/tools/index.ts +0 -4
- package/src/tools/read.ts +23 -33
- package/src/tools/renderers.ts +0 -2
- package/src/tools/search.ts +12 -21
- package/src/tools/write.ts +1 -3
- package/src/utils/file-mentions.ts +1 -3
- package/dist/types/tools/calculator.d.ts +0 -77
- package/src/prompts/tools/calculator.md +0 -10
- package/src/tools/calculator.ts +0 -541
|
@@ -702,6 +702,53 @@ export function buildSessionContext(
|
|
|
702
702
|
}
|
|
703
703
|
}
|
|
704
704
|
|
|
705
|
+
// Strip dangling tool_use blocks — a tool_use with no matching tool_result on the
|
|
706
|
+
// resolved leaf→root path — from ANY assistant turn, not just the trailing one.
|
|
707
|
+
// This happens whenever the leaf (or a branch point) lands such that an assistant
|
|
708
|
+
// turn's tool results are off the selected path: its result children live on a
|
|
709
|
+
// sibling branch, or it is the leaf itself (results are children below it). Left
|
|
710
|
+
// in place, `transformMessages` fabricates one synthetic "aborted"/"No result
|
|
711
|
+
// provided" result per dangling call plus a `<turn-aborted>` developer note, which
|
|
712
|
+
// render as phantom failed calls and re-inject the failed batch into the model's
|
|
713
|
+
// context — the rewind/restore loop.
|
|
714
|
+
//
|
|
715
|
+
// Stripping is necessary but not sufficient: a *modified* assistant turn that still
|
|
716
|
+
// carries signed `thinking`/`redacted_thinking` is rejected by Anthropic — "thinking
|
|
717
|
+
// blocks in the latest assistant message cannot be modified", and signed thinking
|
|
718
|
+
// replayed out of its original turn shape can also fail signature validation (this
|
|
719
|
+
// bites the handoff/branch-summary request). So when we rewrite a turn we also
|
|
720
|
+
// neutralize its protected reasoning: drop `redactedThinking` (encrypted, no
|
|
721
|
+
// plaintext to keep) and clear `thinking` signatures so the provider encoder
|
|
722
|
+
// downgrades them to plain text (verified accepted by the live API), preserving the
|
|
723
|
+
// visible reasoning while removing the immutability/invalid-signature hazard. Drop a
|
|
724
|
+
// turn left with no content. (Live turns never qualify: their results are persisted
|
|
725
|
+
// on the same path before any context rebuild.)
|
|
726
|
+
const pairedToolResultIds = new Set<string>();
|
|
727
|
+
for (const message of messages) {
|
|
728
|
+
if (message.role === "toolResult") pairedToolResultIds.add(message.toolCallId);
|
|
729
|
+
}
|
|
730
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
731
|
+
const message = messages[i];
|
|
732
|
+
if (message.role !== "assistant") continue;
|
|
733
|
+
const hasDangling = message.content.some(
|
|
734
|
+
block => block.type === "toolCall" && !pairedToolResultIds.has(block.id),
|
|
735
|
+
);
|
|
736
|
+
if (!hasDangling) continue;
|
|
737
|
+
const normalized = message.content
|
|
738
|
+
.filter(
|
|
739
|
+
block =>
|
|
740
|
+
!(block.type === "toolCall" && !pairedToolResultIds.has(block.id)) && block.type !== "redactedThinking",
|
|
741
|
+
)
|
|
742
|
+
.map(block =>
|
|
743
|
+
block.type === "thinking" && block.thinkingSignature ? { ...block, thinkingSignature: undefined } : block,
|
|
744
|
+
);
|
|
745
|
+
if (normalized.length === 0) {
|
|
746
|
+
messages.splice(i, 1);
|
|
747
|
+
} else {
|
|
748
|
+
messages[i] = { ...message, content: normalized };
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
705
752
|
return {
|
|
706
753
|
messages,
|
|
707
754
|
thinkingLevel,
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -290,7 +290,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
290
290
|
const absolutePath = path.resolve(this.session.cwd, relativePath);
|
|
291
291
|
try {
|
|
292
292
|
const fullText = normalizeToLF(await Bun.file(absolutePath).text());
|
|
293
|
-
const tag = snapshotStore.
|
|
293
|
+
const tag = snapshotStore.record(absolutePath, fullText);
|
|
294
294
|
hashContexts.set(relativePath, { tag });
|
|
295
295
|
} catch {
|
|
296
296
|
// Best-effort: if a file disappears between ast-edit and rendering, emit plain line output.
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { constants } from "node:fs";
|
|
2
|
-
import { access } from "node:fs/promises";
|
|
3
1
|
import * as path from "node:path";
|
|
4
2
|
import { formatHashlineHeader } from "@oh-my-pi/hashline";
|
|
5
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
@@ -8,7 +6,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
8
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
9
7
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
8
|
import * as z from "zod/v4";
|
|
11
|
-
import {
|
|
9
|
+
import { recordFileSnapshot } from "../edit/file-snapshot-store";
|
|
12
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
11
|
import type { Theme } from "../modes/theme/theme";
|
|
14
12
|
import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
|
|
@@ -221,17 +219,14 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
221
219
|
}
|
|
222
220
|
|
|
223
221
|
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
224
|
-
const hashContexts = new Map<string, {
|
|
225
|
-
const snapshotStore = useHashLines ? getFileSnapshotStore(this.session) : undefined;
|
|
222
|
+
const hashContexts = new Map<string, { tag: string }>();
|
|
226
223
|
if (useHashLines) {
|
|
227
224
|
for (const relativePath of fileList) {
|
|
228
225
|
const absolutePath = path.resolve(this.session.cwd, relativePath);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// Best-effort: if a file disappears between ast-grep and rendering, emit plain line output.
|
|
234
|
-
}
|
|
226
|
+
// Whole-file content tag: any anchor validates while the file is
|
|
227
|
+
// unchanged; over-cap / unreadable files get no tag (plain output).
|
|
228
|
+
const tag = await recordFileSnapshot(this.session, absolutePath);
|
|
229
|
+
if (tag) hashContexts.set(relativePath, { tag });
|
|
235
230
|
}
|
|
236
231
|
}
|
|
237
232
|
const outputLines: string[] = [];
|
|
@@ -246,7 +241,6 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
246
241
|
const endLine = match.startLine + lineCount - 1;
|
|
247
242
|
return Math.max(width, String(match.startLine).length, String(endLine).length);
|
|
248
243
|
}, 0);
|
|
249
|
-
const cacheEntries: Array<readonly [number, string]> = [];
|
|
250
244
|
for (const match of fileMatches) {
|
|
251
245
|
const matchLines = match.text.split("\n");
|
|
252
246
|
for (let index = 0; index < matchLines.length; index++) {
|
|
@@ -257,7 +251,6 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
257
251
|
formatMatchLine(lineNumber, line, isMatch, { useHashLines: hashContext !== undefined }),
|
|
258
252
|
);
|
|
259
253
|
displayOut.push(formatCodeFrameLine(isMatch ? "*" : " ", lineNumber, line, lineNumberWidth));
|
|
260
|
-
cacheEntries.push([lineNumber, line] as const);
|
|
261
254
|
}
|
|
262
255
|
if (match.metaVariables && Object.keys(match.metaVariables).length > 0) {
|
|
263
256
|
const serializedMeta = Object.entries(match.metaVariables)
|
|
@@ -269,10 +262,6 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
269
262
|
}
|
|
270
263
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
271
264
|
}
|
|
272
|
-
if (hashContext && cacheEntries.length > 0) {
|
|
273
|
-
const tag = snapshotStore?.recordSparse(hashContext.absolutePath, cacheEntries);
|
|
274
|
-
if (tag) hashContext.tag = tag;
|
|
275
|
-
}
|
|
276
265
|
return { model: modelOut, display: displayOut };
|
|
277
266
|
};
|
|
278
267
|
|
package/src/tools/eval.ts
CHANGED
|
@@ -13,10 +13,19 @@ import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
|
13
13
|
import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
14
14
|
import evalDescription from "../prompts/tools/eval.md" with { type: "text" };
|
|
15
15
|
import { DEFAULT_MAX_BYTES, OutputSink, type OutputSummary, TailBuffer } from "../session/streaming-output";
|
|
16
|
-
import {
|
|
16
|
+
import { renderCodeCell } from "../tui";
|
|
17
17
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
18
18
|
import { resolveEvalBackends, type ToolSession } from ".";
|
|
19
19
|
import { truncateForPrompt } from "./approval";
|
|
20
|
+
import {
|
|
21
|
+
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
22
|
+
JSON_TREE_MAX_DEPTH_EXPANDED,
|
|
23
|
+
JSON_TREE_MAX_LINES_COLLAPSED,
|
|
24
|
+
JSON_TREE_MAX_LINES_EXPANDED,
|
|
25
|
+
JSON_TREE_SCALAR_LEN_COLLAPSED,
|
|
26
|
+
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
27
|
+
renderJsonTreeLines,
|
|
28
|
+
} from "./json-tree";
|
|
20
29
|
import {
|
|
21
30
|
formatStyledTruncationWarning,
|
|
22
31
|
resolveOutputMaxColumns,
|
|
@@ -61,15 +70,6 @@ export type EvalToolResult = {
|
|
|
61
70
|
|
|
62
71
|
export type EvalProxyExecutor = (params: EvalToolParams, signal?: AbortSignal) => Promise<EvalToolResult>;
|
|
63
72
|
|
|
64
|
-
function formatJsonScalar(value: unknown): string {
|
|
65
|
-
if (value === null) return "null";
|
|
66
|
-
if (value === undefined) return "undefined";
|
|
67
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
68
|
-
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
69
|
-
if (typeof value === "function") return "[function]";
|
|
70
|
-
return "[object]";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
73
|
/** Cap per `display()` value sent back to the model. */
|
|
74
74
|
const MAX_DISPLAY_TEXT_BYTES = 8000;
|
|
75
75
|
|
|
@@ -102,41 +102,6 @@ function formatDisplayOutputsForText(outputs: EvalDisplayOutput[]): string {
|
|
|
102
102
|
return chunks.join("\n\n");
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function renderJsonTree(value: unknown, theme: Theme, expanded: boolean, maxDepth = expanded ? 6 : 2): string[] {
|
|
106
|
-
const maxItems = expanded ? 20 : 5;
|
|
107
|
-
|
|
108
|
-
const renderNode = (node: unknown, prefix: string, depth: number, isLast: boolean, label?: string): string[] => {
|
|
109
|
-
const branch = getTreeBranch(isLast, theme);
|
|
110
|
-
const displayLabel = label ? `${label}: ` : "";
|
|
111
|
-
|
|
112
|
-
if (depth >= maxDepth || node === null || typeof node !== "object") {
|
|
113
|
-
return [`${prefix}${branch} ${displayLabel}${formatJsonScalar(node)}`];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const isArray = Array.isArray(node);
|
|
117
|
-
const entries = isArray
|
|
118
|
-
? node.map((val, index) => [String(index), val] as const)
|
|
119
|
-
: Object.entries(node as object);
|
|
120
|
-
const header = `${prefix}${branch} ${displayLabel}${isArray ? `Array(${entries.length})` : `Object(${entries.length})`}`;
|
|
121
|
-
const lines = [header];
|
|
122
|
-
|
|
123
|
-
const childPrefix = prefix + getTreeContinuePrefix(isLast, theme);
|
|
124
|
-
const visible = entries.slice(0, maxItems);
|
|
125
|
-
for (let i = 0; i < visible.length; i++) {
|
|
126
|
-
const [key, val] = visible[i];
|
|
127
|
-
const childLast = i === visible.length - 1 && (expanded || entries.length <= maxItems);
|
|
128
|
-
lines.push(...renderNode(val, childPrefix, depth + 1, childLast, isArray ? `[${key}]` : key));
|
|
129
|
-
}
|
|
130
|
-
if (!expanded && entries.length > maxItems) {
|
|
131
|
-
const moreBranch = theme.tree.last;
|
|
132
|
-
lines.push(`${childPrefix}${moreBranch} ${entries.length - maxItems} more item(s)`);
|
|
133
|
-
}
|
|
134
|
-
return lines;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
return renderNode(value, "", 0, true);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
105
|
export interface EvalToolDescriptionOptions {
|
|
141
106
|
py?: boolean;
|
|
142
107
|
js?: boolean;
|
|
@@ -669,6 +634,7 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
|
|
|
669
634
|
sh: "icon.package",
|
|
670
635
|
env: "icon.package",
|
|
671
636
|
batch: "icon.package",
|
|
637
|
+
llm: "icon.package",
|
|
672
638
|
};
|
|
673
639
|
|
|
674
640
|
const iconKey = opIcons[op] ?? "icon.file";
|
|
@@ -735,6 +701,11 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
|
|
|
735
701
|
case "batch":
|
|
736
702
|
parts.push(`${data.files} file${(data.files as number) !== 1 ? "s" : ""} processed`);
|
|
737
703
|
break;
|
|
704
|
+
case "llm":
|
|
705
|
+
if (data.model) parts.push(String(data.model));
|
|
706
|
+
if (data.tier && data.tier !== data.model) parts.push(`(${data.tier})`);
|
|
707
|
+
parts.push(`${data.chars ?? 0} chars`);
|
|
708
|
+
break;
|
|
738
709
|
case "wc":
|
|
739
710
|
parts.push(`${data.lines}L ${data.words}W ${data.chars}C`);
|
|
740
711
|
break;
|
|
@@ -950,10 +921,15 @@ export const evalToolRenderer = {
|
|
|
950
921
|
const output = stripOutputNotice(rawOutput, details?.meta).trimEnd();
|
|
951
922
|
|
|
952
923
|
const jsonOutputs = details?.jsonOutputs ?? [];
|
|
924
|
+
const treeExpanded = options.renderContext?.expanded ?? options.expanded;
|
|
925
|
+
const treeDepth = treeExpanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
|
|
926
|
+
const treeLineCap = treeExpanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
|
|
927
|
+
const treeScalarLen = treeExpanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
|
|
928
|
+
const labelOutputs = jsonOutputs.length > 1;
|
|
953
929
|
const jsonLines = jsonOutputs.flatMap((value, index) => {
|
|
954
|
-
const
|
|
955
|
-
const
|
|
956
|
-
return [
|
|
930
|
+
const tree = renderJsonTreeLines(value, uiTheme, treeDepth, treeLineCap, treeScalarLen);
|
|
931
|
+
const body = tree.truncated ? [...tree.lines, uiTheme.fg("dim", "…")] : tree.lines;
|
|
932
|
+
return labelOutputs ? [uiTheme.fg("dim", `display[${index + 1}]`), ...body] : body;
|
|
957
933
|
});
|
|
958
934
|
|
|
959
935
|
const timeoutSeconds = options.renderContext?.timeout;
|
package/src/tools/index.ts
CHANGED
|
@@ -28,7 +28,6 @@ import { AstEditTool } from "./ast-edit";
|
|
|
28
28
|
import { AstGrepTool } from "./ast-grep";
|
|
29
29
|
import { BashTool } from "./bash";
|
|
30
30
|
import { BrowserTool } from "./browser";
|
|
31
|
-
import { CalculatorTool } from "./calculator";
|
|
32
31
|
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
|
|
33
32
|
import { DebugTool } from "./debug";
|
|
34
33
|
import { EvalTool } from "./eval";
|
|
@@ -69,7 +68,6 @@ export * from "./ast-edit";
|
|
|
69
68
|
export * from "./ast-grep";
|
|
70
69
|
export * from "./bash";
|
|
71
70
|
export * from "./browser";
|
|
72
|
-
export * from "./calculator";
|
|
73
71
|
export * from "./checkpoint";
|
|
74
72
|
export * from "./debug";
|
|
75
73
|
export * from "./eval";
|
|
@@ -286,7 +284,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
286
284
|
ask: AskTool.createIf,
|
|
287
285
|
debug: DebugTool.createIf,
|
|
288
286
|
eval: s => new EvalTool(s),
|
|
289
|
-
calc: s => new CalculatorTool(s),
|
|
290
287
|
ssh: loadSshTool,
|
|
291
288
|
github: GithubTool.createIf,
|
|
292
289
|
find: s => new FindTool(s),
|
|
@@ -455,7 +452,6 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
455
452
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
456
453
|
// search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
|
|
457
454
|
if (name === "search_tool_bm25") return discoveryActive;
|
|
458
|
-
if (name === "calc") return session.settings.get("calc.enabled");
|
|
459
455
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
460
456
|
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
|
461
457
|
if (name === "irc") {
|
package/src/tools/read.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
9
9
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import { getRemoteDir, logger, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import * as z from "zod/v4";
|
|
12
|
-
import { getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
12
|
+
import { getFileSnapshotStore, recordFileSnapshot } from "../edit/file-snapshot-store";
|
|
13
13
|
import { normalizeToLF } from "../edit/normalize";
|
|
14
14
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
15
15
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -130,9 +130,7 @@ function recordFullHashlineContext(
|
|
|
130
130
|
): HashlineHeaderContext | undefined {
|
|
131
131
|
if (!absolutePath || !path.isAbsolute(absolutePath)) return undefined;
|
|
132
132
|
const normalized = normalizeToLF(fullText);
|
|
133
|
-
const tag = getFileSnapshotStore(session).
|
|
134
|
-
fullText: normalized,
|
|
135
|
-
});
|
|
133
|
+
const tag = getFileSnapshotStore(session).record(absolutePath, normalized);
|
|
136
134
|
return {
|
|
137
135
|
header: formatHashlineHeader(displayPath, tag),
|
|
138
136
|
tag,
|
|
@@ -1033,7 +1031,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1033
1031
|
|
|
1034
1032
|
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1035
1033
|
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1036
|
-
const sparseSnapshotEntries: Array<readonly [number, string]> = [];
|
|
1037
1034
|
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
1038
1035
|
|
|
1039
1036
|
const blocks: string[] = [];
|
|
@@ -1063,10 +1060,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1063
1060
|
}
|
|
1064
1061
|
|
|
1065
1062
|
const collectedLines = streamResult.lines;
|
|
1066
|
-
// Column truncation is display-only
|
|
1067
|
-
//
|
|
1068
|
-
// the live file. Stamping ellipsis-truncated lines into the snapshot makes
|
|
1069
|
-
// every long-line file uneditable on the next edit attempt.
|
|
1063
|
+
// Column truncation is display-only; clone before stamping ellipsis so
|
|
1064
|
+
// the original on-disk lines stay intact for display reconstruction.
|
|
1070
1065
|
let displayLines: string[] = collectedLines;
|
|
1071
1066
|
if (!rawSelector && maxColumns > 0) {
|
|
1072
1067
|
let cloned: string[] | undefined;
|
|
@@ -1080,19 +1075,16 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1080
1075
|
}
|
|
1081
1076
|
if (cloned) displayLines = cloned;
|
|
1082
1077
|
}
|
|
1083
|
-
|
|
1084
|
-
for (let index = 0; index < collectedLines.length; index++) {
|
|
1085
|
-
sparseSnapshotEntries.push([range.startLine + index, collectedLines[index]]);
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
1078
|
const blockText = displayLines.join("\n");
|
|
1089
1079
|
blocks.push(formatTextWithMode(blockText, range.startLine, shouldAddHashLines, shouldAddLineNumbers));
|
|
1090
1080
|
}
|
|
1091
1081
|
|
|
1092
1082
|
let outputText = blocks.join("\n\n…\n\n");
|
|
1093
|
-
if (shouldAddHashLines &&
|
|
1094
|
-
const tag =
|
|
1095
|
-
|
|
1083
|
+
if (shouldAddHashLines && outputText) {
|
|
1084
|
+
const tag = await recordFileSnapshot(this.session, absolutePath);
|
|
1085
|
+
if (tag) {
|
|
1086
|
+
outputText = `${formatHashlineHeader(formatPathRelativeToCwd(absolutePath, this.session.cwd), tag)}\n${outputText}`;
|
|
1087
|
+
}
|
|
1096
1088
|
}
|
|
1097
1089
|
if (notices.length > 0) {
|
|
1098
1090
|
outputText = outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n");
|
|
@@ -1905,17 +1897,17 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1905
1897
|
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1906
1898
|
let hashContext: HashlineHeaderContext | undefined;
|
|
1907
1899
|
if (shouldAddHashLines && collectedLines.length > 0 && !firstLineExceedsLimit) {
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1900
|
+
// The tag is a content hash of the WHOLE file. A whole-file read
|
|
1901
|
+
// already holds every line in memory; a range read re-reads the
|
|
1902
|
+
// file (bounded by SNAPSHOT_MAX_BYTES) so the tag fingerprints the
|
|
1903
|
+
// full file and any anchor validates while the file is unchanged.
|
|
1904
|
+
const isWholeFile = offset === undefined && limit === undefined && !wasTruncated;
|
|
1905
|
+
const tag = isWholeFile
|
|
1906
|
+
? getFileSnapshotStore(this.session).record(absolutePath, normalizeToLF(collectedLines.join("\n")))
|
|
1907
|
+
: await recordFileSnapshot(this.session, absolutePath);
|
|
1908
|
+
if (tag) {
|
|
1909
|
+
hashContext = hashlineHeaderContext(formatPathRelativeToCwd(absolutePath, this.session.cwd), tag);
|
|
1910
|
+
}
|
|
1919
1911
|
}
|
|
1920
1912
|
|
|
1921
1913
|
let capturedDisplayContent: { text: string; startLine: number } | undefined;
|
|
@@ -2060,11 +2052,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2060
2052
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
2061
2053
|
|
|
2062
2054
|
const rawText = region.lines.join("\n");
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
getFileSnapshotStore(this.session).recordContiguous(entry.absolutePath, region.startLine, region.lines),
|
|
2067
|
-
)
|
|
2055
|
+
const tag = shouldAddHashLines ? await recordFileSnapshot(this.session, entry.absolutePath) : undefined;
|
|
2056
|
+
const hashContext = tag
|
|
2057
|
+
? hashlineHeaderContext(formatPathRelativeToCwd(entry.absolutePath, this.session.cwd), tag)
|
|
2068
2058
|
: undefined;
|
|
2069
2059
|
const formattedBody = formatTextWithMode(rawText, region.startLine, shouldAddHashLines, shouldAddLineNumbers);
|
|
2070
2060
|
const formattedText = prependHashlineHeader(formattedBody, hashContext);
|
package/src/tools/renderers.ts
CHANGED
|
@@ -16,7 +16,6 @@ import { astEditToolRenderer } from "./ast-edit";
|
|
|
16
16
|
import { astGrepToolRenderer } from "./ast-grep";
|
|
17
17
|
import { bashToolRenderer } from "./bash";
|
|
18
18
|
import { browserToolRenderer } from "./browser/render";
|
|
19
|
-
import { calculatorToolRenderer } from "./calculator";
|
|
20
19
|
import { debugToolRenderer } from "./debug";
|
|
21
20
|
import { evalToolRenderer } from "./eval";
|
|
22
21
|
import { findToolRenderer } from "./find";
|
|
@@ -54,7 +53,6 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
54
53
|
recipe: recipeToolRenderer as ToolRenderer,
|
|
55
54
|
debug: debugToolRenderer as ToolRenderer,
|
|
56
55
|
eval: evalToolRenderer as ToolRenderer,
|
|
57
|
-
calc: calculatorToolRenderer as ToolRenderer,
|
|
58
56
|
edit: editToolRenderer as ToolRenderer,
|
|
59
57
|
apply_patch: editToolRenderer as ToolRenderer,
|
|
60
58
|
find: findToolRenderer as ToolRenderer,
|
package/src/tools/search.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { access, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
3
2
|
import { tmpdir } from "node:os";
|
|
4
3
|
import * as path from "node:path";
|
|
5
4
|
import { formatHashlineHeader } from "@oh-my-pi/hashline";
|
|
@@ -9,7 +8,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
9
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
10
9
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
11
10
|
import * as z from "zod/v4";
|
|
12
|
-
import {
|
|
11
|
+
import { recordFileSnapshot } from "../edit/file-snapshot-store";
|
|
13
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
14
13
|
import type { Theme } from "../modes/theme/theme";
|
|
15
14
|
import searchDescription from "../prompts/tools/search.md" with { type: "text" };
|
|
@@ -610,19 +609,17 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
610
609
|
matchesByFile.get(relativePath)!.push(match);
|
|
611
610
|
}
|
|
612
611
|
const displayLines: string[] = [];
|
|
613
|
-
const hashContexts = new Map<string, {
|
|
614
|
-
const snapshotStore = baseDisplayMode.hashLines ? getFileSnapshotStore(this.session) : undefined;
|
|
612
|
+
const hashContexts = new Map<string, { tag: string }>();
|
|
615
613
|
if (baseDisplayMode.hashLines) {
|
|
616
614
|
for (const relativePath of fileList) {
|
|
617
615
|
if (archiveDisplaySet.has(relativePath)) continue;
|
|
618
616
|
const absoluteFilePath = path.resolve(this.session.cwd, relativePath);
|
|
619
617
|
if (immutableSourcePaths.has(absoluteFilePath)) continue;
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
618
|
+
// Mint a whole-file content tag so any anchor validates while the
|
|
619
|
+
// file is unchanged; over-cap / unreadable files get no tag (and
|
|
620
|
+
// therefore plain, non-editable line output).
|
|
621
|
+
const tag = await recordFileSnapshot(this.session, absoluteFilePath);
|
|
622
|
+
if (tag) hashContexts.set(relativePath, { tag });
|
|
626
623
|
}
|
|
627
624
|
}
|
|
628
625
|
const renderMatchesForFile = (relativePath: string): { model: string[]; display: string[] } => {
|
|
@@ -641,40 +638,34 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
641
638
|
}
|
|
642
639
|
return nextWidth;
|
|
643
640
|
}, 0);
|
|
644
|
-
const cacheEntries: Array<readonly [number, string]> = [];
|
|
645
641
|
let lastEmittedLine: number | undefined;
|
|
646
642
|
const gutterPad = " ".repeat(lineNumberWidth + 1);
|
|
647
643
|
for (const match of fileMatches) {
|
|
648
|
-
const pushLine = (lineNumber: number, line: string, isMatch: boolean
|
|
644
|
+
const pushLine = (lineNumber: number, line: string, isMatch: boolean) => {
|
|
649
645
|
if (lastEmittedLine !== undefined && lineNumber > lastEmittedLine + 1) {
|
|
650
646
|
modelOut.push("...");
|
|
651
647
|
displayOut.push(`${gutterPad}│...`);
|
|
652
648
|
}
|
|
653
649
|
modelOut.push(formatMatchLine(lineNumber, line, isMatch, { useHashLines }));
|
|
654
650
|
displayOut.push(formatCodeFrameLine(isMatch ? "*" : " ", lineNumber, line, lineNumberWidth));
|
|
655
|
-
if (recordable) cacheEntries.push([lineNumber, line] as const);
|
|
656
651
|
lastEmittedLine = lineNumber;
|
|
657
652
|
};
|
|
658
653
|
if (match.contextBefore) {
|
|
659
654
|
for (const ctx of match.contextBefore) {
|
|
660
|
-
pushLine(ctx.lineNumber, ctx.line, false
|
|
655
|
+
pushLine(ctx.lineNumber, ctx.line, false);
|
|
661
656
|
}
|
|
662
657
|
}
|
|
663
|
-
pushLine(match.lineNumber, match.line, true
|
|
658
|
+
pushLine(match.lineNumber, match.line, true);
|
|
664
659
|
if (match.truncated) {
|
|
665
660
|
linesTruncated = true;
|
|
666
661
|
}
|
|
667
662
|
if (match.contextAfter) {
|
|
668
663
|
for (const ctx of match.contextAfter) {
|
|
669
|
-
pushLine(ctx.lineNumber, ctx.line, false
|
|
664
|
+
pushLine(ctx.lineNumber, ctx.line, false);
|
|
670
665
|
}
|
|
671
666
|
}
|
|
672
667
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
673
668
|
}
|
|
674
|
-
if (cacheEntries.length > 0 && hashContext) {
|
|
675
|
-
const tag = snapshotStore?.recordSparse(hashContext.absolutePath, cacheEntries);
|
|
676
|
-
if (tag) hashContext.tag = tag;
|
|
677
|
-
}
|
|
678
669
|
return { model: modelOut, display: displayOut };
|
|
679
670
|
};
|
|
680
671
|
if (isDirectory) {
|
package/src/tools/write.ts
CHANGED
|
@@ -130,9 +130,7 @@ function stripWriteContent(session: ToolSession, content: string): { text: strin
|
|
|
130
130
|
function maybeWriteSnapshotHeader(session: ToolSession, absolutePath: string, content: string): string | undefined {
|
|
131
131
|
if (!resolveFileDisplayMode(session).hashLines) return undefined;
|
|
132
132
|
const normalized = normalizeToLF(content);
|
|
133
|
-
const tag = getFileSnapshotStore(session).
|
|
134
|
-
fullText: normalized,
|
|
135
|
-
});
|
|
133
|
+
const tag = getFileSnapshotStore(session).record(absolutePath, normalized);
|
|
136
134
|
return formatHashlineHeader(formatPathRelativeToCwd(absolutePath, session.cwd), tag);
|
|
137
135
|
}
|
|
138
136
|
|
|
@@ -359,9 +359,7 @@ export async function generateFileMentionMessages(
|
|
|
359
359
|
const normalized = snapshotStore ? normalizeToLF(content) : content;
|
|
360
360
|
let { output, lineCount } = buildTextOutput(normalized);
|
|
361
361
|
if (snapshotStore) {
|
|
362
|
-
const tag = snapshotStore.
|
|
363
|
-
fullText: normalized,
|
|
364
|
-
});
|
|
362
|
+
const tag = snapshotStore.record(absolutePath, normalized);
|
|
365
363
|
output = `${formatHashlineHeader(resolvedPath, tag)}\n${formatNumberedLines(output)}`;
|
|
366
364
|
}
|
|
367
365
|
files.push({ path: resolvedPath, content: output, lineCount });
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import * as z from "zod/v4";
|
|
4
|
-
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
5
|
-
import type { Theme } from "../modes/theme/theme";
|
|
6
|
-
import type { ToolSession } from ".";
|
|
7
|
-
declare const calculatorSchema: z.ZodObject<{
|
|
8
|
-
calculations: z.ZodArray<z.ZodObject<{
|
|
9
|
-
expression: z.ZodString;
|
|
10
|
-
prefix: z.ZodString;
|
|
11
|
-
suffix: z.ZodString;
|
|
12
|
-
}, z.core.$strip>>;
|
|
13
|
-
}, z.core.$strip>;
|
|
14
|
-
export interface CalculatorToolDetails {
|
|
15
|
-
results: Array<{
|
|
16
|
-
expression: string;
|
|
17
|
-
value: number;
|
|
18
|
-
output: string;
|
|
19
|
-
}>;
|
|
20
|
-
}
|
|
21
|
-
type CalculatorParams = z.infer<typeof calculatorSchema>;
|
|
22
|
-
/**
|
|
23
|
-
* Calculator tool for evaluating mathematical expressions.
|
|
24
|
-
*
|
|
25
|
-
* Supports decimal, hex (0x), binary (0b), octal (0o) literals,
|
|
26
|
-
* standard arithmetic operators, and parentheses.
|
|
27
|
-
*/
|
|
28
|
-
export declare class CalculatorTool implements AgentTool<typeof calculatorSchema, CalculatorToolDetails> {
|
|
29
|
-
readonly name = "calc";
|
|
30
|
-
readonly approval: "read";
|
|
31
|
-
readonly label = "Calc";
|
|
32
|
-
readonly summary = "Evaluate a mathematical expression";
|
|
33
|
-
readonly loadMode = "discoverable";
|
|
34
|
-
readonly description: string;
|
|
35
|
-
readonly parameters: z.ZodObject<{
|
|
36
|
-
calculations: z.ZodArray<z.ZodObject<{
|
|
37
|
-
expression: z.ZodString;
|
|
38
|
-
prefix: z.ZodString;
|
|
39
|
-
suffix: z.ZodString;
|
|
40
|
-
}, z.core.$strip>>;
|
|
41
|
-
}, z.core.$strip>;
|
|
42
|
-
readonly strict = true;
|
|
43
|
-
constructor(_session: ToolSession);
|
|
44
|
-
execute(_toolCallId: string, { calculations }: CalculatorParams, signal?: AbortSignal): Promise<AgentToolResult<CalculatorToolDetails>>;
|
|
45
|
-
}
|
|
46
|
-
interface CalculatorRenderArgs {
|
|
47
|
-
calculations?: Array<{
|
|
48
|
-
expression: string;
|
|
49
|
-
prefix?: string;
|
|
50
|
-
suffix?: string;
|
|
51
|
-
}>;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* TUI renderer for calculator tool calls and results.
|
|
55
|
-
* Handles both collapsed (preview) and expanded (full) display modes.
|
|
56
|
-
*/
|
|
57
|
-
export declare const calculatorToolRenderer: {
|
|
58
|
-
/**
|
|
59
|
-
* Render the tool call header showing the first expression and count.
|
|
60
|
-
* Format: "Calc <expression> (N calcs)"
|
|
61
|
-
*/
|
|
62
|
-
renderCall(args: CalculatorRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component;
|
|
63
|
-
/**
|
|
64
|
-
* Render calculation results as a tree list.
|
|
65
|
-
* Collapsed mode shows first N items with expand hint; expanded shows all.
|
|
66
|
-
*/
|
|
67
|
-
renderResult(result: {
|
|
68
|
-
content: Array<{
|
|
69
|
-
type: string;
|
|
70
|
-
text?: string;
|
|
71
|
-
}>;
|
|
72
|
-
details?: CalculatorToolDetails;
|
|
73
|
-
isError?: boolean;
|
|
74
|
-
}, options: RenderResultOptions, uiTheme: Theme, args?: CalculatorRenderArgs): Component;
|
|
75
|
-
mergeCallAndResult: boolean;
|
|
76
|
-
};
|
|
77
|
-
export {};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
Performs basic calculations.
|
|
2
|
-
|
|
3
|
-
<instruction>
|
|
4
|
-
- Supports +, -, *, /, %, ** and parentheses
|
|
5
|
-
- Supports decimal, hex (0x), binary (0b), and octal (0o) literals
|
|
6
|
-
</instruction>
|
|
7
|
-
|
|
8
|
-
<output>
|
|
9
|
-
Returns each calculation result with its prefix and suffix applied.
|
|
10
|
-
</output>
|