@oh-my-pi/pi-coding-agent 14.9.8 → 15.0.0
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 +101 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +11 -0
- package/scripts/format-prompts.ts +1 -1
- package/src/cli/args.ts +2 -2
- package/src/cli/stats-cli.ts +2 -0
- package/src/cli.ts +24 -1
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/config/model-resolver.ts +30 -0
- package/src/config/settings-schema.ts +61 -9
- package/src/config/settings.ts +18 -1
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +10 -0
- package/src/edit/streaming.ts +1 -1
- package/src/eval/js/context-manager.ts +10 -9
- package/src/eval/js/shared/rewrite-imports.ts +120 -48
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/extensibility/extensions/runner.ts +54 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/skills.ts +33 -1
- package/src/hashline/grammar.lark +1 -1
- package/src/hashline/input.ts +11 -5
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +13 -9
- package/src/modes/acp/acp-agent.ts +361 -54
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/controllers/command-controller.ts +14 -6
- package/src/modes/controllers/event-controller.ts +24 -11
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +72 -39
- package/src/modes/interactive-mode.ts +71 -7
- package/src/modes/rpc/rpc-mode.ts +17 -2
- package/src/modes/types.ts +6 -2
- package/src/modes/utils/ui-helpers.ts +15 -3
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +22 -21
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +2 -2
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/exit-plan-mode.md +2 -2
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/hashline.md +20 -20
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +7 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +69 -12
- package/src/session/agent-session.ts +231 -22
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +699 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/task/executor.ts +10 -3
- package/src/task/index.ts +29 -51
- package/src/task/render.ts +6 -3
- package/src/task/worktree.ts +170 -239
- package/src/tools/bash.ts +176 -2
- package/src/tools/browser/tab-supervisor.ts +13 -13
- package/src/tools/conflict-detect.ts +6 -6
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +19 -1
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +682 -176
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/read.ts +110 -27
- package/src/tools/write.ts +23 -1
- package/src/tui/code-cell.ts +70 -2
- package/src/utils/git.ts +5 -0
- package/src/task/isolation-backend.ts +0 -94
package/src/tools/read.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
|
6
6
|
import { glob, type SummaryResult, summarizeCode } from "@oh-my-pi/pi-natives";
|
|
7
7
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
8
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
9
|
-
import { getRemoteDir, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
9
|
+
import { getRemoteDir, logger, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { type Static, Type } from "@sinclair/typebox";
|
|
11
11
|
import { getFileReadCache } from "../edit/file-read-cache";
|
|
12
12
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
truncateHead,
|
|
27
27
|
truncateHeadBytes,
|
|
28
28
|
} from "../session/streaming-output";
|
|
29
|
-
import { renderCodeCell, renderStatusLine } from "../tui";
|
|
29
|
+
import { renderCodeCell, renderMarkdownCell, renderStatusLine } from "../tui";
|
|
30
30
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
31
31
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
32
32
|
import { ImageInputTooLargeError, loadImageInput, MAX_IMAGE_INPUT_BYTES } from "../utils/image-loading";
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
import { applyListLimit } from "./list-limit";
|
|
57
57
|
import { formatFullOutputReference, formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
|
|
58
58
|
import { expandPath, formatPathRelativeToCwd, resolveReadPath, splitPathAndSel } from "./path-utils";
|
|
59
|
-
import { formatBytes, shortenPath, wrapBrackets } from "./render-utils";
|
|
59
|
+
import { formatBytes, replaceTabs, shortenPath, wrapBrackets } from "./render-utils";
|
|
60
60
|
import {
|
|
61
61
|
executeReadQuery,
|
|
62
62
|
getRowByKey,
|
|
@@ -1045,12 +1045,25 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1045
1045
|
}
|
|
1046
1046
|
}
|
|
1047
1047
|
|
|
1048
|
+
#routeReadThroughBridge(
|
|
1049
|
+
absolutePath: string,
|
|
1050
|
+
options?: { line?: number; limit?: number },
|
|
1051
|
+
): Promise<string> | undefined {
|
|
1052
|
+
const bridge = this.session.getClientBridge?.();
|
|
1053
|
+
if (!bridge?.capabilities.readTextFile || !bridge.readTextFile) return undefined;
|
|
1054
|
+
return bridge.readTextFile({ path: absolutePath, ...options });
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1048
1057
|
async #trySummarize(absolutePath: string, fileSize: number, signal?: AbortSignal): Promise<SummaryResult | null> {
|
|
1049
1058
|
if (fileSize > MAX_SUMMARY_BYTES) return null;
|
|
1050
1059
|
|
|
1051
1060
|
try {
|
|
1052
1061
|
throwIfAborted(signal);
|
|
1053
|
-
const
|
|
1062
|
+
const bridgePromise = this.#routeReadThroughBridge(absolutePath);
|
|
1063
|
+
const code =
|
|
1064
|
+
bridgePromise !== undefined
|
|
1065
|
+
? await bridgePromise.catch(() => Bun.file(absolutePath).text())
|
|
1066
|
+
: await Bun.file(absolutePath).text();
|
|
1054
1067
|
throwIfAborted(signal);
|
|
1055
1068
|
if (countTextLines(code) > MAX_SUMMARY_LINES) return null;
|
|
1056
1069
|
|
|
@@ -1173,7 +1186,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1173
1186
|
if (conflictUri) {
|
|
1174
1187
|
if (conflictUri.id === "*") {
|
|
1175
1188
|
throw new ToolError(
|
|
1176
|
-
"`
|
|
1189
|
+
"Reading `conflict://*` is not supported — wildcards are write-only. Use the `<path>:conflicts` read selector for the full list of conflicts in a file, or read `conflict://<N>` to inspect a single block.",
|
|
1177
1190
|
);
|
|
1178
1191
|
}
|
|
1179
1192
|
return this.#readConflictRegion(conflictUri.id, conflictUri.scope);
|
|
@@ -1211,7 +1224,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1211
1224
|
if (internalRouter.canHandle(internalTarget.path)) {
|
|
1212
1225
|
const parsed = parseSel(internalTarget.sel);
|
|
1213
1226
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1214
|
-
return this.#handleInternalUrl(internalTarget.path, offset, limit, { raw: isRawSelector(parsed) });
|
|
1227
|
+
return this.#handleInternalUrl(internalTarget.path, offset, limit, { raw: isRawSelector(parsed) }, signal);
|
|
1215
1228
|
}
|
|
1216
1229
|
|
|
1217
1230
|
const archivePath = await this.#resolveArchiveReadPath(readPath, signal);
|
|
@@ -1413,6 +1426,29 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1413
1426
|
if (!content) {
|
|
1414
1427
|
// Raw text or line-range mode
|
|
1415
1428
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1429
|
+
// Try ACP bridge first — editor's in-memory buffer is source of truth.
|
|
1430
|
+
// Request full text so local range rendering keeps normal context and line numbers.
|
|
1431
|
+
const bridgePromise = this.#routeReadThroughBridge(absolutePath);
|
|
1432
|
+
if (bridgePromise !== undefined) {
|
|
1433
|
+
try {
|
|
1434
|
+
const bridgeText = await bridgePromise;
|
|
1435
|
+
const bridgeResult = this.#buildInMemoryTextResult(bridgeText, offset, limit, {
|
|
1436
|
+
details: { resolvedPath: absolutePath, suffixResolution },
|
|
1437
|
+
sourcePath: absolutePath,
|
|
1438
|
+
entityLabel: "file",
|
|
1439
|
+
raw: isRawSelector(parsed),
|
|
1440
|
+
});
|
|
1441
|
+
if (suffixResolution) {
|
|
1442
|
+
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
1443
|
+
const firstText = bridgeResult.content.find((c): c is TextContent => c.type === "text");
|
|
1444
|
+
if (firstText) firstText.text = `${notice}\n${firstText.text}`;
|
|
1445
|
+
}
|
|
1446
|
+
return bridgeResult;
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
logger.warn("ACP fs readTextFile failed; falling back to disk", { path: absolutePath, error });
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1416
1452
|
// User-requested 0-indexed range start. Lines BEFORE this become
|
|
1417
1453
|
// leading context (added below if offset is explicit).
|
|
1418
1454
|
const requestedStart = offset ? Math.max(0, offset - 1) : 0;
|
|
@@ -1633,7 +1669,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1633
1669
|
}
|
|
1634
1670
|
|
|
1635
1671
|
/**
|
|
1636
|
-
* Implement
|
|
1672
|
+
* Implement the `<path>:conflicts` read selector: scan the whole file once, register
|
|
1637
1673
|
* every block in the session's conflict history, and return a compact
|
|
1638
1674
|
* `#N L_a-L_b` index instead of file content. Designed for heavily
|
|
1639
1675
|
* conflicted files where dumping every body would be wasteful.
|
|
@@ -1677,6 +1713,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1677
1713
|
offset?: number,
|
|
1678
1714
|
limit?: number,
|
|
1679
1715
|
options?: { raw?: boolean },
|
|
1716
|
+
signal?: AbortSignal,
|
|
1680
1717
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1681
1718
|
const internalRouter = InternalUrlRouter.instance();
|
|
1682
1719
|
|
|
@@ -1703,8 +1740,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1703
1740
|
}
|
|
1704
1741
|
|
|
1705
1742
|
// Resolve the internal URL
|
|
1706
|
-
const resource = await internalRouter.resolve(url
|
|
1707
|
-
|
|
1743
|
+
const resource = await internalRouter.resolve(url, {
|
|
1744
|
+
cwd: this.session.cwd,
|
|
1745
|
+
settings: this.session.settings,
|
|
1746
|
+
signal,
|
|
1747
|
+
});
|
|
1748
|
+
const details: ReadToolDetails = { resolvedPath: resource.sourcePath, contentType: resource.contentType };
|
|
1708
1749
|
|
|
1709
1750
|
// If extraction was used, return directly (no pagination)
|
|
1710
1751
|
if (hasExtraction) {
|
|
@@ -1803,20 +1844,44 @@ export const readToolRenderer = {
|
|
|
1803
1844
|
},
|
|
1804
1845
|
|
|
1805
1846
|
renderResult(
|
|
1806
|
-
result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails },
|
|
1807
|
-
|
|
1847
|
+
result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails; isError?: boolean },
|
|
1848
|
+
options: RenderResultOptions,
|
|
1808
1849
|
uiTheme: Theme,
|
|
1809
1850
|
args?: ReadRenderArgs,
|
|
1810
1851
|
): Component {
|
|
1811
1852
|
const urlDetails = result.details as ReadUrlToolDetails | undefined;
|
|
1812
1853
|
if (urlDetails?.kind === "url" || isReadableUrlPath(args?.file_path || args?.path || "")) {
|
|
1813
1854
|
return renderReadUrlResult(
|
|
1814
|
-
result as {
|
|
1815
|
-
|
|
1855
|
+
result as {
|
|
1856
|
+
content: Array<{ type: string; text?: string }>;
|
|
1857
|
+
details?: ReadUrlToolDetails;
|
|
1858
|
+
isError?: boolean;
|
|
1859
|
+
},
|
|
1860
|
+
options,
|
|
1816
1861
|
uiTheme,
|
|
1817
1862
|
);
|
|
1818
1863
|
}
|
|
1819
1864
|
|
|
1865
|
+
if (result.isError) {
|
|
1866
|
+
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1867
|
+
const errorText = (rawErrorText || "Unknown error").replace(/^Error:\s*/, "");
|
|
1868
|
+
const rawPath = args?.file_path || args?.path || "";
|
|
1869
|
+
const filePath = shortenPath(rawPath);
|
|
1870
|
+
let title = filePath ? `Read ${filePath}` : "Read";
|
|
1871
|
+
if (args?.offset !== undefined || args?.limit !== undefined) {
|
|
1872
|
+
const startLine = args.offset ?? 1;
|
|
1873
|
+
const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
|
|
1874
|
+
title += `:${startLine}${endLine ? `-${endLine}` : ""}`;
|
|
1875
|
+
}
|
|
1876
|
+
const header = renderStatusLine({ icon: "error", title }, uiTheme);
|
|
1877
|
+
const errorLines = errorText.split("\n").map(line => uiTheme.fg("error", replaceTabs(line)));
|
|
1878
|
+
const outputBlock = new CachedOutputBlock();
|
|
1879
|
+
return {
|
|
1880
|
+
render: (width: number) =>
|
|
1881
|
+
outputBlock.render({ header, state: "error", sections: [{ lines: errorLines }], width }, uiTheme),
|
|
1882
|
+
invalidate: () => outputBlock.invalidate(),
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1820
1885
|
const details = result.details;
|
|
1821
1886
|
const rawText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1822
1887
|
// Prefer structured `displayContent` from details when available so the TUI
|
|
@@ -1825,7 +1890,7 @@ export const readToolRenderer = {
|
|
|
1825
1890
|
const imageContent = result.content?.find(c => c.type === "image");
|
|
1826
1891
|
const rawPath = args?.file_path || args?.path || "";
|
|
1827
1892
|
const filePath = shortenPath(rawPath);
|
|
1828
|
-
const lang = getLanguageFromPath(rawPath);
|
|
1893
|
+
const lang = getLanguageFromPath(splitPathAndSel(rawPath).path);
|
|
1829
1894
|
|
|
1830
1895
|
const warningLines: string[] = [];
|
|
1831
1896
|
const truncation = details?.meta?.truncation;
|
|
@@ -1893,28 +1958,46 @@ export const readToolRenderer = {
|
|
|
1893
1958
|
const n = details.conflictCount;
|
|
1894
1959
|
title += ` ${uiTheme.fg("warning", `(⚠ ${n} conflict${n === 1 ? "" : "s"})`)}`;
|
|
1895
1960
|
}
|
|
1961
|
+
const rawRequested = args?.raw === true || isRawSelector(parseSel(splitPathAndSel(rawPath).sel));
|
|
1962
|
+
const isMarkdown = details?.contentType === "text/markdown" && !rawRequested;
|
|
1896
1963
|
let cachedWidth: number | undefined;
|
|
1964
|
+
let cachedExpanded: boolean | undefined;
|
|
1897
1965
|
let cachedLines: string[] | undefined;
|
|
1898
1966
|
return {
|
|
1899
1967
|
render: (width: number) => {
|
|
1900
|
-
|
|
1901
|
-
cachedLines
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1968
|
+
const expanded = options.expanded;
|
|
1969
|
+
if (cachedLines && cachedWidth === width && cachedExpanded === expanded) return cachedLines;
|
|
1970
|
+
cachedLines = isMarkdown
|
|
1971
|
+
? renderMarkdownCell(
|
|
1972
|
+
{
|
|
1973
|
+
content: contentText,
|
|
1974
|
+
title,
|
|
1975
|
+
status: "complete",
|
|
1976
|
+
output: warningLines.length > 0 ? warningLines.join("\n") : undefined,
|
|
1977
|
+
expanded,
|
|
1978
|
+
width,
|
|
1979
|
+
},
|
|
1980
|
+
uiTheme,
|
|
1981
|
+
)
|
|
1982
|
+
: renderCodeCell(
|
|
1983
|
+
{
|
|
1984
|
+
code: contentText,
|
|
1985
|
+
language: lang,
|
|
1986
|
+
title,
|
|
1987
|
+
status: "complete",
|
|
1988
|
+
output: warningLines.length > 0 ? warningLines.join("\n") : undefined,
|
|
1989
|
+
expanded,
|
|
1990
|
+
width,
|
|
1991
|
+
},
|
|
1992
|
+
uiTheme,
|
|
1993
|
+
);
|
|
1913
1994
|
cachedWidth = width;
|
|
1995
|
+
cachedExpanded = expanded;
|
|
1914
1996
|
return cachedLines;
|
|
1915
1997
|
},
|
|
1916
1998
|
invalidate: () => {
|
|
1917
1999
|
cachedWidth = undefined;
|
|
2000
|
+
cachedExpanded = undefined;
|
|
1918
2001
|
cachedLines = undefined;
|
|
1919
2002
|
},
|
|
1920
2003
|
};
|
package/src/tools/write.ts
CHANGED
|
@@ -609,6 +609,11 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
609
609
|
};
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
+
#routeWriteThroughBridge(absolutePath: string, content: string): Promise<void> | undefined {
|
|
613
|
+
const bridge = this.session.getClientBridge?.();
|
|
614
|
+
if (!bridge?.capabilities.writeTextFile || !bridge.writeTextFile) return undefined;
|
|
615
|
+
return bridge.writeTextFile({ path: absolutePath, content });
|
|
616
|
+
}
|
|
612
617
|
async execute(
|
|
613
618
|
_toolCallId: string,
|
|
614
619
|
{ path, content }: WriteParams,
|
|
@@ -623,7 +628,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
623
628
|
if (conflictUri) {
|
|
624
629
|
if (conflictUri.scope) {
|
|
625
630
|
throw new ToolError(
|
|
626
|
-
`Conflict URI scope '/${conflictUri.scope}' is read-only —
|
|
631
|
+
`Conflict URI scope '/${conflictUri.scope}' is read-only — read \`conflict://${conflictUri.id}/${conflictUri.scope}\` to inspect that side. To write, drop the scope (\`conflict://${conflictUri.id}\`) and put the chosen content (or shorthand like \`@${conflictUri.scope}\`) in \`content\`.`,
|
|
627
632
|
);
|
|
628
633
|
}
|
|
629
634
|
if (conflictUri.id === "*") {
|
|
@@ -682,6 +687,23 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
682
687
|
await assertEditableFile(absolutePath, path);
|
|
683
688
|
}
|
|
684
689
|
|
|
690
|
+
// Try ACP bridge first — no disk write when client handles it
|
|
691
|
+
const bridgePromise = this.#routeWriteThroughBridge(absolutePath, cleanContent);
|
|
692
|
+
if (bridgePromise !== undefined) {
|
|
693
|
+
try {
|
|
694
|
+
await bridgePromise;
|
|
695
|
+
} catch (error) {
|
|
696
|
+
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
697
|
+
}
|
|
698
|
+
invalidateFsScanAfterWrite(absolutePath);
|
|
699
|
+
const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
700
|
+
let resultText = `Successfully wrote ${cleanContent.length} bytes to ${displayPath}`;
|
|
701
|
+
if (stripped) {
|
|
702
|
+
resultText += `\nNote: auto-stripped hashline display prefixes from content before writing.`;
|
|
703
|
+
}
|
|
704
|
+
return { content: [{ type: "text", text: resultText }], details: {} };
|
|
705
|
+
}
|
|
706
|
+
|
|
685
707
|
const diagnostics = await this.#writethrough(absolutePath, cleanContent, signal, undefined, batchRequest);
|
|
686
708
|
invalidateFsScanAfterWrite(absolutePath);
|
|
687
709
|
|
package/src/tui/code-cell.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Render a code cell with optional output section.
|
|
2
|
+
* Render a code or markdown cell with optional output section.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { Markdown } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { getMarkdownTheme, highlightCode, type Theme } from "../modes/theme/theme";
|
|
5
6
|
import {
|
|
6
7
|
formatDuration,
|
|
7
8
|
formatExpandHint,
|
|
@@ -116,3 +117,70 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
|
|
|
116
117
|
|
|
117
118
|
return renderOutputBlock({ header: title, headerMeta: meta, state, sections, width }, theme);
|
|
118
119
|
}
|
|
120
|
+
|
|
121
|
+
export interface MarkdownCellOptions {
|
|
122
|
+
content: string;
|
|
123
|
+
index?: number;
|
|
124
|
+
total?: number;
|
|
125
|
+
title?: string;
|
|
126
|
+
status?: "pending" | "running" | "warning" | "complete" | "error";
|
|
127
|
+
spinnerFrame?: number;
|
|
128
|
+
duration?: number;
|
|
129
|
+
output?: string;
|
|
130
|
+
outputMaxLines?: number;
|
|
131
|
+
contentMaxLines?: number;
|
|
132
|
+
expanded?: boolean;
|
|
133
|
+
width: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function renderMarkdownCell(options: MarkdownCellOptions, theme: Theme): string[] {
|
|
137
|
+
const { content, output, expanded = false, outputMaxLines = 6, contentMaxLines = 12, width } = options;
|
|
138
|
+
const codeOptions: CodeCellOptions = {
|
|
139
|
+
code: "",
|
|
140
|
+
index: options.index,
|
|
141
|
+
total: options.total,
|
|
142
|
+
title: options.title,
|
|
143
|
+
status: options.status,
|
|
144
|
+
spinnerFrame: options.spinnerFrame,
|
|
145
|
+
duration: options.duration,
|
|
146
|
+
width,
|
|
147
|
+
};
|
|
148
|
+
const { title, meta } = formatHeader(codeOptions, theme);
|
|
149
|
+
const state = getState(options.status);
|
|
150
|
+
|
|
151
|
+
// Markdown component manages its own wrapping at the inner content width.
|
|
152
|
+
// `renderOutputBlock` adds a `│ ` prefix + `│` suffix → 3 visible columns.
|
|
153
|
+
const innerWidth = Math.max(20, width - 3);
|
|
154
|
+
const allLines = content.trim() ? new Markdown(content, 0, 0, getMarkdownTheme()).render(innerWidth) : [];
|
|
155
|
+
const maxContentLines = expanded ? allLines.length : Math.min(allLines.length, contentMaxLines);
|
|
156
|
+
const contentLines = allLines.slice(0, maxContentLines);
|
|
157
|
+
const hiddenContentLines = allLines.length - maxContentLines;
|
|
158
|
+
if (hiddenContentLines > 0) {
|
|
159
|
+
const hint = formatExpandHint(theme, expanded, hiddenContentLines > 0);
|
|
160
|
+
const moreLine = `${formatMoreItems(hiddenContentLines, "line")}${hint ? ` ${hint}` : ""}`;
|
|
161
|
+
contentLines.push(theme.fg("dim", moreLine));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const outputLines: string[] = [];
|
|
165
|
+
if (output?.trim()) {
|
|
166
|
+
const rawLines = output.split("\n");
|
|
167
|
+
const maxLines = expanded ? rawLines.length : Math.min(rawLines.length, outputMaxLines);
|
|
168
|
+
const displayLines = rawLines
|
|
169
|
+
.slice(0, maxLines)
|
|
170
|
+
.map(line => (line.includes("\x1b[") ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line))));
|
|
171
|
+
outputLines.push(...displayLines);
|
|
172
|
+
const remaining = rawLines.length - maxLines;
|
|
173
|
+
if (remaining > 0) {
|
|
174
|
+
const hint = formatExpandHint(theme, expanded, remaining > 0);
|
|
175
|
+
const moreLine = `${formatMoreItems(remaining, "line")}${hint ? ` ${hint}` : ""}`;
|
|
176
|
+
outputLines.push(theme.fg("dim", moreLine));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sections: Array<{ label?: string; lines: string[] }> = [{ lines: contentLines }];
|
|
181
|
+
if (outputLines.length > 0) {
|
|
182
|
+
sections.push({ label: theme.fg("toolTitle", "Output"), lines: outputLines });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return renderOutputBlock({ header: title, headerMeta: meta, state, sections, width }, theme);
|
|
186
|
+
}
|
package/src/utils/git.ts
CHANGED
|
@@ -895,6 +895,11 @@ export async function readTree(
|
|
|
895
895
|
await runEffect(cwd, ["read-tree", treeish], options);
|
|
896
896
|
}
|
|
897
897
|
|
|
898
|
+
/** Write the current index as a tree and return its object id. */
|
|
899
|
+
export async function writeTree(cwd: string, options: Pick<CommandOptions, "env" | "signal"> = {}): Promise<string> {
|
|
900
|
+
return (await runText(cwd, ["write-tree"], options)).trim();
|
|
901
|
+
}
|
|
902
|
+
|
|
898
903
|
// ════════════════════════════════════════════════════════════════════════════
|
|
899
904
|
// API: show
|
|
900
905
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { projfsOverlayProbe } from "@oh-my-pi/pi-natives";
|
|
2
|
-
import { Snowflake } from "@oh-my-pi/pi-utils";
|
|
3
|
-
import { cleanupProjfsOverlay, ensureProjfsOverlay, isProjfsUnavailableError } from "./worktree";
|
|
4
|
-
|
|
5
|
-
export type TaskIsolationMode = "none" | "worktree" | "fuse-overlay" | "fuse-projfs";
|
|
6
|
-
|
|
7
|
-
export interface IsolationBackendResolution {
|
|
8
|
-
effectiveIsolationMode: TaskIsolationMode;
|
|
9
|
-
warning: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type ProcessorEnv = Partial<Pick<NodeJS.ProcessEnv, "PROCESSOR_ARCHITECTURE" | "PROCESSOR_ARCHITEW6432">>;
|
|
13
|
-
|
|
14
|
-
function isWindowsArm64HostUnderX64Emulation(
|
|
15
|
-
platform: NodeJS.Platform,
|
|
16
|
-
arch: NodeJS.Architecture,
|
|
17
|
-
env: ProcessorEnv,
|
|
18
|
-
): boolean {
|
|
19
|
-
if (platform !== "win32" || arch !== "x64") return false;
|
|
20
|
-
return (
|
|
21
|
-
env.PROCESSOR_ARCHITECTURE?.toUpperCase() === "ARM64" || env.PROCESSOR_ARCHITEW6432?.toUpperCase() === "ARM64"
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function resolveIsolationBackendForTaskExecution(
|
|
26
|
-
requestedMode: TaskIsolationMode,
|
|
27
|
-
isIsolated: boolean,
|
|
28
|
-
repoRoot: string | null,
|
|
29
|
-
platform: NodeJS.Platform = process.platform,
|
|
30
|
-
arch: NodeJS.Architecture = process.arch,
|
|
31
|
-
env: ProcessorEnv = process.env as ProcessorEnv,
|
|
32
|
-
): Promise<IsolationBackendResolution> {
|
|
33
|
-
let effectiveIsolationMode = requestedMode;
|
|
34
|
-
let warning = "";
|
|
35
|
-
if (!(isIsolated && repoRoot)) {
|
|
36
|
-
return { effectiveIsolationMode, warning };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (requestedMode === "fuse-overlay" && platform === "win32") {
|
|
40
|
-
effectiveIsolationMode = "worktree";
|
|
41
|
-
warning =
|
|
42
|
-
'<system-notification>fuse-overlay isolation is unavailable on Windows. Use task.isolation.mode = "fuse-projfs" for ProjFS. Falling back to worktree isolation.</system-notification>';
|
|
43
|
-
return { effectiveIsolationMode, warning };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (requestedMode === "fuse-projfs" && platform !== "win32") {
|
|
47
|
-
effectiveIsolationMode = "worktree";
|
|
48
|
-
warning =
|
|
49
|
-
"<system-notification>fuse-projfs isolation is only available on Windows. Falling back to worktree isolation.</system-notification>";
|
|
50
|
-
return { effectiveIsolationMode, warning };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (!(requestedMode === "fuse-projfs" && platform === "win32")) {
|
|
54
|
-
return { effectiveIsolationMode, warning };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (isWindowsArm64HostUnderX64Emulation(platform, arch, env)) {
|
|
58
|
-
effectiveIsolationMode = "worktree";
|
|
59
|
-
warning =
|
|
60
|
-
"<system-notification>ProjFS isolation is disabled on Windows ARM64 x64 emulation. Falling back to worktree isolation.</system-notification>";
|
|
61
|
-
return { effectiveIsolationMode, warning };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const probe = projfsOverlayProbe();
|
|
65
|
-
if (!probe.available) {
|
|
66
|
-
effectiveIsolationMode = "worktree";
|
|
67
|
-
const reason = probe.reason ? ` Reason: ${probe.reason}` : "";
|
|
68
|
-
warning = `<system-notification>ProjFS is unavailable on this host. Falling back to worktree isolation.${reason}</system-notification>`;
|
|
69
|
-
return { effectiveIsolationMode, warning };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const probeIsolationId = `probe-${Snowflake.next()}`;
|
|
73
|
-
let probeIsolationDir: string | null = null;
|
|
74
|
-
try {
|
|
75
|
-
probeIsolationDir = await ensureProjfsOverlay(repoRoot, probeIsolationId);
|
|
76
|
-
} catch (err) {
|
|
77
|
-
if (isProjfsUnavailableError(err)) {
|
|
78
|
-
effectiveIsolationMode = "worktree";
|
|
79
|
-
const raw = err instanceof Error ? err.message : String(err);
|
|
80
|
-
const reason = raw.replace(/^PROJFS_UNAVAILABLE:\s*/, "");
|
|
81
|
-
const detail = reason ? ` Reason: ${reason}` : "";
|
|
82
|
-
warning = `<system-notification>ProjFS prerequisites are unavailable for this repository. Falling back to worktree isolation.${detail}</system-notification>`;
|
|
83
|
-
} else {
|
|
84
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
85
|
-
throw new Error(`ProjFS isolation initialization failed. ${message}`);
|
|
86
|
-
}
|
|
87
|
-
} finally {
|
|
88
|
-
if (probeIsolationDir) {
|
|
89
|
-
await cleanupProjfsOverlay(probeIsolationDir);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { effectiveIsolationMode, warning };
|
|
94
|
-
}
|