@oh-my-pi/pi-coding-agent 15.10.1 → 15.10.3
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 +113 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +7 -1
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/edit/index.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +0 -5
- package/dist/types/main.d.ts +14 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +0 -9
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/late-diagnostics-message.d.ts +20 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-selector.d.ts +16 -7
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +0 -18
- package/dist/types/modes/controllers/event-controller.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/messages.d.ts +11 -8
- package/dist/types/session/session-manager.d.ts +5 -2
- package/dist/types/session/yield-queue.d.ts +10 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/tools/eval-render.d.ts +0 -1
- package/dist/types/tools/eval.d.ts +8 -0
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- package/dist/types/tools/index.d.ts +31 -0
- package/dist/types/tools/path-utils.d.ts +13 -1
- package/dist/types/tools/read.d.ts +2 -1
- package/dist/types/tools/render-utils.d.ts +3 -1
- package/dist/types/tools/renderers.d.ts +0 -15
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/write.d.ts +0 -2
- package/dist/types/tools/yield.d.ts +8 -0
- package/dist/types/tui/code-cell.d.ts +0 -2
- package/dist/types/tui/hyperlink.d.ts +5 -7
- package/dist/types/tui/output-block.d.ts +0 -18
- package/package.json +9 -9
- package/src/cli/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +2 -4
- package/src/cli/gallery-cli.ts +4 -0
- package/src/cli/gallery-fixtures/codeintel.ts +0 -1
- package/src/cli/gallery-fixtures/fs.ts +68 -1
- package/src/cli/gallery-fixtures/types.ts +8 -1
- package/src/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -0
- package/src/commit/agentic/agent.ts +1 -0
- package/src/commit/model-selection.ts +3 -2
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +4 -22
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings.ts +86 -41
- package/src/debug/index.ts +8 -0
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/diff.ts +86 -0
- package/src/edit/hashline/execute.ts +14 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/edit/index.ts +31 -17
- package/src/edit/renderer.ts +116 -31
- package/src/eval/__tests__/llm-bridge.test.ts +20 -0
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/js/shared/prelude.txt +26 -10
- package/src/eval/llm-bridge.ts +14 -3
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +189 -61
- package/src/main.ts +144 -78
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +2 -2
- package/src/modes/components/assistant-message.ts +3 -15
- package/src/modes/components/custom-editor.ts +143 -111
- package/src/modes/components/late-diagnostics-message.ts +60 -0
- package/src/modes/components/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- package/src/modes/components/plan-review-overlay.ts +26 -5
- package/src/modes/components/read-tool-group.ts +415 -35
- package/src/modes/components/session-selector.ts +89 -35
- package/src/modes/components/status-line.ts +19 -4
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/tool-execution.ts +7 -49
- package/src/modes/components/transcript-container.ts +108 -32
- package/src/modes/components/user-message.ts +1 -1
- package/src/modes/controllers/event-controller.ts +32 -1
- package/src/modes/controllers/input-controller.ts +56 -9
- package/src/modes/interactive-mode.ts +107 -20
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/types.ts +7 -0
- package/src/modes/utils/ui-helpers.ts +26 -5
- package/src/modes/workflow.ts +10 -10
- package/src/prompts/system/manual-continue.md +7 -0
- package/src/prompts/system/plan-mode-active.md +56 -72
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +85 -10
- package/src/session/agent-session.ts +42 -15
- package/src/session/auth-storage.ts +2 -0
- package/src/session/messages.ts +21 -14
- package/src/session/session-manager.ts +98 -25
- package/src/session/yield-queue.ts +20 -2
- package/src/task/executor.ts +72 -36
- package/src/task/render.ts +3 -4
- package/src/tiny/title-client.ts +6 -1
- package/src/tools/bash.ts +7 -7
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/eval-render.ts +4 -23
- package/src/tools/eval.ts +13 -2
- package/src/tools/find.ts +148 -99
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/github-cache.ts +25 -0
- package/src/tools/index.ts +32 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/path-utils.ts +47 -24
- package/src/tools/plan-mode-guard.ts +52 -7
- package/src/tools/read.ts +41 -20
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/renderers.ts +0 -15
- package/src/tools/search.ts +38 -3
- package/src/tools/ssh.ts +0 -1
- package/src/tools/todo.ts +1 -0
- package/src/tools/write.ts +5 -14
- package/src/tools/yield.ts +10 -1
- package/src/tui/code-cell.ts +1 -6
- package/src/tui/hyperlink.ts +13 -23
- package/src/tui/output-block.ts +2 -97
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/enhanced-paste.ts +30 -2
- package/src/web/search/providers/codex.ts +37 -8
package/src/tools/index.ts
CHANGED
|
@@ -117,6 +117,29 @@ export type {
|
|
|
117
117
|
DiscoverableToolSource,
|
|
118
118
|
} from "../tool-discovery/tool-index";
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* A late LSP diagnostics result that arrived after the edit/write tool already
|
|
122
|
+
* returned. Surfaced to the model and the transcript via
|
|
123
|
+
* {@link ToolSession.queueDeferredDiagnostics}, batched through the session
|
|
124
|
+
* yield queue like background-job results.
|
|
125
|
+
*/
|
|
126
|
+
export interface DeferredDiagnosticsEntry {
|
|
127
|
+
/** Absolute path the diagnostics belong to (the renderer shortens it). */
|
|
128
|
+
path: string;
|
|
129
|
+
/** One-line severity summary, e.g. "2 errors". */
|
|
130
|
+
summary: string;
|
|
131
|
+
/** Formatted, ready-to-display diagnostic lines. */
|
|
132
|
+
messages: string[];
|
|
133
|
+
/** True when any message is error severity. */
|
|
134
|
+
errored: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Evaluated at injection time (in the dispatcher's stale check): drop the entry
|
|
137
|
+
* when a newer mutation to the same file has superseded it, so the model never
|
|
138
|
+
* sees diagnostics for stale content.
|
|
139
|
+
*/
|
|
140
|
+
isStale(): boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
120
143
|
/** Session context for tool factories */
|
|
121
144
|
export interface ToolSession {
|
|
122
145
|
/** Current working directory */
|
|
@@ -284,6 +307,15 @@ export interface ToolSession {
|
|
|
284
307
|
|
|
285
308
|
/** Queue a hidden message to be injected at the next agent turn. */
|
|
286
309
|
queueDeferredMessage?(message: CustomMessage): void;
|
|
310
|
+
/** Queue late LSP diagnostics (arrived after an edit/write returned) to be shown
|
|
311
|
+
* in the transcript and delivered to the model at the next yield, like background
|
|
312
|
+
* job results. */
|
|
313
|
+
queueDeferredDiagnostics?(entry: DeferredDiagnosticsEntry): void;
|
|
314
|
+
/** Bump and return the session-global mutation counter for `path`. Edit/write
|
|
315
|
+
* tools call this on every file mutation so stale late-diagnostics can be dropped. */
|
|
316
|
+
bumpFileMutationVersion?(path: string): number;
|
|
317
|
+
/** Read the current session-global mutation counter for `path` (0 if never mutated). */
|
|
318
|
+
getFileMutationVersion?(path: string): number;
|
|
287
319
|
/** Get the active OpenTelemetry config so subagent dispatch can forward
|
|
288
320
|
* the parent's tracer/hooks with the subagent's own identity stamped. */
|
|
289
321
|
getTelemetry?: () => AgentTelemetryConfig | undefined;
|
|
@@ -5,7 +5,7 @@ import { prompt } from "@oh-my-pi/pi-utils";
|
|
|
5
5
|
import * as z from "zod/v4";
|
|
6
6
|
import { extractTextContent } from "../commit/utils";
|
|
7
7
|
|
|
8
|
-
import { expandRoleAlias, resolveModelFromString } from "../config/model-resolver";
|
|
8
|
+
import { expandRoleAlias, getModelMatchPreferences, resolveModelFromString } from "../config/model-resolver";
|
|
9
9
|
import inspectImageDescription from "../prompts/tools/inspect-image.md" with { type: "text" };
|
|
10
10
|
import inspectImageSystemPromptTemplate from "../prompts/tools/inspect-image-system.md" with { type: "text" };
|
|
11
11
|
import {
|
|
@@ -72,7 +72,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
|
|
|
72
72
|
throw new ToolError("No models available for inspect_image.");
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const matchPreferences =
|
|
75
|
+
const matchPreferences = getModelMatchPreferences(this.session.settings);
|
|
76
76
|
const resolvePattern = (pattern: string | undefined): Model<Api> | undefined => {
|
|
77
77
|
if (!pattern) return undefined;
|
|
78
78
|
const expanded = expandRoleAlias(pattern, this.session.settings);
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -572,9 +572,14 @@ export interface ResolvedMultiSearchPath {
|
|
|
572
572
|
targets?: ResolvedSearchTarget[];
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
-
export interface
|
|
575
|
+
export interface ResolvedFindTarget {
|
|
576
576
|
basePath: string;
|
|
577
577
|
globPattern: string;
|
|
578
|
+
hasGlob: boolean;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export interface ResolvedMultiFindPattern {
|
|
582
|
+
targets: ResolvedFindTarget[];
|
|
578
583
|
scopePath: string;
|
|
579
584
|
}
|
|
580
585
|
|
|
@@ -601,6 +606,23 @@ export function parseSearchPath(filePath: string): ParsedSearchPath {
|
|
|
601
606
|
};
|
|
602
607
|
}
|
|
603
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Async sibling of {@link parseSearchPath} that prefers literal interpretation
|
|
611
|
+
* when a path containing glob metacharacters resolves to an existing entry on
|
|
612
|
+
* disk. Disambiguates Next.js/SvelteKit routes like `apps/[id]/page.tsx` —
|
|
613
|
+
* without this, `[id]` is parsed as a glob character class and silently
|
|
614
|
+
* matches nothing.
|
|
615
|
+
*/
|
|
616
|
+
export async function parseSearchPathPreferringLiteral(filePath: string, cwd: string): Promise<ParsedSearchPath> {
|
|
617
|
+
if (!hasGlobPathChars(filePath) || isInternalUrlPath(filePath)) return parseSearchPath(filePath);
|
|
618
|
+
try {
|
|
619
|
+
await fs.promises.stat(resolveToCwd(filePath, cwd));
|
|
620
|
+
return { basePath: filePath };
|
|
621
|
+
} catch {
|
|
622
|
+
return parseSearchPath(filePath);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
604
626
|
// Parse a find pattern into a base directory path and a glob pattern.
|
|
605
627
|
// Examples:
|
|
606
628
|
// src/app/**/\*.tsx -> { basePath: "src/app", globPattern: "**/*.tsx", hasGlob: true }
|
|
@@ -707,7 +729,7 @@ async function resolveSearchPathItems(
|
|
|
707
729
|
|
|
708
730
|
const parsedItems = await Promise.all(
|
|
709
731
|
pathItems.map(async item => {
|
|
710
|
-
const parsedPath =
|
|
732
|
+
const parsedPath = await parseSearchPathPreferringLiteral(item, cwd);
|
|
711
733
|
const absoluteBasePath = resolveToCwd(parsedPath.basePath, cwd);
|
|
712
734
|
const stat = await fs.promises.stat(absoluteBasePath);
|
|
713
735
|
return { raw: item, parsedPath, absoluteBasePath, stat };
|
|
@@ -765,30 +787,22 @@ async function resolveFindPatternItems(
|
|
|
765
787
|
return undefined;
|
|
766
788
|
}
|
|
767
789
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
if (item.parsedPattern.hasGlob) {
|
|
781
|
-
return joinRelativeGlob(relativeBasePath, item.parsedPattern.globPattern);
|
|
782
|
-
}
|
|
783
|
-
if (item.stat.isDirectory()) {
|
|
784
|
-
return joinRelativeGlob(relativeBasePath, "**/*");
|
|
785
|
-
}
|
|
786
|
-
return relativeBasePath === "." ? path.basename(item.absoluteBasePath) : relativeBasePath;
|
|
790
|
+
// Each path becomes its own walk root. Collapsing to a shared common ancestor
|
|
791
|
+
// (and filtering with a brace-union glob) would force the walker to traverse
|
|
792
|
+
// and stat every unrelated sibling under that ancestor — two paths under
|
|
793
|
+
// $HOME would scan all of $HOME. The find tool fans these targets out in
|
|
794
|
+
// parallel instead, so every scan stays bounded to exactly one requested path.
|
|
795
|
+
const targets = patternItems.map(item => {
|
|
796
|
+
const parsedPattern = parseFindPattern(item);
|
|
797
|
+
return {
|
|
798
|
+
basePath: resolveToCwd(parsedPattern.basePath, cwd),
|
|
799
|
+
globPattern: parsedPattern.globPattern,
|
|
800
|
+
hasGlob: parsedPattern.hasGlob,
|
|
801
|
+
};
|
|
787
802
|
});
|
|
788
803
|
|
|
789
804
|
return {
|
|
790
|
-
|
|
791
|
-
globPattern: buildBraceUnion(combinedPatterns) ?? "**/*",
|
|
805
|
+
targets,
|
|
792
806
|
scopePath: toScopeDisplay(patternItems, cwd),
|
|
793
807
|
};
|
|
794
808
|
}
|
|
@@ -946,6 +960,15 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
946
960
|
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
947
961
|
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
948
962
|
}
|
|
963
|
+
// External (http/https/ftp/file) URLs are not searchable; route the caller
|
|
964
|
+
// to `read` instead of letting the path-resolver surface a confusing
|
|
965
|
+
// "Path not found" for a slash-stripped URL.
|
|
966
|
+
const externalUrl = rawPaths.find(rawPath => /^(?:https?|ftp|file|ws|wss):\/\//i.test(rawPath));
|
|
967
|
+
if (externalUrl) {
|
|
968
|
+
throw new ToolError(
|
|
969
|
+
`Cannot ${internalUrlAction} external URL: ${externalUrl}. Use \`read\` to fetch web content, then search the returned text.`,
|
|
970
|
+
);
|
|
971
|
+
}
|
|
949
972
|
const internalRouter = InternalUrlRouter.instance();
|
|
950
973
|
const resolvedPathInputs: string[] = [];
|
|
951
974
|
const immutableSourcePaths = new Set<string>();
|
|
@@ -989,7 +1012,7 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
989
1012
|
let multiTargets: ResolvedSearchTarget[] | undefined;
|
|
990
1013
|
let exactFilePaths: string[] | undefined;
|
|
991
1014
|
if (effectivePaths.length === 1) {
|
|
992
|
-
const parsedPath =
|
|
1015
|
+
const parsedPath = await parseSearchPathPreferringLiteral(effectivePaths[0] ?? ".", cwd);
|
|
993
1016
|
searchPath = resolveToCwd(parsedPath.basePath, cwd);
|
|
994
1017
|
globFilter = parsedPath.glob;
|
|
995
1018
|
scopePath = formatPathRelativeToCwd(searchPath, cwd);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { resolveLocalRoot, resolveLocalUrlToPath, resolveVaultUrlToPath } from "../internal-urls";
|
|
2
4
|
import type { ToolSession } from ".";
|
|
3
5
|
import { normalizeLocalScheme, resolveToCwd } from "./path-utils";
|
|
4
6
|
import { ToolError } from "./tool-errors";
|
|
@@ -6,11 +8,54 @@ import { ToolError } from "./tool-errors";
|
|
|
6
8
|
const VAULT_SCHEME_PREFIX = "vault:";
|
|
7
9
|
const LOCAL_SCHEME_PREFIX = "local:";
|
|
8
10
|
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
/** Resolve the absolute path of the session's `local://` artifact sandbox.
|
|
12
|
+
* Returns `null` when the session has no artifact wiring (e.g. tests). */
|
|
13
|
+
function localSandboxRoot(session: ToolSession): string | null {
|
|
14
|
+
try {
|
|
15
|
+
return path.resolve(
|
|
16
|
+
resolveLocalRoot({
|
|
17
|
+
getArtifactsDir: session.getArtifactsDir,
|
|
18
|
+
getSessionId: session.getSessionId,
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** True when `absolutePath` resolves inside `root` (== root or under it). */
|
|
27
|
+
function isWithinRoot(absolutePath: string, root: string): boolean {
|
|
28
|
+
if (absolutePath === root) return true;
|
|
29
|
+
const sep = `${root}${path.sep}`;
|
|
30
|
+
return absolutePath.startsWith(sep);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True when `targetPath` addresses the session-local artifact sandbox.
|
|
34
|
+
* Accepts both `local://…` URLs and absolute paths pointing inside the
|
|
35
|
+
* resolved sandbox root — the latter is what `read local://…` echoes back
|
|
36
|
+
* in the `[path#tag]` header. Those files are not part of the working tree,
|
|
37
|
+
* so plan mode treats them as freely writable scratch/plan space. */
|
|
38
|
+
function targetsLocalSandbox(session: ToolSession, targetPath: string): boolean {
|
|
39
|
+
const normalized = normalizeLocalScheme(targetPath);
|
|
40
|
+
if (normalized.startsWith(LOCAL_SCHEME_PREFIX)) return true;
|
|
41
|
+
if (!path.isAbsolute(normalized)) return false;
|
|
42
|
+
const root = localSandboxRoot(session);
|
|
43
|
+
if (!root) return false;
|
|
44
|
+
// Compare both raw and realpath-normalized forms so that
|
|
45
|
+
// `/tmp/…` vs `/private/tmp/…` (macOS) and other symlink-collapsed
|
|
46
|
+
// roots both resolve to the same sandbox identity.
|
|
47
|
+
const resolved = path.resolve(normalized);
|
|
48
|
+
if (isWithinRoot(resolved, root)) return true;
|
|
49
|
+
try {
|
|
50
|
+
const realRoot = fs.realpathSync.native(root);
|
|
51
|
+
if (isWithinRoot(resolved, realRoot)) return true;
|
|
52
|
+
// `resolved` itself may live in `/tmp/...` while `realRoot` is `/private/tmp/...`;
|
|
53
|
+
// realpath the parent dir of `resolved` so we catch that direction too.
|
|
54
|
+
const realParent = fs.realpathSync.native(path.dirname(resolved));
|
|
55
|
+
return isWithinRoot(path.join(realParent, path.basename(resolved)), realRoot);
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
14
59
|
}
|
|
15
60
|
|
|
16
61
|
/**
|
|
@@ -55,7 +100,7 @@ export function enforcePlanModeWrite(
|
|
|
55
100
|
throw new ToolError("Plan mode: deleting files is not allowed.");
|
|
56
101
|
}
|
|
57
102
|
|
|
58
|
-
if (targetsLocalSandbox(targetPath)) return;
|
|
103
|
+
if (targetsLocalSandbox(session, targetPath)) return;
|
|
59
104
|
|
|
60
105
|
throw new ToolError(
|
|
61
106
|
"Plan mode: the working tree is read-only. Write your plan to a local://<slug>-plan.md file instead.",
|
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, recordFileSnapshot } from "../edit/file-snapshot-store";
|
|
12
|
+
import { canonicalSnapshotKey, 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";
|
|
@@ -131,7 +131,7 @@ function recordFullHashlineContext(
|
|
|
131
131
|
): HashlineHeaderContext | undefined {
|
|
132
132
|
if (!absolutePath || !path.isAbsolute(absolutePath)) return undefined;
|
|
133
133
|
const normalized = normalizeToLF(fullText);
|
|
134
|
-
const tag = getFileSnapshotStore(session).record(absolutePath, normalized);
|
|
134
|
+
const tag = getFileSnapshotStore(session).record(canonicalSnapshotKey(absolutePath), normalized);
|
|
135
135
|
return {
|
|
136
136
|
header: formatHashlineHeader(displayPath, tag),
|
|
137
137
|
tag,
|
|
@@ -575,6 +575,8 @@ export interface ReadToolDetails {
|
|
|
575
575
|
summary?: { lines: number; elidedSpans: number; elidedLines: number };
|
|
576
576
|
/** Number of unresolved git conflicts surfaced by this read (TUI uses for inline `⚠ N` badge). */
|
|
577
577
|
conflictCount?: number;
|
|
578
|
+
/** Paths recovered from a delimited read argument; used only by the TUI to render one call as multiple read rows. */
|
|
579
|
+
displayReadTargets?: string[];
|
|
578
580
|
}
|
|
579
581
|
|
|
580
582
|
type ReadParams = ReadToolInput;
|
|
@@ -670,7 +672,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
670
672
|
readonly loadMode = "essential";
|
|
671
673
|
readonly description: string;
|
|
672
674
|
readonly parameters = readSchema;
|
|
673
|
-
readonly nonAbortable = true;
|
|
674
675
|
readonly strict = true;
|
|
675
676
|
|
|
676
677
|
readonly #autoResizeImages: boolean;
|
|
@@ -704,6 +705,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
704
705
|
const notice = `Note: interpreted as ${parts.length} paths: ${parts.join(", ")}`;
|
|
705
706
|
const notes = [notice];
|
|
706
707
|
const content: Array<TextContent | ImageContent> = [];
|
|
708
|
+
const displayReadTargets: string[] = [];
|
|
707
709
|
let pendingText = notice;
|
|
708
710
|
const flushText = () => {
|
|
709
711
|
if (pendingText.length === 0) return;
|
|
@@ -717,6 +719,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
717
719
|
for (const part of parts) {
|
|
718
720
|
try {
|
|
719
721
|
const result = await this.execute("read-delimited-part", { path: part }, signal);
|
|
722
|
+
displayReadTargets.push(result.details?.suffixResolution?.to ?? part);
|
|
720
723
|
for (const block of result.content) {
|
|
721
724
|
if (block.type === "text") {
|
|
722
725
|
appendText(block.text);
|
|
@@ -730,12 +733,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
730
733
|
const message = error instanceof Error ? error.message : String(error);
|
|
731
734
|
const errorNote = `Could not read ${part}: ${message}`;
|
|
732
735
|
notes.push(errorNote);
|
|
736
|
+
displayReadTargets.push(part);
|
|
733
737
|
appendText(`[${errorNote}]`);
|
|
734
738
|
}
|
|
735
739
|
}
|
|
736
740
|
flushText();
|
|
737
741
|
|
|
738
|
-
return toolResult<ReadToolDetails>({ notes }).content(content).done();
|
|
742
|
+
return toolResult<ReadToolDetails>({ notes, displayReadTargets }).content(content).done();
|
|
739
743
|
}
|
|
740
744
|
|
|
741
745
|
async #resolveArchiveReadPath(readPath: string, signal?: AbortSignal): Promise<ResolvedArchiveReadPath | null> {
|
|
@@ -1648,7 +1652,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1648
1652
|
throw new ToolError("Multi-range line selectors are not supported for directory listings.");
|
|
1649
1653
|
}
|
|
1650
1654
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1651
|
-
|
|
1655
|
+
// Directory listings are deterministic and fast; never abort them mid-scan
|
|
1656
|
+
// (an interrupt would otherwise surface a misleading "Operation aborted").
|
|
1657
|
+
const dirResult = await this.#readDirectory(absolutePath, offset, limit, undefined);
|
|
1652
1658
|
if (suffixResolution) {
|
|
1653
1659
|
dirResult.details ??= {};
|
|
1654
1660
|
dirResult.details.suffixResolution = suffixResolution;
|
|
@@ -1750,15 +1756,25 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1750
1756
|
// Convert document via markit.
|
|
1751
1757
|
const result = await convertFileWithMarkit(absolutePath, signal);
|
|
1752
1758
|
if (result.ok) {
|
|
1753
|
-
//
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1759
|
+
// Route the converted markdown through the in-memory text builder
|
|
1760
|
+
// so line-range selectors (`file.pdf:50-100`, `:5-16,40-80`) and
|
|
1761
|
+
// raw mode apply against the converted output. Without this,
|
|
1762
|
+
// `file.pdf:50-100` silently returned the head of the document
|
|
1763
|
+
// because only `truncateHead` was being applied.
|
|
1764
|
+
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
1765
|
+
return this.#buildInMemoryMultiRangeResult(result.content, parsed.ranges, {
|
|
1766
|
+
details: { resolvedPath: absolutePath },
|
|
1767
|
+
sourcePath: absolutePath,
|
|
1768
|
+
entityLabel: "document",
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1772
|
+
return this.#buildInMemoryTextResult(result.content, offset, limit, {
|
|
1773
|
+
details: { resolvedPath: absolutePath },
|
|
1774
|
+
sourcePath: absolutePath,
|
|
1775
|
+
entityLabel: "document",
|
|
1776
|
+
raw: isRawSelector(parsed),
|
|
1777
|
+
});
|
|
1762
1778
|
} else if (result.error) {
|
|
1763
1779
|
content = [{ type: "text", text: `[Cannot read ${ext} file: ${result.error || "conversion failed"}]` }];
|
|
1764
1780
|
} else {
|
|
@@ -1805,7 +1821,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1805
1821
|
parsed,
|
|
1806
1822
|
displayMode,
|
|
1807
1823
|
suffixResolution,
|
|
1808
|
-
|
|
1824
|
+
undefined, // plain-file read: deterministic and fast, never abort mid-read
|
|
1809
1825
|
);
|
|
1810
1826
|
if (multiResult.bridgeResult) return multiResult.bridgeResult;
|
|
1811
1827
|
content = [{ type: "text", text: multiResult.outputText }];
|
|
@@ -1864,7 +1880,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1864
1880
|
maxLinesToCollect,
|
|
1865
1881
|
maxBytesForRead,
|
|
1866
1882
|
selectedLineLimit,
|
|
1867
|
-
|
|
1883
|
+
undefined, // plain-file read: deterministic and fast, never abort mid-read
|
|
1868
1884
|
);
|
|
1869
1885
|
|
|
1870
1886
|
const {
|
|
@@ -1944,7 +1960,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1944
1960
|
// full file and any anchor validates while the file is unchanged.
|
|
1945
1961
|
const isWholeFile = offset === undefined && limit === undefined && !wasTruncated;
|
|
1946
1962
|
const tag = isWholeFile
|
|
1947
|
-
? getFileSnapshotStore(this.session).record(
|
|
1963
|
+
? getFileSnapshotStore(this.session).record(
|
|
1964
|
+
canonicalSnapshotKey(absolutePath),
|
|
1965
|
+
normalizeToLF(collectedLines.join("\n")),
|
|
1966
|
+
)
|
|
1948
1967
|
: await recordFileSnapshot(this.session, absolutePath);
|
|
1949
1968
|
if (tag) {
|
|
1950
1969
|
hashContext = hashlineHeaderContext(formatPathRelativeToCwd(absolutePath, this.session.cwd), tag);
|
|
@@ -2355,11 +2374,13 @@ function formatReadPathLink(
|
|
|
2355
2374
|
const plainDisplayPath = options.suffixResolution
|
|
2356
2375
|
? shortenPath(options.suffixResolution.to)
|
|
2357
2376
|
: shortenPath(basePath || options.resolvedPath || options.fallbackLabel || rawPath);
|
|
2358
|
-
const
|
|
2377
|
+
const absoluteInputPath = path.isAbsolute(basePath) ? basePath : undefined;
|
|
2378
|
+
const target =
|
|
2379
|
+
options.resolvedPath ?? options.sourcePath ?? tryResolveInternalUrlSync(basePath) ?? absoluteInputPath;
|
|
2359
2380
|
const line = firstReadSelectorLine(split.sel) ?? options.offset;
|
|
2360
2381
|
const linkOptions = line !== undefined ? { line } : undefined;
|
|
2361
|
-
const
|
|
2362
|
-
return `${
|
|
2382
|
+
const linkedPath = target ? fileHyperlink(target, plainDisplayPath, linkOptions) : plainDisplayPath;
|
|
2383
|
+
return `${linkedPath}${selectorSuffix}`;
|
|
2363
2384
|
}
|
|
2364
2385
|
|
|
2365
2386
|
export const readToolRenderer = {
|
|
@@ -338,6 +338,7 @@ export function formatDiagnostics(
|
|
|
338
338
|
expanded: boolean,
|
|
339
339
|
theme: Theme,
|
|
340
340
|
getLangIcon: (filePath: string) => string,
|
|
341
|
+
options?: { title?: string },
|
|
341
342
|
): string {
|
|
342
343
|
if (diag.messages.length === 0) return "";
|
|
343
344
|
|
|
@@ -369,7 +370,8 @@ export function formatDiagnostics(
|
|
|
369
370
|
? theme.styledSymbol("status.error", "error")
|
|
370
371
|
: theme.styledSymbol("status.warning", "warning");
|
|
371
372
|
const summary = sanitizeDiagnosticDisplayText(diag.summary);
|
|
372
|
-
|
|
373
|
+
const summaryTag = summary ? ` ${theme.fg("dim", `(${summary})`)}` : "";
|
|
374
|
+
let output = `\n\n${headerIcon} ${theme.fg("toolTitle", options?.title ?? "Diagnostics")}${summaryTag}`;
|
|
373
375
|
|
|
374
376
|
const maxDiags = expanded ? diag.messages.length : 5;
|
|
375
377
|
let diagsShown = 0;
|
package/src/tools/renderers.ts
CHANGED
|
@@ -40,21 +40,6 @@ export type ToolRenderer = {
|
|
|
40
40
|
args?: unknown,
|
|
41
41
|
) => Component;
|
|
42
42
|
mergeCallAndResult?: boolean;
|
|
43
|
-
/**
|
|
44
|
-
* While a tool's preview is still streaming, report whether the
|
|
45
|
-
* currently-rendered preview is append-only: its rows only grow at the bottom
|
|
46
|
-
* and never re-layout above the bottom live region (a full, top-anchored
|
|
47
|
-
* content/code preview). The transcript reports this up to the TUI so a
|
|
48
|
-
* streaming preview taller than the viewport commits its scrolled-off head to
|
|
49
|
-
* native scrollback instead of dropping it (see
|
|
50
|
-
* `ToolExecutionComponent.isTranscriptBlockAppendOnly`). `result` is the
|
|
51
|
-
* latest (possibly partial) tool result, or `undefined` before one exists —
|
|
52
|
-
* `eval`/`bash` use its presence to defer committing until the streamed input
|
|
53
|
-
* (code) has finalized. Omit (or return `false`) for previews that slide a
|
|
54
|
-
* tail window or later collapse to a compact result — committing their head
|
|
55
|
-
* would strand stale rows.
|
|
56
|
-
*/
|
|
57
|
-
isStreamingPreviewAppendOnly?: (args: unknown, options: RenderResultOptions, result?: unknown) => boolean;
|
|
58
43
|
/** Render without background box, inline in the response flow */
|
|
59
44
|
inline?: boolean;
|
|
60
45
|
};
|
package/src/tools/search.ts
CHANGED
|
@@ -83,6 +83,7 @@ const searchSchema = z
|
|
|
83
83
|
gitignore: z.boolean().optional().describe("respect gitignore"),
|
|
84
84
|
skip: z
|
|
85
85
|
.number()
|
|
86
|
+
.nullable()
|
|
86
87
|
.optional()
|
|
87
88
|
.describe("files to skip before collecting results — use to paginate when the prior call hit the file limit"),
|
|
88
89
|
})
|
|
@@ -107,6 +108,10 @@ export const SINGLE_FILE_MATCHES = 200;
|
|
|
107
108
|
* (DEFAULT_FILE_LIMIT files × MULTI_FILE_PER_FILE_MATCHES matches) plus
|
|
108
109
|
* pagination headroom so the caller can see total file count. */
|
|
109
110
|
const INTERNAL_TOTAL_CAP = 2000;
|
|
111
|
+
/** Mirrors `MAX_FILE_BYTES` in `crates/pi-natives/src/grep.rs`. Native grep
|
|
112
|
+
* silently returns no matches for files larger than this; surface a warning
|
|
113
|
+
* when the caller explicitly targeted such a file so they know to chunk it. */
|
|
114
|
+
const NATIVE_GREP_MAX_FILE_BYTES = 4 * 1024 * 1024;
|
|
110
115
|
|
|
111
116
|
/**
|
|
112
117
|
* Parsed `paths` entry — a path (possibly archive-shaped) plus an optional
|
|
@@ -666,7 +671,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
666
671
|
throw new ToolError("Pattern must not be empty");
|
|
667
672
|
}
|
|
668
673
|
|
|
669
|
-
const normalizedSkip =
|
|
674
|
+
const normalizedSkip =
|
|
675
|
+
skip === undefined || skip === null ? 0 : Number.isFinite(skip) ? Math.floor(skip) : Number.NaN;
|
|
670
676
|
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
|
|
671
677
|
throw new ToolError("Skip must be a non-negative number");
|
|
672
678
|
}
|
|
@@ -728,7 +734,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
728
734
|
// reason instead of a downstream "path not found" from the scope resolver.
|
|
729
735
|
throw new ToolError(
|
|
730
736
|
`Cannot search archive member(s): ${archiveUnreadable.join(", ")}. ` +
|
|
731
|
-
`Read the
|
|
737
|
+
`Read the member with \`read <archive>:<member>\` and inspect the returned text, ` +
|
|
732
738
|
`or pass a UTF-8 text member.`,
|
|
733
739
|
);
|
|
734
740
|
}
|
|
@@ -991,6 +997,34 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
991
997
|
: "";
|
|
992
998
|
const { record: recordFile, list: fileList } = createFileRecorder();
|
|
993
999
|
const fileMatchCounts = new Map<string, number>();
|
|
1000
|
+
// Detect explicit file targets that exceed the native grep size cap.
|
|
1001
|
+
// Native silently returns no matches above the cap; without this note the
|
|
1002
|
+
// caller sees "no matches" for a literal pattern that visibly exists.
|
|
1003
|
+
const oversizedNote = await (async (): Promise<string | undefined> => {
|
|
1004
|
+
const explicitFileTargets: string[] = [];
|
|
1005
|
+
if (exactFilePaths) {
|
|
1006
|
+
explicitFileTargets.push(...exactFilePaths);
|
|
1007
|
+
} else if (searchablePaths.length > 0 && !isDirectory && !multiTargets) {
|
|
1008
|
+
explicitFileTargets.push(searchPath);
|
|
1009
|
+
}
|
|
1010
|
+
if (explicitFileTargets.length === 0) return undefined;
|
|
1011
|
+
const oversized: string[] = [];
|
|
1012
|
+
await Promise.all(
|
|
1013
|
+
explicitFileTargets.map(async target => {
|
|
1014
|
+
try {
|
|
1015
|
+
const st = await stat(target);
|
|
1016
|
+
if (st.isFile() && st.size > NATIVE_GREP_MAX_FILE_BYTES) {
|
|
1017
|
+
oversized.push(path.relative(this.session.cwd, target) || target);
|
|
1018
|
+
}
|
|
1019
|
+
} catch {
|
|
1020
|
+
// Stat failures here are surfaced by other code paths.
|
|
1021
|
+
}
|
|
1022
|
+
}),
|
|
1023
|
+
);
|
|
1024
|
+
if (oversized.length === 0) return undefined;
|
|
1025
|
+
const limitMb = Math.floor(NATIVE_GREP_MAX_FILE_BYTES / (1024 * 1024));
|
|
1026
|
+
return `Skipped oversized files (>${limitMb}MB grep limit; split the file or narrow with \`read\`): ${oversized.join(", ")}`;
|
|
1027
|
+
})();
|
|
994
1028
|
const archiveNote =
|
|
995
1029
|
archiveUnreadable.length > 0
|
|
996
1030
|
? `Skipped archive entries (search supports text members only): ${archiveUnreadable.join(", ")}`
|
|
@@ -1002,7 +1036,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
1002
1036
|
const missingPathsNote =
|
|
1003
1037
|
missingPathsForNote.length > 0 ? `Skipped missing paths: ${missingPathsForNote.join(", ")}` : undefined;
|
|
1004
1038
|
const warningNote =
|
|
1005
|
-
[missingPathsNote, archiveNote].filter((s): s is string => Boolean(s)).join("\n") ||
|
|
1039
|
+
[missingPathsNote, archiveNote, oversizedNote].filter((s): s is string => Boolean(s)).join("\n") ||
|
|
1040
|
+
undefined;
|
|
1006
1041
|
if (selectedMatches.length === 0) {
|
|
1007
1042
|
const details: SearchToolDetails = {
|
|
1008
1043
|
scopePath,
|
package/src/tools/ssh.ts
CHANGED
package/src/tools/todo.ts
CHANGED
package/src/tools/write.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
8
8
|
import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import * as z from "zod/v4";
|
|
10
10
|
|
|
11
|
-
import { getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
11
|
+
import { canonicalSnapshotKey, getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
12
12
|
import { normalizeToLF } from "../edit/normalize";
|
|
13
13
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
14
14
|
import { InternalUrlRouter } from "../internal-urls";
|
|
@@ -132,7 +132,7 @@ function stripWriteContent(session: ToolSession, content: string): { text: strin
|
|
|
132
132
|
function maybeWriteSnapshotHeader(session: ToolSession, absolutePath: string, content: string): string | undefined {
|
|
133
133
|
if (!resolveFileDisplayMode(session).hashLines) return undefined;
|
|
134
134
|
const normalized = normalizeToLF(content);
|
|
135
|
-
const tag = getFileSnapshotStore(session).record(absolutePath, normalized);
|
|
135
|
+
const tag = getFileSnapshotStore(session).record(canonicalSnapshotKey(absolutePath), normalized);
|
|
136
136
|
return formatHashlineHeader(formatPathRelativeToCwd(absolutePath, session.cwd), tag);
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -277,7 +277,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
277
277
|
readonly label = "Write";
|
|
278
278
|
readonly description: string;
|
|
279
279
|
readonly parameters = writeSchema;
|
|
280
|
-
readonly nonAbortable = true;
|
|
281
280
|
readonly strict = true;
|
|
282
281
|
readonly concurrency = "exclusive";
|
|
283
282
|
readonly loadMode = "discoverable";
|
|
@@ -582,6 +581,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
582
581
|
const batchRequest = getLspBatchRequest(context?.toolCall);
|
|
583
582
|
const diagnostics = await this.#writethrough(absolutePath, newContent, signal, undefined, batchRequest);
|
|
584
583
|
invalidateFsScanAfterWrite(absolutePath);
|
|
584
|
+
this.session.bumpFileMutationVersion?.(absolutePath);
|
|
585
585
|
this.session.fileSnapshotStore?.invalidate(absolutePath);
|
|
586
586
|
this.session.conflictHistory?.invalidate(entry.id);
|
|
587
587
|
|
|
@@ -707,6 +707,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
707
707
|
|
|
708
708
|
const diagnostics = await this.#writethrough(absolutePath, text, signal, undefined, batchRequest);
|
|
709
709
|
invalidateFsScanAfterWrite(absolutePath);
|
|
710
|
+
this.session.bumpFileMutationVersion?.(absolutePath);
|
|
710
711
|
this.session.fileSnapshotStore?.invalidate(absolutePath);
|
|
711
712
|
for (const entry of fileEntries) history.invalidate(entry.id);
|
|
712
713
|
const header = maybeWriteSnapshotHeader(this.session, absolutePath, text);
|
|
@@ -886,6 +887,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
886
887
|
|
|
887
888
|
const diagnostics = await this.#writethrough(absolutePath, cleanContent, signal, undefined, batchRequest);
|
|
888
889
|
invalidateFsScanAfterWrite(absolutePath);
|
|
890
|
+
this.session.bumpFileMutationVersion?.(absolutePath);
|
|
889
891
|
const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, cleanContent);
|
|
890
892
|
|
|
891
893
|
const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
@@ -1039,17 +1041,6 @@ export const writeToolRenderer = {
|
|
|
1039
1041
|
});
|
|
1040
1042
|
},
|
|
1041
1043
|
|
|
1042
|
-
// Only the expanded (Ctrl+O) preview is append-only: it renders the whole
|
|
1043
|
-
// content top-anchored, so streamed chunks only append rows at the bottom.
|
|
1044
|
-
// The collapsed preview slides a bounded tail window (`formatStreamingContent`
|
|
1045
|
-
// with `WRITE_STREAMING_PREVIEW_LINES`) whose visible rows re-layout as the
|
|
1046
|
-
// window moves — not append-only, but it never overflows the viewport, so its
|
|
1047
|
-
// head is never at risk of being dropped regardless. `write` has no partial
|
|
1048
|
-
// result (content streams as args), so `result` is ignored here.
|
|
1049
|
-
isStreamingPreviewAppendOnly(args: WriteRenderArgs, options: RenderResultOptions, _result?: unknown): boolean {
|
|
1050
|
-
return Boolean(options?.expanded && args.content);
|
|
1051
|
-
},
|
|
1052
|
-
|
|
1053
1044
|
renderResult(
|
|
1054
1045
|
result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails; isError?: boolean },
|
|
1055
1046
|
options: RenderResultOptions,
|
package/src/tools/yield.ts
CHANGED
|
@@ -20,6 +20,14 @@ export interface YieldDetails {
|
|
|
20
20
|
data: unknown;
|
|
21
21
|
status: "success" | "aborted";
|
|
22
22
|
error?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Set when the yield tool exhausted its in-tool schema-retry budget
|
|
25
|
+
* (MAX_SCHEMA_RETRIES) and accepted the data anyway. Surfaced so the
|
|
26
|
+
* executor's post-mortem finalizer can honor the override instead of
|
|
27
|
+
* re-rejecting the same payload with `schema_violation` — keeping the
|
|
28
|
+
* subagent's acceptance and the parent's view of the result in lockstep.
|
|
29
|
+
*/
|
|
30
|
+
schemaOverridden?: boolean;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
function formatSchema(schema: unknown): string {
|
|
@@ -237,7 +245,7 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
237
245
|
: "Result submitted.";
|
|
238
246
|
return {
|
|
239
247
|
content: [{ type: "text", text: responseText }],
|
|
240
|
-
details: { data, status, error: errorMessage },
|
|
248
|
+
details: { data, status, error: errorMessage, schemaOverridden: schemaValidationOverridden || undefined },
|
|
241
249
|
};
|
|
242
250
|
}
|
|
243
251
|
}
|
|
@@ -254,6 +262,7 @@ subprocessToolRegistry.register<YieldDetails>("yield", {
|
|
|
254
262
|
data: record.data,
|
|
255
263
|
status,
|
|
256
264
|
error: typeof record.error === "string" ? record.error : undefined,
|
|
265
|
+
schemaOverridden: record.schemaOverridden === true ? true : undefined,
|
|
257
266
|
};
|
|
258
267
|
},
|
|
259
268
|
shouldTerminate: event => !event.isError,
|