@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.
Files changed (138) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/package.json +7 -7
  3. package/scripts/build-binary.ts +11 -0
  4. package/scripts/format-prompts.ts +1 -1
  5. package/src/cli/args.ts +2 -2
  6. package/src/cli/stats-cli.ts +2 -0
  7. package/src/cli.ts +24 -1
  8. package/src/commands/acp.ts +24 -0
  9. package/src/commands/launch.ts +6 -4
  10. package/src/commit/agentic/prompts/system.md +1 -1
  11. package/src/config/model-resolver.ts +30 -0
  12. package/src/config/settings-schema.ts +61 -9
  13. package/src/config/settings.ts +18 -1
  14. package/src/edit/index.ts +22 -1
  15. package/src/edit/modes/patch.ts +10 -0
  16. package/src/edit/modes/replace.ts +3 -0
  17. package/src/edit/renderer.ts +10 -0
  18. package/src/edit/streaming.ts +1 -1
  19. package/src/eval/js/context-manager.ts +10 -9
  20. package/src/eval/js/shared/rewrite-imports.ts +120 -48
  21. package/src/eval/js/shared/runtime.ts +31 -4
  22. package/src/eval/js/tool-bridge.ts +43 -21
  23. package/src/extensibility/extensions/runner.ts +54 -1
  24. package/src/extensibility/extensions/types.ts +11 -0
  25. package/src/extensibility/skills.ts +33 -1
  26. package/src/hashline/grammar.lark +1 -1
  27. package/src/hashline/input.ts +11 -5
  28. package/src/internal-urls/docs-index.generated.ts +7 -7
  29. package/src/internal-urls/index.ts +1 -0
  30. package/src/internal-urls/issue-pr-protocol.ts +577 -0
  31. package/src/internal-urls/router.ts +6 -3
  32. package/src/internal-urls/types.ts +22 -1
  33. package/src/main.ts +13 -9
  34. package/src/modes/acp/acp-agent.ts +361 -54
  35. package/src/modes/acp/acp-client-bridge.ts +152 -0
  36. package/src/modes/acp/acp-event-mapper.ts +180 -15
  37. package/src/modes/acp/terminal-auth.ts +37 -0
  38. package/src/modes/components/read-tool-group.ts +29 -1
  39. package/src/modes/controllers/command-controller.ts +14 -6
  40. package/src/modes/controllers/event-controller.ts +24 -11
  41. package/src/modes/controllers/extension-ui-controller.ts +8 -2
  42. package/src/modes/controllers/input-controller.ts +72 -39
  43. package/src/modes/interactive-mode.ts +71 -7
  44. package/src/modes/rpc/rpc-mode.ts +17 -2
  45. package/src/modes/types.ts +6 -2
  46. package/src/modes/utils/ui-helpers.ts +15 -3
  47. package/src/prompts/agents/designer.md +5 -5
  48. package/src/prompts/agents/explore.md +7 -7
  49. package/src/prompts/agents/init.md +9 -9
  50. package/src/prompts/agents/librarian.md +14 -14
  51. package/src/prompts/agents/plan.md +4 -4
  52. package/src/prompts/agents/reviewer.md +5 -5
  53. package/src/prompts/agents/task.md +10 -10
  54. package/src/prompts/commands/orchestrate.md +2 -2
  55. package/src/prompts/compaction/branch-summary.md +3 -3
  56. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  57. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  58. package/src/prompts/compaction/compaction-summary.md +5 -5
  59. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  60. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  61. package/src/prompts/memories/consolidation.md +2 -2
  62. package/src/prompts/memories/read-path.md +1 -1
  63. package/src/prompts/memories/stage_one_input.md +1 -1
  64. package/src/prompts/memories/stage_one_system.md +5 -5
  65. package/src/prompts/review-request.md +4 -4
  66. package/src/prompts/system/agent-creation-architect.md +17 -17
  67. package/src/prompts/system/agent-creation-user.md +2 -2
  68. package/src/prompts/system/commit-message-system.md +2 -2
  69. package/src/prompts/system/custom-system-prompt.md +2 -2
  70. package/src/prompts/system/eager-todo.md +6 -6
  71. package/src/prompts/system/handoff-document.md +1 -1
  72. package/src/prompts/system/plan-mode-active.md +22 -21
  73. package/src/prompts/system/plan-mode-approved.md +4 -4
  74. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  75. package/src/prompts/system/plan-mode-reference.md +2 -2
  76. package/src/prompts/system/plan-mode-subagent.md +8 -8
  77. package/src/prompts/system/plan-mode-tool-decision-reminder.md +2 -2
  78. package/src/prompts/system/project-prompt.md +4 -4
  79. package/src/prompts/system/subagent-system-prompt.md +7 -7
  80. package/src/prompts/system/subagent-yield-reminder.md +4 -4
  81. package/src/prompts/system/system-prompt.md +72 -71
  82. package/src/prompts/system/ttsr-interrupt.md +1 -1
  83. package/src/prompts/tools/apply-patch.md +1 -1
  84. package/src/prompts/tools/ast-edit.md +3 -3
  85. package/src/prompts/tools/ast-grep.md +3 -3
  86. package/src/prompts/tools/browser.md +3 -3
  87. package/src/prompts/tools/checkpoint.md +3 -3
  88. package/src/prompts/tools/exit-plan-mode.md +2 -2
  89. package/src/prompts/tools/find.md +3 -3
  90. package/src/prompts/tools/github.md +2 -5
  91. package/src/prompts/tools/hashline.md +20 -20
  92. package/src/prompts/tools/image-gen.md +3 -3
  93. package/src/prompts/tools/irc.md +1 -1
  94. package/src/prompts/tools/lsp.md +2 -2
  95. package/src/prompts/tools/patch.md +6 -6
  96. package/src/prompts/tools/read.md +7 -7
  97. package/src/prompts/tools/replace.md +5 -5
  98. package/src/prompts/tools/retain.md +1 -1
  99. package/src/prompts/tools/rewind.md +2 -2
  100. package/src/prompts/tools/search.md +2 -2
  101. package/src/prompts/tools/ssh.md +2 -2
  102. package/src/prompts/tools/task.md +12 -6
  103. package/src/prompts/tools/web-search.md +2 -2
  104. package/src/prompts/tools/write.md +3 -3
  105. package/src/sdk.ts +69 -12
  106. package/src/session/agent-session.ts +231 -22
  107. package/src/session/client-bridge.ts +81 -0
  108. package/src/session/compaction/errors.ts +31 -0
  109. package/src/session/compaction/index.ts +1 -0
  110. package/src/slash-commands/acp-builtins.ts +46 -0
  111. package/src/slash-commands/builtin-registry.ts +699 -116
  112. package/src/slash-commands/helpers/context-report.ts +39 -0
  113. package/src/slash-commands/helpers/format.ts +23 -0
  114. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  115. package/src/slash-commands/helpers/mcp.ts +532 -0
  116. package/src/slash-commands/helpers/parse.ts +85 -0
  117. package/src/slash-commands/helpers/ssh.ts +193 -0
  118. package/src/slash-commands/helpers/todo.ts +279 -0
  119. package/src/slash-commands/helpers/usage-report.ts +91 -0
  120. package/src/slash-commands/types.ts +126 -0
  121. package/src/task/executor.ts +10 -3
  122. package/src/task/index.ts +29 -51
  123. package/src/task/render.ts +6 -3
  124. package/src/task/worktree.ts +170 -239
  125. package/src/tools/bash.ts +176 -2
  126. package/src/tools/browser/tab-supervisor.ts +13 -13
  127. package/src/tools/conflict-detect.ts +6 -6
  128. package/src/tools/fetch.ts +15 -4
  129. package/src/tools/find.ts +19 -1
  130. package/src/tools/gh-renderer.ts +0 -12
  131. package/src/tools/gh.ts +682 -176
  132. package/src/tools/github-cache.ts +548 -0
  133. package/src/tools/index.ts +3 -0
  134. package/src/tools/read.ts +110 -27
  135. package/src/tools/write.ts +23 -1
  136. package/src/tui/code-cell.ts +70 -2
  137. package/src/utils/git.ts +5 -0
  138. 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 code = await Bun.file(absolutePath).text();
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
- "`read conflict://*` is not supported — wildcards are write-only. Use `read <path>:conflicts` for the full list of conflicts in a file, or `read conflict://<N>` to inspect a single block.",
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 `read <path>:conflicts`: scan the whole file once, register
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
- const details: ReadToolDetails = { resolvedPath: resource.sourcePath };
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
- _options: RenderResultOptions,
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 { content: Array<{ type: string; text?: string }>; details?: ReadUrlToolDetails },
1815
- _options,
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
- if (cachedLines && cachedWidth === width) return cachedLines;
1901
- cachedLines = renderCodeCell(
1902
- {
1903
- code: contentText,
1904
- language: lang,
1905
- title,
1906
- status: "complete",
1907
- output: warningLines.length > 0 ? warningLines.join("\n") : undefined,
1908
- expanded: true,
1909
- width,
1910
- },
1911
- uiTheme,
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
  };
@@ -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 — use \`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\`.`,
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
 
@@ -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 { highlightCode, type Theme } from "../modes/theme/theme";
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
- }