@oh-my-pi/pi-coding-agent 8.0.20 → 8.1.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 (166) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/package.json +14 -11
  3. package/scripts/generate-wasm-b64.ts +24 -0
  4. package/src/capability/context-file.ts +1 -1
  5. package/src/capability/extension-module.ts +1 -1
  6. package/src/capability/extension.ts +1 -1
  7. package/src/capability/hook.ts +1 -1
  8. package/src/capability/instruction.ts +1 -1
  9. package/src/capability/mcp.ts +1 -1
  10. package/src/capability/prompt.ts +1 -1
  11. package/src/capability/rule.ts +1 -1
  12. package/src/capability/settings.ts +1 -1
  13. package/src/capability/skill.ts +1 -1
  14. package/src/capability/slash-command.ts +1 -1
  15. package/src/capability/ssh.ts +1 -1
  16. package/src/capability/system-prompt.ts +1 -1
  17. package/src/capability/tool.ts +1 -1
  18. package/src/cli/args.ts +1 -1
  19. package/src/cli/plugin-cli.ts +1 -5
  20. package/src/commit/agentic/agent.ts +309 -0
  21. package/src/commit/agentic/fallback.ts +96 -0
  22. package/src/commit/agentic/index.ts +359 -0
  23. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  24. package/src/commit/agentic/prompts/session-user.md +26 -0
  25. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  26. package/src/commit/agentic/prompts/system.md +40 -0
  27. package/src/commit/agentic/state.ts +74 -0
  28. package/src/commit/agentic/tools/analyze-file.ts +131 -0
  29. package/src/commit/agentic/tools/git-file-diff.ts +194 -0
  30. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  31. package/src/commit/agentic/tools/git-overview.ts +84 -0
  32. package/src/commit/agentic/tools/index.ts +56 -0
  33. package/src/commit/agentic/tools/propose-changelog.ts +128 -0
  34. package/src/commit/agentic/tools/propose-commit.ts +154 -0
  35. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  36. package/src/commit/agentic/tools/split-commit.ts +284 -0
  37. package/src/commit/agentic/topo-sort.ts +44 -0
  38. package/src/commit/agentic/trivial.ts +51 -0
  39. package/src/commit/agentic/validation.ts +200 -0
  40. package/src/commit/analysis/conventional.ts +169 -0
  41. package/src/commit/analysis/index.ts +4 -0
  42. package/src/commit/analysis/scope.ts +242 -0
  43. package/src/commit/analysis/summary.ts +114 -0
  44. package/src/commit/analysis/validation.ts +66 -0
  45. package/src/commit/changelog/detect.ts +36 -0
  46. package/src/commit/changelog/generate.ts +112 -0
  47. package/src/commit/changelog/index.ts +233 -0
  48. package/src/commit/changelog/parse.ts +44 -0
  49. package/src/commit/cli.ts +93 -0
  50. package/src/commit/git/diff.ts +148 -0
  51. package/src/commit/git/errors.ts +11 -0
  52. package/src/commit/git/index.ts +217 -0
  53. package/src/commit/git/operations.ts +53 -0
  54. package/src/commit/index.ts +5 -0
  55. package/src/commit/map-reduce/.map-phase.ts.kate-swp +0 -0
  56. package/src/commit/map-reduce/index.ts +63 -0
  57. package/src/commit/map-reduce/map-phase.ts +193 -0
  58. package/src/commit/map-reduce/reduce-phase.ts +147 -0
  59. package/src/commit/map-reduce/utils.ts +9 -0
  60. package/src/commit/message.ts +11 -0
  61. package/src/commit/model-selection.ts +84 -0
  62. package/src/commit/pipeline.ts +242 -0
  63. package/src/commit/prompts/analysis-system.md +155 -0
  64. package/src/commit/prompts/analysis-user.md +41 -0
  65. package/src/commit/prompts/changelog-system.md +56 -0
  66. package/src/commit/prompts/changelog-user.md +19 -0
  67. package/src/commit/prompts/file-observer-system.md +26 -0
  68. package/src/commit/prompts/file-observer-user.md +9 -0
  69. package/src/commit/prompts/reduce-system.md +60 -0
  70. package/src/commit/prompts/reduce-user.md +17 -0
  71. package/src/commit/prompts/summary-retry.md +4 -0
  72. package/src/commit/prompts/summary-system.md +52 -0
  73. package/src/commit/prompts/summary-user.md +13 -0
  74. package/src/commit/prompts/types-description.md +2 -0
  75. package/src/commit/types.ts +109 -0
  76. package/src/commit/utils/exclusions.ts +42 -0
  77. package/src/config/file-lock.ts +111 -0
  78. package/src/config/model-registry.ts +16 -7
  79. package/src/config/settings-manager.ts +115 -40
  80. package/src/config.ts +5 -5
  81. package/src/discovery/agents-md.ts +1 -1
  82. package/src/discovery/builtin.ts +1 -1
  83. package/src/discovery/claude.ts +1 -1
  84. package/src/discovery/cline.ts +1 -1
  85. package/src/discovery/codex.ts +1 -1
  86. package/src/discovery/cursor.ts +1 -1
  87. package/src/discovery/gemini.ts +1 -1
  88. package/src/discovery/github.ts +1 -1
  89. package/src/discovery/index.ts +11 -11
  90. package/src/discovery/mcp-json.ts +1 -1
  91. package/src/discovery/ssh.ts +1 -1
  92. package/src/discovery/vscode.ts +1 -1
  93. package/src/discovery/windsurf.ts +1 -1
  94. package/src/extensibility/custom-commands/loader.ts +1 -1
  95. package/src/extensibility/custom-commands/types.ts +1 -1
  96. package/src/extensibility/custom-tools/loader.ts +1 -1
  97. package/src/extensibility/custom-tools/types.ts +1 -1
  98. package/src/extensibility/extensions/loader.ts +1 -1
  99. package/src/extensibility/extensions/types.ts +1 -1
  100. package/src/extensibility/hooks/loader.ts +1 -1
  101. package/src/extensibility/hooks/types.ts +3 -3
  102. package/src/index.ts +10 -10
  103. package/src/ipy/executor.ts +97 -1
  104. package/src/lsp/index.ts +1 -1
  105. package/src/lsp/render.ts +90 -46
  106. package/src/main.ts +16 -3
  107. package/src/mcp/loader.ts +3 -3
  108. package/src/migrations.ts +3 -3
  109. package/src/modes/components/assistant-message.ts +29 -1
  110. package/src/modes/components/tool-execution.ts +5 -3
  111. package/src/modes/components/tree-selector.ts +1 -1
  112. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  113. package/src/modes/controllers/selector-controller.ts +1 -1
  114. package/src/modes/interactive-mode.ts +5 -3
  115. package/src/modes/rpc/rpc-client.ts +1 -1
  116. package/src/modes/rpc/rpc-mode.ts +1 -4
  117. package/src/modes/rpc/rpc-types.ts +1 -1
  118. package/src/modes/theme/mermaid-cache.ts +89 -0
  119. package/src/modes/theme/theme.ts +2 -0
  120. package/src/modes/types.ts +2 -2
  121. package/src/patch/index.ts +3 -9
  122. package/src/patch/shared.ts +33 -5
  123. package/src/prompts/tools/task.md +2 -0
  124. package/src/sdk.ts +60 -22
  125. package/src/session/agent-session.ts +3 -3
  126. package/src/session/agent-storage.ts +32 -28
  127. package/src/session/artifacts.ts +24 -1
  128. package/src/session/auth-storage.ts +25 -10
  129. package/src/session/storage-migration.ts +12 -53
  130. package/src/system-prompt.ts +2 -2
  131. package/src/task/.executor.ts.kate-swp +0 -0
  132. package/src/task/executor.ts +1 -1
  133. package/src/task/index.ts +10 -1
  134. package/src/task/output-manager.ts +94 -0
  135. package/src/task/render.ts +7 -12
  136. package/src/task/worker.ts +1 -1
  137. package/src/tools/ask.ts +35 -13
  138. package/src/tools/bash.ts +80 -87
  139. package/src/tools/calculator.ts +42 -40
  140. package/src/tools/complete.ts +1 -1
  141. package/src/tools/fetch.ts +67 -104
  142. package/src/tools/find.ts +83 -86
  143. package/src/tools/grep.ts +80 -96
  144. package/src/tools/index.ts +10 -7
  145. package/src/tools/ls.ts +39 -65
  146. package/src/tools/notebook.ts +48 -64
  147. package/src/tools/output-utils.ts +1 -1
  148. package/src/tools/python.ts +71 -183
  149. package/src/tools/read.ts +74 -15
  150. package/src/tools/render-utils.ts +1 -15
  151. package/src/tools/ssh.ts +43 -24
  152. package/src/tools/todo-write.ts +27 -15
  153. package/src/tools/write.ts +93 -64
  154. package/src/tui/code-cell.ts +115 -0
  155. package/src/tui/file-list.ts +48 -0
  156. package/src/tui/index.ts +11 -0
  157. package/src/tui/output-block.ts +73 -0
  158. package/src/tui/status-line.ts +40 -0
  159. package/src/tui/tree-list.ts +56 -0
  160. package/src/tui/types.ts +17 -0
  161. package/src/tui/utils.ts +49 -0
  162. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
  163. package/src/web/search/auth.ts +1 -1
  164. package/src/web/search/index.ts +1 -1
  165. package/src/web/search/render.ts +119 -163
  166. package/tsconfig.json +0 -42
package/src/tools/read.ts CHANGED
@@ -5,11 +5,12 @@ import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
5
5
  import { CONFIG_DIR_NAME } from "@oh-my-pi/pi-coding-agent/config";
6
6
  import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
7
7
  import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
8
- import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
8
+ import { getLanguageFromPath, type Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
9
9
  import readDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/read.md" with { type: "text" };
10
10
  import type { ToolSession } from "@oh-my-pi/pi-coding-agent/sdk";
11
11
  import type { OutputMeta } from "@oh-my-pi/pi-coding-agent/tools/output-meta";
12
12
  import { ToolAbortError, ToolError, throwIfAborted } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
13
+ import { renderCodeCell, renderOutputBlock, renderStatusLine } from "@oh-my-pi/pi-coding-agent/tui";
13
14
  import { formatDimensionNote, resizeImage } from "@oh-my-pi/pi-coding-agent/utils/image-resize";
14
15
  import { detectSupportedImageMimeTypeFromFile } from "@oh-my-pi/pi-coding-agent/utils/mime";
15
16
  import { ensureTool } from "@oh-my-pi/pi-coding-agent/utils/tools-manager";
@@ -378,6 +379,7 @@ const readSchema = Type.Object({
378
379
  export interface ReadToolDetails {
379
380
  truncation?: TruncationResult;
380
381
  redirectedTo?: "ls";
382
+ resolvedPath?: string;
381
383
  meta?: OutputMeta;
382
384
  }
383
385
 
@@ -708,11 +710,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
708
710
 
709
711
  // If extraction was used, return directly (no pagination)
710
712
  if (hasExtraction) {
711
- let text = resource.content;
713
+ const details: ReadToolDetails = {};
712
714
  if (resource.sourcePath) {
713
- text += `\n\n[Resolved path: ${resource.sourcePath}]`;
715
+ details.resolvedPath = resource.sourcePath;
714
716
  }
715
- return toolResult<ReadToolDetails>().text(text).sourceInternal(url).done();
717
+ return toolResult(details).text(resource.content).sourceInternal(url).done();
716
718
  }
717
719
 
718
720
  // Apply pagination similar to file reading
@@ -806,9 +808,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
806
808
  details = {};
807
809
  }
808
810
 
809
- // Append resolved path notice
810
811
  if (resource.sourcePath) {
811
- outputText += `\n\n[Resolved path: ${resource.sourcePath}]`;
812
+ details.resolvedPath = resource.sourcePath;
812
813
  }
813
814
 
814
815
  const resultBuilder = toolResult(details).text(outputText).sourceInternal(url);
@@ -837,14 +838,14 @@ export const readToolRenderer = {
837
838
  const offset = args.offset;
838
839
  const limit = args.limit;
839
840
 
840
- let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
841
+ let pathDisplay = filePath || uiTheme.format.ellipsis;
841
842
  if (offset !== undefined || limit !== undefined) {
842
843
  const startLine = offset ?? 1;
843
844
  const endLine = limit !== undefined ? startLine + limit - 1 : "";
844
- pathDisplay += uiTheme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
845
+ pathDisplay += `:${startLine}${endLine ? `-${endLine}` : ""}`;
845
846
  }
846
847
 
847
- const text = `${uiTheme.fg("toolTitle", uiTheme.bold("Read"))} ${pathDisplay}`;
848
+ const text = renderStatusLine({ icon: "pending", title: "Read", description: pathDisplay }, uiTheme);
848
849
  return new Text(text, 0, 0);
849
850
  },
850
851
 
@@ -852,15 +853,24 @@ export const readToolRenderer = {
852
853
  result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails },
853
854
  _options: RenderResultOptions,
854
855
  uiTheme: Theme,
855
- _args?: ReadRenderArgs,
856
+ args?: ReadRenderArgs,
856
857
  ): Component {
857
858
  const details = result.details;
858
- const lines: string[] = [];
859
-
860
- lines.push(uiTheme.fg("dim", "Content hidden"));
859
+ const contentText = result.content?.find((c) => c.type === "text")?.text ?? "";
860
+ const imageContent = result.content?.find((c) => c.type === "image");
861
+ const rawPath = args?.file_path || args?.path || "";
862
+ const filePath = shortenPath(rawPath);
863
+ const lang = getLanguageFromPath(rawPath);
861
864
 
865
+ const warningLines: string[] = [];
862
866
  const truncation = details?.meta?.truncation;
863
867
  const fallback = details?.truncation;
868
+ if (details?.redirectedTo) {
869
+ warningLines.push(uiTheme.fg("warning", wrapBrackets(`Redirected to ${details.redirectedTo}`, uiTheme)));
870
+ }
871
+ if (details?.resolvedPath) {
872
+ warningLines.push(uiTheme.fg("dim", wrapBrackets(`Resolved path: ${details.resolvedPath}`, uiTheme)));
873
+ }
864
874
  if (truncation) {
865
875
  let warning: string;
866
876
  if (fallback?.firstLineExceedsLimit) {
@@ -874,9 +884,58 @@ export const readToolRenderer = {
874
884
  if (truncation.artifactId) {
875
885
  warning += `. Full output: artifact://${truncation.artifactId}`;
876
886
  }
877
- lines.push(uiTheme.fg("warning", wrapBrackets(warning, uiTheme)));
887
+ warningLines.push(uiTheme.fg("warning", wrapBrackets(warning, uiTheme)));
878
888
  }
879
889
 
880
- return new Text(lines.join("\n"), 0, 0);
890
+ if (imageContent) {
891
+ const header = renderStatusLine(
892
+ { icon: "success", title: "Read", description: filePath || rawPath || "image" },
893
+ uiTheme,
894
+ );
895
+ const detailLines = contentText ? contentText.split("\n").map((line) => uiTheme.fg("toolOutput", line)) : [];
896
+ const lines = [...detailLines, ...warningLines];
897
+ return {
898
+ render: (width: number) =>
899
+ renderOutputBlock(
900
+ {
901
+ header,
902
+ state: "success",
903
+ sections: [
904
+ {
905
+ label: uiTheme.fg("toolTitle", "Details"),
906
+ lines: lines.length > 0 ? lines : [uiTheme.fg("dim", "(image)")],
907
+ },
908
+ ],
909
+ width,
910
+ },
911
+ uiTheme,
912
+ ),
913
+ invalidate: () => {},
914
+ };
915
+ }
916
+
917
+ let title = filePath ? `Read ${filePath}` : "Read";
918
+ if (args?.offset !== undefined || args?.limit !== undefined) {
919
+ const startLine = args.offset ?? 1;
920
+ const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
921
+ title += `:${startLine}${endLine ? `-${endLine}` : ""}`;
922
+ }
923
+ return {
924
+ render: (width: number) =>
925
+ renderCodeCell(
926
+ {
927
+ code: contentText,
928
+ language: lang,
929
+ title,
930
+ status: "complete",
931
+ output: warningLines.length > 0 ? warningLines.join("\n") : undefined,
932
+ expanded: true,
933
+ width,
934
+ },
935
+ uiTheme,
936
+ ),
937
+ invalidate: () => {},
938
+ };
881
939
  },
940
+ mergeCallAndResult: true,
882
941
  };
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { homedir } from "node:os";
9
9
  import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
10
+ import { getTreeBranch } from "@oh-my-pi/pi-coding-agent/tui/utils";
10
11
 
11
12
  // =============================================================================
12
13
  // Standardized Display Constants
@@ -667,21 +668,6 @@ function pluralize(label: string, count: number): string {
667
668
  // =============================================================================
668
669
  // Tree Rendering Utilities
669
670
  // =============================================================================
670
-
671
- /**
672
- * Get the branch character for a tree item.
673
- */
674
- export function getTreeBranch(isLast: boolean, theme: Theme): string {
675
- return isLast ? theme.tree.last : theme.tree.branch;
676
- }
677
-
678
- /**
679
- * Get the continuation prefix for nested content under a tree item.
680
- */
681
- export function getTreeContinuePrefix(isLast: boolean, theme: Theme): string {
682
- return isLast ? " " : `${theme.tree.vertical} `;
683
- }
684
-
685
671
  /**
686
672
  * Render a list of items with tree branches, handling truncation.
687
673
  *
package/src/tools/ssh.ts CHANGED
@@ -2,7 +2,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import type { SSHHost } from "@oh-my-pi/pi-coding-agent/capability/ssh";
3
3
  import { sshCapability } from "@oh-my-pi/pi-coding-agent/capability/ssh";
4
4
  import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
5
- import { loadCapability } from "@oh-my-pi/pi-coding-agent/discovery/index";
5
+ import { loadCapability } from "@oh-my-pi/pi-coding-agent/discovery";
6
6
  import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
7
7
  import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
8
8
  import sshDescriptionBase from "@oh-my-pi/pi-coding-agent/prompts/tools/ssh.md" with { type: "text" };
@@ -11,12 +11,13 @@ import { ensureHostInfo, getHostInfoForHost } from "@oh-my-pi/pi-coding-agent/ss
11
11
  import { executeSSH } from "@oh-my-pi/pi-coding-agent/ssh/ssh-executor";
12
12
  import type { OutputMeta } from "@oh-my-pi/pi-coding-agent/tools/output-meta";
13
13
  import { ToolError } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
14
+ import { renderOutputBlock, renderStatusLine } from "@oh-my-pi/pi-coding-agent/tui";
14
15
  import type { Component } from "@oh-my-pi/pi-tui";
15
16
  import { Text } from "@oh-my-pi/pi-tui";
16
17
  import { Type } from "@sinclair/typebox";
17
- import type { ToolSession } from "./index";
18
+ import type { ToolSession } from ".";
18
19
  import { allocateOutputArtifact, createTailBuffer } from "./output-utils";
19
- import { ToolUIKit } from "./render-utils";
20
+ import { formatBytes, wrapBrackets } from "./render-utils";
20
21
  import { toolResult } from "./tool-result";
21
22
  import { DEFAULT_MAX_BYTES } from "./truncate";
22
23
 
@@ -237,10 +238,9 @@ interface SshRenderContext {
237
238
 
238
239
  export const sshToolRenderer = {
239
240
  renderCall(args: SshRenderArgs, uiTheme: Theme): Component {
240
- const ui = new ToolUIKit(uiTheme);
241
241
  const host = args.host || uiTheme.format.ellipsis;
242
242
  const command = args.command || uiTheme.format.ellipsis;
243
- const text = ui.title(`[${host}] $ ${command}`);
243
+ const text = renderStatusLine({ icon: "pending", title: "SSH", description: `[${host}] $ ${command}` }, uiTheme);
244
244
  return new Text(text, 0, 0);
245
245
  },
246
246
 
@@ -251,42 +251,48 @@ export const sshToolRenderer = {
251
251
  },
252
252
  options: RenderResultOptions & { renderContext?: SshRenderContext },
253
253
  uiTheme: Theme,
254
+ args?: SshRenderArgs,
254
255
  ): Component {
255
- const ui = new ToolUIKit(uiTheme);
256
256
  const { expanded, renderContext } = options;
257
257
  const details = result.details;
258
- const lines: string[] = [];
258
+ const host = args?.host || uiTheme.format.ellipsis;
259
+ const command = args?.command || uiTheme.format.ellipsis;
260
+ const header = renderStatusLine(
261
+ { icon: "success", title: "SSH", description: `[${host}] $ ${command}` },
262
+ uiTheme,
263
+ );
264
+ const outputLines: string[] = [];
259
265
 
260
266
  const textContent = result.content?.find((c) => c.type === "text")?.text ?? "";
261
- const output = textContent.trim();
267
+ const output = textContent.trimEnd();
262
268
 
263
269
  if (output) {
264
270
  if (expanded) {
265
- const styledOutput = output
266
- .split("\n")
267
- .map((line) => uiTheme.fg("toolOutput", line))
268
- .join("\n");
269
- lines.push(styledOutput);
271
+ outputLines.push(...output.split("\n").map((line) => uiTheme.fg("toolOutput", line)));
270
272
  } else if (renderContext?.visualLines) {
271
273
  const { visualLines, skippedCount = 0, totalVisualLines = visualLines.length } = renderContext;
272
274
  if (skippedCount > 0) {
273
- lines.push(
275
+ outputLines.push(
274
276
  uiTheme.fg(
275
277
  "dim",
276
278
  `${uiTheme.format.ellipsis} (${skippedCount} earlier lines, showing ${visualLines.length} of ${totalVisualLines}) (ctrl+o to expand)`,
277
279
  ),
278
280
  );
279
281
  }
280
- lines.push(...visualLines);
282
+ const styledVisual = visualLines.map((line) =>
283
+ line.includes("\x1b[") ? line : uiTheme.fg("toolOutput", line),
284
+ );
285
+ outputLines.push(...styledVisual);
281
286
  } else {
282
- const outputLines = output.split("\n");
287
+ const outputLinesRaw = output.split("\n");
283
288
  const maxLines = 5;
284
- const displayLines = outputLines.slice(0, maxLines);
285
- const remaining = outputLines.length - maxLines;
286
-
287
- lines.push(...displayLines.map((line) => uiTheme.fg("toolOutput", line)));
289
+ const displayLines = outputLinesRaw.slice(0, maxLines);
290
+ const remaining = outputLinesRaw.length - maxLines;
291
+ outputLines.push(...displayLines.map((line) => uiTheme.fg("toolOutput", line)));
288
292
  if (remaining > 0) {
289
- lines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${remaining} more lines) (ctrl+o to expand)`));
293
+ outputLines.push(
294
+ uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${remaining} more lines) (ctrl+o to expand)`),
295
+ );
290
296
  }
291
297
  }
292
298
  }
@@ -301,12 +307,25 @@ export const sshToolRenderer = {
301
307
  warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
302
308
  } else {
303
309
  warnings.push(
304
- `Truncated: ${truncation.outputLines} lines shown (${ui.formatBytes(truncation.outputBytes)} limit)`,
310
+ `Truncated: ${truncation.outputLines} lines shown (${formatBytes(truncation.outputBytes)} limit)`,
305
311
  );
306
312
  }
307
- lines.push(uiTheme.fg("warning", ui.wrapBrackets(warnings.join(". "))));
313
+ outputLines.push(uiTheme.fg("warning", wrapBrackets(warnings.join(". "), uiTheme)));
308
314
  }
309
315
 
310
- return new Text(lines.join("\n"), 0, 0);
316
+ return {
317
+ render: (width: number) =>
318
+ renderOutputBlock(
319
+ {
320
+ header,
321
+ state: "success",
322
+ sections: [{ label: uiTheme.fg("toolTitle", "Output"), lines: outputLines }],
323
+ width,
324
+ },
325
+ uiTheme,
326
+ ),
327
+ invalidate: () => {},
328
+ };
311
329
  },
330
+ mergeCallAndResult: true,
312
331
  };
@@ -7,11 +7,13 @@ import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibilit
7
7
  import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
8
8
  import todoWriteDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/todo-write.md" with { type: "text" };
9
9
  import type { ToolSession } from "@oh-my-pi/pi-coding-agent/sdk";
10
+ import { renderStatusLine, renderTreeList } from "@oh-my-pi/pi-coding-agent/tui";
10
11
  import type { Component } from "@oh-my-pi/pi-tui";
11
12
  import { Text } from "@oh-my-pi/pi-tui";
12
13
  import { logger } from "@oh-my-pi/pi-utils";
13
14
  import { Type } from "@sinclair/typebox";
14
15
  import chalk from "chalk";
16
+ import { PREVIEW_LIMITS } from "./render-utils";
15
17
 
16
18
  const todoWriteSchema = Type.Object({
17
19
  todos: Type.Array(
@@ -217,29 +219,39 @@ interface TodoWriteRenderArgs {
217
219
  export const todoWriteToolRenderer = {
218
220
  renderCall(args: TodoWriteRenderArgs, uiTheme: Theme): Component {
219
221
  const count = args.todos?.length ?? 0;
220
- const summary = count > 0 ? uiTheme.fg("accent", `${count} items`) : uiTheme.fg("toolOutput", "empty");
221
- return new Text(`${uiTheme.fg("toolTitle", uiTheme.bold("Todo Write"))} ${summary}`, 0, 0);
222
+ const meta = count > 0 ? [`${count} items`] : ["empty"];
223
+ const text = renderStatusLine({ icon: "pending", title: "Todo Write", meta }, uiTheme);
224
+ return new Text(text, 0, 0);
222
225
  },
223
226
 
224
227
  renderResult(
225
228
  result: { content: Array<{ type: string; text?: string }>; details?: TodoWriteToolDetails },
226
- _options: RenderResultOptions,
229
+ options: RenderResultOptions,
227
230
  uiTheme: Theme,
228
231
  _args?: TodoWriteRenderArgs,
229
232
  ): Component {
233
+ const { expanded } = options;
230
234
  const todos = result.details?.todos ?? [];
231
- const indent = " ";
232
- const hook = uiTheme.tree.hook;
233
- const lines = [indent + uiTheme.bold(uiTheme.fg("accent", "Todos"))];
234
-
235
- if (todos.length > 0) {
236
- const visibleTodos = todos;
237
- visibleTodos.forEach((todo, index) => {
238
- const prefix = `${indent}${index === 0 ? hook : " "} `;
239
- lines.push(formatTodoLine(todo, uiTheme, prefix));
240
- });
235
+ const header = renderStatusLine(
236
+ { icon: "success", title: "Todo Write", meta: [`${todos.length} items`] },
237
+ uiTheme,
238
+ );
239
+ if (todos.length === 0) {
240
+ const fallback = result.content?.find((c) => c.type === "text")?.text ?? "No todos";
241
+ return new Text([header, uiTheme.fg("dim", fallback)].join("\n"), 0, 0);
241
242
  }
242
-
243
- return new Text(lines.join("\n"), 0, 0);
243
+ const lines = renderTreeList(
244
+ {
245
+ items: todos,
246
+ expanded,
247
+ maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
248
+ itemType: "todo",
249
+ renderItem: (todo) => formatTodoLine(todo, uiTheme, ""),
250
+ },
251
+ uiTheme,
252
+ );
253
+
254
+ return new Text([header, ...lines].join("\n"), 0, 0);
244
255
  },
256
+ mergeCallAndResult: true,
245
257
  };
@@ -12,17 +12,18 @@ import {
12
12
  type FileDiagnosticsResult,
13
13
  type WritethroughCallback,
14
14
  writethroughNoop,
15
- } from "@oh-my-pi/pi-coding-agent/lsp/index";
16
- import { getLanguageFromPath, highlightCode, type Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
15
+ } from "@oh-my-pi/pi-coding-agent/lsp";
16
+ import { getLanguageFromPath, type Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
17
17
  import writeDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/write.md" with { type: "text" };
18
18
  import type { ToolSession } from "@oh-my-pi/pi-coding-agent/sdk";
19
19
  import { type OutputMeta, outputMeta } from "@oh-my-pi/pi-coding-agent/tools/output-meta";
20
+ import { renderCodeCell, renderStatusLine } from "@oh-my-pi/pi-coding-agent/tui";
20
21
  import type { Component } from "@oh-my-pi/pi-tui";
21
22
  import { Text } from "@oh-my-pi/pi-tui";
22
23
  import { untilAborted } from "@oh-my-pi/pi-utils";
23
24
  import { Type } from "@sinclair/typebox";
24
25
  import { resolveToCwd } from "./path-utils";
25
- import { formatDiagnostics, formatExpandHint, formatStatusIcon, replaceTabs, shortenPath } from "./render-utils";
26
+ import { formatDiagnostics, shortenPath } from "./render-utils";
26
27
  import type { RenderCallOptions } from "./renderers";
27
28
 
28
29
  const writeSchema = Type.Object({
@@ -134,27 +135,6 @@ function countLines(text: string): number {
134
135
  return text.split("\n").length;
135
136
  }
136
137
 
137
- function formatStreamingContent(content: string, rawPath: string, uiTheme: Theme): string {
138
- if (!content) return "";
139
- const lang = getLanguageFromPath(rawPath);
140
- const lines = content.split("\n");
141
- const total = lines.length;
142
- const displayLines = lines.slice(-WRITE_STREAMING_PREVIEW_LINES);
143
- const hidden = total - displayLines.length;
144
-
145
- const formattedLines = lang
146
- ? highlightCode(replaceTabs(displayLines.join("\n")), lang)
147
- : displayLines.map((line: string) => uiTheme.fg("toolOutput", replaceTabs(line)));
148
-
149
- let text = "\n\n";
150
- if (hidden > 0) {
151
- text += uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${hidden} earlier lines)\n`);
152
- }
153
- text += formattedLines.join("\n");
154
- text += uiTheme.fg("dim", `\n${uiTheme.format.ellipsis} (streaming)`);
155
- return text;
156
- }
157
-
158
138
  function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
159
139
  const icon = uiTheme.getLangIcon(language);
160
140
  if (lineCount !== null) {
@@ -167,68 +147,117 @@ export const writeToolRenderer = {
167
147
  renderCall(args: WriteRenderArgs, uiTheme: Theme, options?: RenderCallOptions): Component {
168
148
  const rawPath = args.file_path || args.path || "";
169
149
  const filePath = shortenPath(rawPath);
170
- const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
171
- const spinner =
172
- options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
173
- let text = `${uiTheme.fg("toolTitle", uiTheme.bold("Write"))} ${spinner ? `${spinner} ` : ""}${pathDisplay}`;
174
-
175
- // Show streaming preview of content
176
- if (args.content) {
177
- text += formatStreamingContent(args.content, rawPath, uiTheme);
150
+ const pathDisplay = filePath || uiTheme.format.ellipsis;
151
+ const status = options?.spinnerFrame !== undefined ? "running" : "pending";
152
+ const text = renderStatusLine(
153
+ { icon: status, title: "Write", description: pathDisplay, spinnerFrame: options?.spinnerFrame },
154
+ uiTheme,
155
+ );
156
+ if (!args.content) {
157
+ return new Text(text, 0, 0);
178
158
  }
179
159
 
180
- return new Text(text, 0, 0);
160
+ const contentLines = args.content.split("\n");
161
+ const displayLines = contentLines.slice(-WRITE_STREAMING_PREVIEW_LINES);
162
+ const hidden = contentLines.length - displayLines.length;
163
+ const outputLines: string[] = [];
164
+ if (hidden > 0) {
165
+ outputLines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${hidden} earlier lines)`));
166
+ }
167
+ outputLines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (streaming)`));
168
+
169
+ return {
170
+ render: (width: number) =>
171
+ renderCodeCell(
172
+ {
173
+ code: displayLines.join("\n"),
174
+ language: getLanguageFromPath(rawPath),
175
+ title: filePath ? `Write ${filePath}` : "Write",
176
+ status,
177
+ spinnerFrame: options?.spinnerFrame,
178
+ output: outputLines.join("\n"),
179
+ codeMaxLines: WRITE_STREAMING_PREVIEW_LINES,
180
+ expanded: true,
181
+ width,
182
+ },
183
+ uiTheme,
184
+ ),
185
+ invalidate: () => {},
186
+ };
181
187
  },
182
188
 
183
189
  renderResult(
184
190
  result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails },
185
- { expanded }: RenderResultOptions,
191
+ { expanded, isPartial, spinnerFrame }: RenderResultOptions,
186
192
  uiTheme: Theme,
187
193
  args?: WriteRenderArgs,
188
194
  ): Component {
189
195
  const rawPath = args?.file_path || args?.path || "";
196
+ const filePath = shortenPath(rawPath);
190
197
  const fileContent = args?.content || "";
191
198
  const lang = getLanguageFromPath(rawPath);
192
- const contentLines = fileContent
193
- ? lang
194
- ? highlightCode(replaceTabs(fileContent), lang)
195
- : fileContent.split("\n")
196
- : [];
197
- const totalLines = contentLines.length;
198
199
  const outputLines: string[] = [];
200
+ const lineCount = countLines(fileContent);
199
201
 
200
- outputLines.push(formatMetadataLine(countLines(fileContent), lang ?? "text", uiTheme));
202
+ outputLines.push(formatMetadataLine(lineCount, lang ?? "text", uiTheme));
201
203
 
202
- if (fileContent) {
203
- const maxLines = expanded ? contentLines.length : 10;
204
- const displayLines = contentLines.slice(0, maxLines);
205
- const remaining = contentLines.length - maxLines;
204
+ if (isPartial && fileContent) {
205
+ const contentLines = fileContent.split("\n");
206
+ const displayLines = contentLines.slice(-WRITE_STREAMING_PREVIEW_LINES);
207
+ const hidden = contentLines.length - displayLines.length;
208
+ if (hidden > 0) {
209
+ outputLines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${hidden} earlier lines)`));
210
+ }
211
+ outputLines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (streaming)`));
206
212
 
207
- outputLines.push(
208
- "",
209
- ...displayLines.map((line: string) =>
210
- lang ? replaceTabs(line) : uiTheme.fg("toolOutput", replaceTabs(line)),
211
- ),
212
- );
213
- if (remaining > 0) {
214
- outputLines.push(
215
- uiTheme.fg(
216
- "toolOutput",
217
- `${uiTheme.format.ellipsis} (${remaining} more lines, ${totalLines} total) ${formatExpandHint(uiTheme)}`,
213
+ return {
214
+ render: (width: number) =>
215
+ renderCodeCell(
216
+ {
217
+ code: displayLines.join("\n"),
218
+ language: lang,
219
+ title: filePath ? `Write ${filePath}` : "Write",
220
+ status: spinnerFrame !== undefined ? "running" : "pending",
221
+ spinnerFrame,
222
+ output: outputLines.join("\n"),
223
+ codeMaxLines: WRITE_STREAMING_PREVIEW_LINES,
224
+ expanded: true,
225
+ width,
226
+ },
227
+ uiTheme,
218
228
  ),
219
- );
220
- }
229
+ invalidate: () => {},
230
+ };
221
231
  }
222
232
 
223
- // Show LSP diagnostics if available
224
233
  if (result.details?.diagnostics) {
225
- outputLines.push(
226
- formatDiagnostics(result.details.diagnostics, expanded, uiTheme, (fp) =>
227
- uiTheme.getLangIcon(getLanguageFromPath(fp)),
228
- ),
234
+ const diagText = formatDiagnostics(result.details.diagnostics, expanded, uiTheme, (fp) =>
235
+ uiTheme.getLangIcon(getLanguageFromPath(fp)),
229
236
  );
237
+ if (diagText.trim()) {
238
+ const diagLines = diagText.split("\n");
239
+ const firstNonEmpty = diagLines.findIndex((line) => line.trim());
240
+ outputLines.push(...(firstNonEmpty >= 0 ? diagLines.slice(firstNonEmpty) : []));
241
+ }
230
242
  }
231
243
 
232
- return new Text(outputLines.join("\n"), 0, 0);
244
+ return {
245
+ render: (width: number) =>
246
+ renderCodeCell(
247
+ {
248
+ code: fileContent,
249
+ language: lang,
250
+ title: filePath ? `Write ${filePath}` : "Write",
251
+ status: "complete",
252
+ output: outputLines.join("\n"),
253
+ codeMaxLines: expanded ? Number.POSITIVE_INFINITY : 10,
254
+ expanded,
255
+ width,
256
+ },
257
+ uiTheme,
258
+ ),
259
+ invalidate: () => {},
260
+ };
233
261
  },
262
+ mergeCallAndResult: true,
234
263
  };