@oh-my-pi/pi-coding-agent 17.1.6 → 17.1.8
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 +58 -0
- package/dist/{CHANGELOG-x9zt79k8.md → CHANGELOG-vt8ene9g.md} +58 -0
- package/dist/cli.js +4309 -7287
- package/dist/template-dys3vk5b.js +1671 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/dist/types/advisor/advise-tool.d.ts +0 -7
- package/dist/types/advisor/runtime.d.ts +0 -9
- package/dist/types/async/job-manager.d.ts +11 -0
- package/dist/types/cleanse/agent.d.ts +19 -0
- package/dist/types/cleanse/balance.d.ts +7 -0
- package/dist/types/cleanse/checkers.d.ts +13 -0
- package/dist/types/cleanse/index.d.ts +16 -0
- package/dist/types/cleanse/loop.d.ts +16 -0
- package/dist/types/cleanse/parsers.d.ts +13 -0
- package/dist/types/cleanse/progress.d.ts +14 -0
- package/dist/types/cleanse/types.d.ts +64 -0
- package/dist/types/commands/cleanse.d.ts +23 -0
- package/dist/types/config/settings-schema.d.ts +16 -1
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/export/html/index.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -3
- package/dist/types/session/agent-session-types.d.ts +5 -5
- package/dist/types/session/agent-session.d.ts +26 -1
- package/dist/types/session/async-job-delivery.d.ts +8 -0
- package/dist/types/session/model-controls.d.ts +4 -4
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/turn-recovery.d.ts +1 -1
- package/dist/types/task/isolation-ownership.d.ts +34 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
- package/dist/types/tools/browser/tab-worker.d.ts +1 -4
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/tui/output-block.d.ts +5 -5
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/package.json +16 -12
- package/scripts/bundle-dist.ts +3 -1
- package/src/advisor/advise-tool.ts +0 -11
- package/src/advisor/runtime.ts +0 -12
- package/src/async/job-manager.ts +30 -7
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- package/src/cli/update-cli.ts +3 -0
- package/src/cli/usage-cli.ts +29 -4
- package/src/cli/worktree-cli.ts +28 -11
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +2 -1
- package/src/commands/cleanse.ts +45 -0
- package/src/config/settings-schema.ts +15 -1
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/discovery/claude-plugins.ts +144 -34
- package/src/export/html/index.ts +17 -10
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/launch/broker.ts +14 -4
- package/src/modes/acp/acp-agent.ts +12 -9
- package/src/modes/acp/acp-event-mapper.ts +38 -4
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/settings-selector.ts +9 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/extension-ui-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/interactive-mode.ts +36 -47
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/prompts/goals/guided-goal-interview.md +41 -6
- package/src/prompts/system/vibe-mode-active.md +4 -1
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/goal.md +1 -1
- package/src/sdk.ts +47 -50
- package/src/session/agent-session-types.ts +5 -5
- package/src/session/agent-session.ts +151 -11
- package/src/session/async-job-delivery.ts +8 -0
- package/src/session/model-controls.ts +9 -9
- package/src/session/session-advisors.ts +2 -7
- package/src/session/session-history-format.ts +31 -6
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +158 -52
- package/src/session/streaming-output.ts +18 -6
- package/src/session/turn-recovery.ts +4 -4
- package/src/slash-commands/builtin-registry.ts +74 -2
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/worktree.ts +8 -0
- package/src/tools/ask.ts +3 -3
- package/src/tools/ast-edit.ts +9 -2
- package/src/tools/bash.ts +16 -9
- package/src/tools/browser/cmux/cmux-tab.ts +9 -14
- package/src/tools/browser/tab-worker.ts +12 -35
- package/src/tools/browser.ts +2 -2
- package/src/tools/gh-renderer.ts +3 -3
- package/src/tools/index.ts +46 -17
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +87 -13
- package/src/tools/write.ts +51 -7
- package/src/tools/xdev.ts +198 -210
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/output-block.ts +25 -8
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/shell-snapshot-fn-env.sh +5 -2
- package/src/web/search/render.ts +2 -2
- package/dist/types/goals/guided-setup.d.ts +0 -30
- package/src/goals/guided-setup.ts +0 -171
- package/src/prompts/goals/guided-goal-system.md +0 -33
package/src/tools/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { TaskTool } from "../task";
|
|
|
32
32
|
import type { AgentOutputManager } from "../task/output-manager";
|
|
33
33
|
import { canSpawnAtDepth, type StructuredSubagentSchemaMode } from "../task/types";
|
|
34
34
|
import type { EventBus } from "../utils/event-bus";
|
|
35
|
+
import { type InspectImageMode, isInspectImageToolActive } from "../utils/inspect-image-mode";
|
|
35
36
|
import { WebSearchTool } from "../web/search";
|
|
36
37
|
import type { WorkspaceTree } from "../workspace-tree";
|
|
37
38
|
import { AskTool } from "./ask";
|
|
@@ -61,7 +62,7 @@ import { ReadTool } from "./read";
|
|
|
61
62
|
import type { PlanProposalHandler } from "./resolve";
|
|
62
63
|
import { type TodoPhase, TodoTool } from "./todo";
|
|
63
64
|
import { WriteTool } from "./write";
|
|
64
|
-
import { isMountableUnderXdev,
|
|
65
|
+
import { isMountableUnderXdev, type XdevState } from "./xdev";
|
|
65
66
|
import { YieldTool } from "./yield";
|
|
66
67
|
|
|
67
68
|
export * from "../edit";
|
|
@@ -243,8 +244,10 @@ export interface ToolSession {
|
|
|
243
244
|
isToolActive?: (name: string) => boolean;
|
|
244
245
|
/** Update the active built-in tool predicate when a session changes tools mid-run. */
|
|
245
246
|
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
246
|
-
/**
|
|
247
|
-
|
|
247
|
+
/** Canonical map containing every registered tool exactly once. */
|
|
248
|
+
toolRegistry?: Map<string, Tool>;
|
|
249
|
+
/** `xd://` presentation state backed by {@link toolRegistry}. */
|
|
250
|
+
xdev?: XdevState;
|
|
248
251
|
/** Agent registry for IRC routing across live sessions. */
|
|
249
252
|
agentRegistry?: AgentRegistry;
|
|
250
253
|
/** Idle→parked→revive lifecycle owner; lets the hub kill a non-job-backed agent registration. Default: AgentLifecycleManager.global(). */
|
|
@@ -263,6 +266,8 @@ export interface ToolSession {
|
|
|
263
266
|
getActiveModelString?: () => string | undefined;
|
|
264
267
|
/** Get the current session model object (provider/api capabilities), regardless of how it was chosen. */
|
|
265
268
|
getActiveModel?: () => Model | undefined;
|
|
269
|
+
/** Session-scoped inspect_image mode override set by `/vision`; wins over the persisted setting. */
|
|
270
|
+
getInspectImageModeOverride?: () => InspectImageMode | undefined;
|
|
266
271
|
/** Get the session's live per-family service tiers (undefined = none). Source of truth for subagent `tier.subagent: inherit`. */
|
|
267
272
|
getServiceTierByFamily?: () => ServiceTierByFamily | undefined;
|
|
268
273
|
/** Auth storage for passing to subagents (avoids re-discovery) */
|
|
@@ -543,7 +548,16 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
543
548
|
}
|
|
544
549
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
|
545
550
|
const isToolAllowed = (name: string) => {
|
|
546
|
-
|
|
551
|
+
// Never in the default set. Explicitly activatable while goal.enabled and
|
|
552
|
+
// no goal record exists yet — /guided-goal enables it so the agent can
|
|
553
|
+
// finish the interview with `goal create`, which turns goal mode on. Once
|
|
554
|
+
// a goal record exists, only an enabled goal keeps the tool: a completed
|
|
555
|
+
// (exiting) or paused goal must stop advertising it on the next rebuild.
|
|
556
|
+
if (name === "goal") {
|
|
557
|
+
if (!goalEnabled || restrictToolNames) return false;
|
|
558
|
+
const goalState = session.getGoalModeState?.();
|
|
559
|
+
return goalState === undefined || goalState.enabled === true || goalState.goal.status === "dropped";
|
|
560
|
+
}
|
|
547
561
|
if (name === "lsp") return enableLsp && session.settings.get("lsp.enabled");
|
|
548
562
|
if (name === "bash") return session.settings.get("bash.enabled");
|
|
549
563
|
if (name === "eval") return allowEval;
|
|
@@ -555,7 +569,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
555
569
|
if (name === "github") return session.settings.get("github.enabled");
|
|
556
570
|
if (name === "ast_grep") return session.settings.get("astGrep.enabled");
|
|
557
571
|
if (name === "ast_edit") return session.settings.get("astEdit.enabled");
|
|
558
|
-
if (name === "inspect_image") return session
|
|
572
|
+
if (name === "inspect_image") return isInspectImageToolActive(session);
|
|
559
573
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
560
574
|
if (name === "ask") return session.settings.get("ask.enabled");
|
|
561
575
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
@@ -613,6 +627,10 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
613
627
|
}),
|
|
614
628
|
);
|
|
615
629
|
let tools = baseResults.filter((r): r is Tool => r !== null);
|
|
630
|
+
const toolRegistry = session.toolRegistry ?? new Map<string, Tool>();
|
|
631
|
+
session.toolRegistry = toolRegistry;
|
|
632
|
+
const builtInNames = new Set(tools.map(tool => tool.name));
|
|
633
|
+
for (const tool of tools) toolRegistry.set(tool.name, tool);
|
|
616
634
|
|
|
617
635
|
// Ordinary sessions use xd:// for discoverable built-ins, custom tools, and
|
|
618
636
|
// MCP tools. Structured children must expose only their host-provided names,
|
|
@@ -621,26 +639,26 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
621
639
|
const xdevEnabled = !restrictToolNames && session.settings.get("tools.xdev");
|
|
622
640
|
const mountBuiltinTools = requestedTools === undefined;
|
|
623
641
|
if (xdevEnabled) {
|
|
624
|
-
const
|
|
642
|
+
const mountedNames = new Set<string>();
|
|
625
643
|
const kept: Tool[] = [];
|
|
626
644
|
for (const tool of tools) {
|
|
627
645
|
const mountable = mountBuiltinTools && isMountableUnderXdev(tool) && tool.name in BUILTIN_TOOLS;
|
|
628
|
-
(mountable
|
|
646
|
+
if (mountable) mountedNames.add(tool.name);
|
|
647
|
+
else kept.push(tool);
|
|
629
648
|
}
|
|
630
|
-
session.
|
|
649
|
+
session.xdev = {
|
|
650
|
+
tools: toolRegistry,
|
|
651
|
+
mountedNames,
|
|
652
|
+
builtInNames,
|
|
653
|
+
isActive: name => session.isToolActive?.(name) === true,
|
|
654
|
+
};
|
|
631
655
|
tools = kept;
|
|
632
|
-
const finalActiveNames = new Set(tools.map(tool => tool.name));
|
|
633
|
-
if (session.setActiveToolNames) {
|
|
634
|
-
session.setActiveToolNames(finalActiveNames);
|
|
635
|
-
} else {
|
|
636
|
-
session.isToolActive = name => finalActiveNames.has(name);
|
|
637
|
-
}
|
|
638
656
|
}
|
|
639
657
|
// The xd:// transport rides read/write: `read xd://` lists+documents devices,
|
|
640
658
|
// `write xd://<tool>` executes them. Staged previews from deferrable tools
|
|
641
659
|
// (e.g. ast_edit) also resolve through a `write` to xd://resolve/reject. Retain
|
|
642
660
|
// both whenever any device is mounted or a deferrable tool can stage one.
|
|
643
|
-
const xdevMounted = (session.
|
|
661
|
+
const xdevMounted = (session.xdev?.mountedNames.size ?? 0) > 0;
|
|
644
662
|
if (
|
|
645
663
|
!restrictToolNames &&
|
|
646
664
|
(tools.some(tool => tool.deferrable === true) || xdevMounted) &&
|
|
@@ -648,15 +666,26 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
648
666
|
) {
|
|
649
667
|
const writeTool = await logger.time("createTools:write", BUILTIN_TOOLS.write, session);
|
|
650
668
|
if (writeTool) {
|
|
651
|
-
|
|
669
|
+
const wrapped = wrapToolWithMetaNotice(writeTool);
|
|
670
|
+
tools.push(wrapped);
|
|
671
|
+
toolRegistry.set(wrapped.name, wrapped);
|
|
672
|
+
builtInNames.add(wrapped.name);
|
|
652
673
|
}
|
|
653
674
|
}
|
|
654
675
|
if (!restrictToolNames && xdevMounted && !tools.some(tool => tool.name === "read")) {
|
|
655
676
|
const readTool = await logger.time("createTools:read", BUILTIN_TOOLS.read, session);
|
|
656
677
|
if (readTool) {
|
|
657
|
-
|
|
678
|
+
const wrapped = wrapToolWithMetaNotice(readTool);
|
|
679
|
+
tools.push(wrapped);
|
|
680
|
+
toolRegistry.set(wrapped.name, wrapped);
|
|
681
|
+
builtInNames.add(wrapped.name);
|
|
658
682
|
}
|
|
659
683
|
}
|
|
684
|
+
if (xdevEnabled) {
|
|
685
|
+
const finalActiveNames = new Set(tools.map(tool => tool.name));
|
|
686
|
+
if (session.setActiveToolNames) session.setActiveToolNames(finalActiveNames);
|
|
687
|
+
else session.isToolActive = name => finalActiveNames.has(name);
|
|
688
|
+
}
|
|
660
689
|
|
|
661
690
|
return tools;
|
|
662
691
|
}
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -631,6 +631,26 @@ export function resolveOutputSinkHeadBytes(s: Settings | undefined): number {
|
|
|
631
631
|
return getSpillConfig(s).headBytes;
|
|
632
632
|
}
|
|
633
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Slack on top of the configured spill threshold before the final-defense
|
|
636
|
+
* inline byte cap fires. The OutputSink already bounds inline bodies to the
|
|
637
|
+
* threshold; only notice slop (wall time, exit code, elision marker,
|
|
638
|
+
* `[raw output: artifact://N]` footer) rides above it. The slack keeps the
|
|
639
|
+
* cap a genuine last resort for paths that bypass the sink (e.g. ACP
|
|
640
|
+
* client-bridge terminals) instead of re-truncating — and re-saving — every
|
|
641
|
+
* sink-elided result (the double-artifact `Artifact: N+1` vs `artifact://N`
|
|
642
|
+
* mismatch).
|
|
643
|
+
*/
|
|
644
|
+
const INLINE_CAP_SLACK_BYTES = 2 * 1024;
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Resolve the `enforceInlineByteCap` budget for streaming tools (bash/ssh)
|
|
648
|
+
* from session settings: the user's spill threshold plus notice slack.
|
|
649
|
+
*/
|
|
650
|
+
export function resolveInlineByteCapBudget(s: Settings | undefined): number {
|
|
651
|
+
return getSpillConfig(s).threshold + INLINE_CAP_SLACK_BYTES;
|
|
652
|
+
}
|
|
653
|
+
|
|
634
654
|
/**
|
|
635
655
|
* Resolve the per-line column cap from session settings. Shared by streaming
|
|
636
656
|
* executors (bash/python/ssh/eval via OutputSink) and the `read` tool's
|
package/src/tools/read.ts
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
import { fileHyperlink, renderCodeCell, renderMarkdownCell, renderStatusLine, tryResolveInternalUrlSync } from "../tui";
|
|
58
58
|
import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
|
|
59
59
|
import { buildLineEntriesWithBlockContext, type LineEntry, lineEntriesToPlainText } from "../utils/block-context";
|
|
60
|
+
import { isCpuProfilePath, renderCpuProfile } from "../utils/cpuprofile";
|
|
60
61
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
61
62
|
import {
|
|
62
63
|
ImageInputTooLargeError,
|
|
@@ -64,7 +65,9 @@ import {
|
|
|
64
65
|
MAX_IMAGE_INPUT_BYTES,
|
|
65
66
|
webpExclusionForModel,
|
|
66
67
|
} from "../utils/image-loading";
|
|
68
|
+
import { isInspectImageToolActive } from "../utils/inspect-image-mode";
|
|
67
69
|
import { CONVERTIBLE_EXTENSIONS, convertFileWithMarkit } from "../utils/markit";
|
|
70
|
+
import { isSampleProfilePath, renderSampleProfile } from "../utils/sample-profile";
|
|
68
71
|
import { type ArchiveReader, formatArchiveEntryLines, openArchive, parseArchivePathCandidates } from "../utils/zip";
|
|
69
72
|
import { buildDirectoryTree, type DirectoryTree } from "../workspace-tree";
|
|
70
73
|
import {
|
|
@@ -131,6 +134,7 @@ import {
|
|
|
131
134
|
} from "./sqlite-reader";
|
|
132
135
|
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
133
136
|
import { toolResult } from "./tool-result";
|
|
137
|
+
import { xdevDocs, xdevListing } from "./xdev";
|
|
134
138
|
|
|
135
139
|
// Per-session memo for tree-sitter summaries. `summarizeCode` is a pure function
|
|
136
140
|
// of (code, path, fold settings) but costs ~12-18ms for a ~1500-line file, and a
|
|
@@ -155,6 +159,8 @@ function getSummaryParseCache(session: object): LRUCache<string, SummaryResult |
|
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
|
|
162
|
+
/** Largest profile (`*.sample.txt`, `*.cpuprofile`) converted to a bottleneck summary; bigger files read as plain text. */
|
|
163
|
+
const MAX_PROFILE_SUMMARY_BYTES = 32 * 1024 * 1024;
|
|
158
164
|
const MAX_SUMMARY_LINES = 20_000;
|
|
159
165
|
const MAX_ARTIFACT_RAW_INLINE_BYTES = DEFAULT_MAX_BYTES;
|
|
160
166
|
/**
|
|
@@ -855,31 +861,74 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
855
861
|
pathTargetsSsh(String((args as { path?: unknown }).path ?? "")) ? "exec" : "read";
|
|
856
862
|
readonly label = "Read";
|
|
857
863
|
readonly loadMode = "essential";
|
|
858
|
-
|
|
864
|
+
description: string;
|
|
859
865
|
readonly parameters = readSchema;
|
|
860
866
|
readonly strict = true;
|
|
861
867
|
|
|
862
868
|
readonly #autoResizeImages: boolean;
|
|
863
869
|
readonly #defaultLimit: number;
|
|
864
|
-
|
|
870
|
+
#inspectImageActive: boolean;
|
|
865
871
|
|
|
866
872
|
constructor(private readonly session: ToolSession) {
|
|
867
|
-
const displayMode = resolveFileDisplayMode(session);
|
|
868
873
|
this.#autoResizeImages = session.settings.get("images.autoResize");
|
|
869
874
|
this.#defaultLimit = Math.max(
|
|
870
875
|
1,
|
|
871
876
|
Math.min(session.settings.get("read.defaultLimit") ?? DEFAULT_MAX_LINES, DEFAULT_MAX_LINES),
|
|
872
877
|
);
|
|
873
|
-
this.#
|
|
874
|
-
this.description =
|
|
878
|
+
this.#inspectImageActive = this.#resolveInspectImageAvailability();
|
|
879
|
+
this.description = this.#renderDescription();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Re-render the tool description for the current display mode and the
|
|
884
|
+
* effective inspect_image state (mode setting, `/vision` override, and
|
|
885
|
+
* active-model image capability all feed it, so it can change at runtime).
|
|
886
|
+
*/
|
|
887
|
+
#renderDescription(): string {
|
|
888
|
+
const displayMode = resolveFileDisplayMode(this.session);
|
|
889
|
+
return prompt.render(readDescription, {
|
|
875
890
|
DEFAULT_LIMIT: String(this.#defaultLimit),
|
|
876
891
|
DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
|
|
877
892
|
IS_HL_MODE: displayMode.hashLines,
|
|
878
893
|
IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
|
|
879
|
-
INSPECT_IMAGE_ENABLED: this.#
|
|
894
|
+
INSPECT_IMAGE_ENABLED: this.#inspectImageActive,
|
|
880
895
|
});
|
|
881
896
|
}
|
|
882
897
|
|
|
898
|
+
/**
|
|
899
|
+
* Whether the agent can actually reach `inspect_image` right now: exposed
|
|
900
|
+
* top-level, or mounted as an `xd://` device while the effective mode wants
|
|
901
|
+
* it (mounted devices stay executable via `write xd://inspect_image`, so a
|
|
902
|
+
* metadata-only read remains actionable). Sessions with neither
|
|
903
|
+
* availability signal (tests, embedded use) fall back to the mode
|
|
904
|
+
* computation alone. Restricted slates (subagents without the tool and
|
|
905
|
+
* without xdev) resolve to unavailable, so those sessions get inline image
|
|
906
|
+
* blocks instead of guidance pointing at an absent tool.
|
|
907
|
+
*/
|
|
908
|
+
#resolveInspectImageAvailability(): boolean {
|
|
909
|
+
const topLevel = this.session.isToolActive?.("inspect_image");
|
|
910
|
+
const xdev = this.session.xdev;
|
|
911
|
+
if (topLevel === undefined && xdev === undefined) return isInspectImageToolActive(this.session);
|
|
912
|
+
if (topLevel === true) return true;
|
|
913
|
+
return xdev?.mountedNames.has("inspect_image") === true && isInspectImageToolActive(this.session);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Re-evaluate the effective inspect_image state; it can flip when the model
|
|
918
|
+
* or the `/vision` override changes after this tool was constructed. Keeps
|
|
919
|
+
* the behavior branch and the advertised description in lockstep. Called
|
|
920
|
+
* per image read and by tool reconciliation before prompt rebuilds (which
|
|
921
|
+
* passes the post-change availability as `availableOverride`).
|
|
922
|
+
*/
|
|
923
|
+
syncInspectImageState(availableOverride?: boolean): boolean {
|
|
924
|
+
const active = availableOverride ?? this.#resolveInspectImageAvailability();
|
|
925
|
+
if (active !== this.#inspectImageActive) {
|
|
926
|
+
this.#inspectImageActive = active;
|
|
927
|
+
this.description = this.#renderDescription();
|
|
928
|
+
}
|
|
929
|
+
return active;
|
|
930
|
+
}
|
|
931
|
+
|
|
883
932
|
/**
|
|
884
933
|
* Recover the active approved plan when a model rewrites its `local://` URL
|
|
885
934
|
* as a same-basename path in the working-directory root.
|
|
@@ -1256,10 +1305,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1256
1305
|
|
|
1257
1306
|
/**
|
|
1258
1307
|
* Build content blocks for an on-disk image file: an `inspect_image`
|
|
1259
|
-
* metadata note when inspection is
|
|
1308
|
+
* metadata note when inspection is active, otherwise the decoded image
|
|
1260
1309
|
* block. Shared by the plain-file read path and the `local://` image fast
|
|
1261
|
-
* path so both honor
|
|
1262
|
-
* identically. Too-large / unsupported images surface as {@link ToolError}.
|
|
1310
|
+
* path so both honor the effective inspect_image state, the size cap, and
|
|
1311
|
+
* auto-resize identically. Too-large / unsupported images surface as {@link ToolError}.
|
|
1263
1312
|
*/
|
|
1264
1313
|
async #loadImageContent(options: {
|
|
1265
1314
|
readPath: string;
|
|
@@ -1269,7 +1318,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1269
1318
|
fileSize: number;
|
|
1270
1319
|
}): Promise<{ content: Array<TextContent | ImageContent>; details: ReadToolDetails; sourcePath: string }> {
|
|
1271
1320
|
const { readPath, absolutePath, mimeType, imageMetadata, fileSize } = options;
|
|
1272
|
-
if (this
|
|
1321
|
+
if (this.syncInspectImageState()) {
|
|
1273
1322
|
const outputMime = imageMetadata?.mimeType ?? mimeType;
|
|
1274
1323
|
const metadataLines = [
|
|
1275
1324
|
"Image metadata:",
|
|
@@ -2432,6 +2481,31 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2432
2481
|
const mimeType = imageMetadata?.mimeType;
|
|
2433
2482
|
const ext = path.extname(absolutePath).toLowerCase();
|
|
2434
2483
|
const shouldConvertWithMarkit = CONVERTIBLE_EXTENSIONS.has(ext);
|
|
2484
|
+
|
|
2485
|
+
// Profiler reports (macOS `sample` call trees, V8 `.cpuprofile` JSON):
|
|
2486
|
+
// replace the raw dump with a bottleneck summary (hot paths, top self
|
|
2487
|
+
// time/samples). `:raw` reads the original bytes; text that merely wears
|
|
2488
|
+
// the extension falls through to the plain-text path.
|
|
2489
|
+
if (!mimeType && !isRawSelector(parsed) && fileSize <= MAX_PROFILE_SUMMARY_BYTES) {
|
|
2490
|
+
let rendered: string | null = null;
|
|
2491
|
+
if (isSampleProfilePath(absolutePath)) rendered = renderSampleProfile(await Bun.file(absolutePath).text());
|
|
2492
|
+
else if (isCpuProfilePath(absolutePath)) rendered = renderCpuProfile(await Bun.file(absolutePath).text());
|
|
2493
|
+
if (rendered) {
|
|
2494
|
+
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
2495
|
+
return this.#buildInMemoryMultiRangeResult(rendered, parsed.ranges, {
|
|
2496
|
+
details: { resolvedPath: absolutePath },
|
|
2497
|
+
sourcePath: absolutePath,
|
|
2498
|
+
entityLabel: "profile summary",
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
const { offset, limit } = selToOffsetLimit(parsed);
|
|
2502
|
+
return this.#buildInMemoryTextResult(rendered, offset, limit, {
|
|
2503
|
+
details: { resolvedPath: absolutePath },
|
|
2504
|
+
sourcePath: absolutePath,
|
|
2505
|
+
entityLabel: "profile summary",
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2435
2509
|
// Read the file based on type
|
|
2436
2510
|
let content: Array<TextContent | ImageContent> | undefined;
|
|
2437
2511
|
let details: ReadToolDetails = {};
|
|
@@ -3230,9 +3304,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
3230
3304
|
read: async name => {
|
|
3231
3305
|
if (name === REPORT_ISSUE_DEVICE_NAME) return reportIssueDeviceUsage();
|
|
3232
3306
|
if (name && isResolutionDeviceName(name)) return resolutionDeviceUsage(name);
|
|
3233
|
-
const
|
|
3234
|
-
if (!
|
|
3235
|
-
return name === null ?
|
|
3307
|
+
const xdev = this.session.xdev;
|
|
3308
|
+
if (!xdev) throw new ToolError("xd:// is not mounted in this session.");
|
|
3309
|
+
return name === null ? xdevListing(xdev) : xdevDocs(xdev, name);
|
|
3236
3310
|
},
|
|
3237
3311
|
},
|
|
3238
3312
|
});
|
package/src/tools/write.ts
CHANGED
|
@@ -88,7 +88,14 @@ import {
|
|
|
88
88
|
} from "./sqlite-reader";
|
|
89
89
|
import { ToolError } from "./tool-errors";
|
|
90
90
|
import { toolResult } from "./tool-result";
|
|
91
|
-
import {
|
|
91
|
+
import {
|
|
92
|
+
dispatchXdevTool,
|
|
93
|
+
renderXdevCall,
|
|
94
|
+
renderXdevResult,
|
|
95
|
+
resolveXdevTool,
|
|
96
|
+
type XdevDispatch,
|
|
97
|
+
xdevListing,
|
|
98
|
+
} from "./xdev";
|
|
92
99
|
|
|
93
100
|
const LOOSE_HASHLINE_HEADER_RE = /^\s*\[[^#\r\n]+#[^ \t\r\n]*\]\s*$/;
|
|
94
101
|
const EXECUTABLE_NOTICE = "[Notice: Made executable via chmod +x]";
|
|
@@ -153,7 +160,42 @@ function throwReadSelectorMisfire(target: string, sel: string): never {
|
|
|
153
160
|
);
|
|
154
161
|
}
|
|
155
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Recognize a semicolon-joined list of read-tool selectors mis-dispatched as a
|
|
165
|
+
* single write target — the multi-file read expression the scout emitted in
|
|
166
|
+
* issue #6809 (`a.txt:1-2;b/c.txt:3-4`). Every `;`-segment must be non-empty and
|
|
167
|
+
* carry its own read selector ({@link splitPathAndSel} peels a `:N-M`, `:raw`,
|
|
168
|
+
* or `:conflicts` tail). No real call targets such a list: `read` accepts one
|
|
169
|
+
* path, `write` writes one file. Unlike {@link readSelectorForEmptyWrite} this
|
|
170
|
+
* fires regardless of `content` — the non-empty-content escape hatch exists for
|
|
171
|
+
* a lone selector-shaped *filename*, never a `;`-list, and honoring it here
|
|
172
|
+
* silently creates a nested directory tree (`a.txt:1-2;b/`) in the workspace.
|
|
173
|
+
* The caller still probes the literal target first, so an existing POSIX file
|
|
174
|
+
* by that exact name stays writable (same escape as the single-selector guard).
|
|
175
|
+
*/
|
|
176
|
+
function readSelectorListMisfire(target: string): number | undefined {
|
|
177
|
+
if (!target.includes(";")) return undefined;
|
|
178
|
+
const segments = target.split(";");
|
|
179
|
+
if (segments.length < 2) return undefined;
|
|
180
|
+
for (const segment of segments) {
|
|
181
|
+
const trimmed = segment.trim();
|
|
182
|
+
if (trimmed.length === 0 || splitPathAndSel(trimmed).sel === undefined) return undefined;
|
|
183
|
+
}
|
|
184
|
+
return segments.length;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function throwReadSelectorListMisfire(target: string, count: number): never {
|
|
188
|
+
throw new ToolError(
|
|
189
|
+
`write target '${target}' is a semicolon-joined list of ${count} read-tool selectors, not a filesystem path — refusing to create it. ` +
|
|
190
|
+
`write creates a single file; issue one read() per path to read these ranges (e.g. read({ path: "<one path>:<range>" })).`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
156
194
|
async function assertNotReadSelectorMisfire(target: string, content: string, cwd: string): Promise<void> {
|
|
195
|
+
const listCount = readSelectorListMisfire(target);
|
|
196
|
+
if (listCount !== undefined && (await probeLiteralPathExists(target, cwd)) === "missing") {
|
|
197
|
+
throwReadSelectorListMisfire(target, listCount);
|
|
198
|
+
}
|
|
157
199
|
const sel = readSelectorForEmptyWrite(target, content);
|
|
158
200
|
if (sel === undefined) return;
|
|
159
201
|
if ((await probeLiteralPathExists(target, cwd)) !== "missing") return;
|
|
@@ -471,7 +513,8 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
471
513
|
if (xdevTarget) {
|
|
472
514
|
if (xdevTarget.name === REPORT_ISSUE_DEVICE_NAME) return "write";
|
|
473
515
|
if (xdevTarget.name && isResolutionDeviceName(xdevTarget.name)) return "read";
|
|
474
|
-
const inst =
|
|
516
|
+
const inst =
|
|
517
|
+
xdevTarget.name && this.session.xdev ? resolveXdevTool(this.session.xdev, xdevTarget.name) : undefined;
|
|
475
518
|
if (!inst) return "exec";
|
|
476
519
|
// Decode the device JSON payload and evaluate the mounted tool's own
|
|
477
520
|
// approval (which may be argument-dependent, e.g. ast_edit is read-tier
|
|
@@ -1104,14 +1147,15 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
1104
1147
|
};
|
|
1105
1148
|
return;
|
|
1106
1149
|
}
|
|
1107
|
-
const
|
|
1108
|
-
if (!
|
|
1150
|
+
const xdev = this.session.xdev;
|
|
1151
|
+
if (!xdev) {
|
|
1109
1152
|
throw new ToolError("xd:// is not mounted in this session.");
|
|
1110
1153
|
}
|
|
1111
1154
|
if (!name) {
|
|
1112
|
-
throw new ToolError(`Cannot write to xd:// itself — pick a device:\n${
|
|
1155
|
+
throw new ToolError(`Cannot write to xd:// itself — pick a device:\n${xdevListing(xdev)}`);
|
|
1113
1156
|
}
|
|
1114
|
-
const { result, xdev } = await
|
|
1157
|
+
const { result, xdev: dispatch } = await dispatchXdevTool(
|
|
1158
|
+
xdev,
|
|
1115
1159
|
name,
|
|
1116
1160
|
deviceContent,
|
|
1117
1161
|
_toolCallId,
|
|
@@ -1124,7 +1168,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
1124
1168
|
);
|
|
1125
1169
|
xdResult = {
|
|
1126
1170
|
content: result.content,
|
|
1127
|
-
details: { xdev },
|
|
1171
|
+
details: { xdev: dispatch },
|
|
1128
1172
|
isError: result.isError,
|
|
1129
1173
|
useless: result.useless,
|
|
1130
1174
|
};
|