@oh-my-pi/pi-coding-agent 8.0.16 → 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/ls.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import nodePath from "node:path";
2
2
  import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
3
3
  import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
4
- import { getLanguageFromPath, type Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
4
+ import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
5
5
  import type { OutputMeta } from "@oh-my-pi/pi-coding-agent/tools/output-meta";
6
6
  import { ToolError, throwIfAborted } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
7
+ import { renderFileList, renderStatusLine } from "@oh-my-pi/pi-coding-agent/tui";
7
8
  import { type Component, Text } from "@oh-my-pi/pi-tui";
8
9
  import { untilAborted } from "@oh-my-pi/pi-utils";
9
10
  import { Type } from "@sinclair/typebox";
10
- import type { ToolSession } from "./index";
11
+ import type { ToolSession } from ".";
11
12
  import { applyListLimit } from "./list-limit";
12
13
  import { resolveToCwd } from "./path-utils";
13
14
  import {
@@ -16,10 +17,6 @@ import {
16
17
  formatCount,
17
18
  formatEmptyMessage,
18
19
  formatErrorMessage,
19
- formatExpandHint,
20
- formatMeta,
21
- formatMoreItems,
22
- formatTruncationSuffix,
23
20
  PREVIEW_LIMITS,
24
21
  } from "./render-utils";
25
22
  import { toolResult } from "./tool-result";
@@ -214,13 +211,9 @@ const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
214
211
  export const lsToolRenderer = {
215
212
  inline: true,
216
213
  renderCall(args: LsRenderArgs, uiTheme: Theme): Component {
217
- const label = uiTheme.fg("toolTitle", uiTheme.bold("Ls"));
218
- let text = `${uiTheme.format.bullet} ${label} ${uiTheme.fg("accent", args.path || ".")}`;
219
-
220
214
  const meta: string[] = [];
221
215
  if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
222
- text += formatMeta(meta, uiTheme);
223
-
216
+ const text = renderStatusLine({ icon: "pending", title: "Ls", description: args.path || ".", meta }, uiTheme);
224
217
  return new Text(text, 0, 0);
225
218
  },
226
219
 
@@ -228,19 +221,22 @@ export const lsToolRenderer = {
228
221
  result: { content: Array<{ type: string; text?: string }>; details?: LsToolDetails; isError?: boolean },
229
222
  { expanded }: RenderResultOptions,
230
223
  uiTheme: Theme,
224
+ args?: LsRenderArgs,
231
225
  ): Component {
232
226
  const details = result.details;
233
227
  const textContent = result.content?.find((c) => c.type === "text")?.text ?? "";
234
228
 
235
229
  if (result.isError) {
236
- return new Text(` ${formatErrorMessage(textContent, uiTheme)}`, 0, 0);
230
+ const header = renderStatusLine({ icon: "error", title: "Ls", description: args?.path || "." }, uiTheme);
231
+ return new Text([header, formatErrorMessage(textContent, uiTheme)].join("\n"), 0, 0);
237
232
  }
238
233
 
239
234
  if (
240
235
  (!textContent || textContent.trim() === "" || textContent.trim() === "(empty directory)") &&
241
236
  (!details?.entries || details.entries.length === 0)
242
237
  ) {
243
- return new Text(` ${formatEmptyMessage("Empty directory", uiTheme)}`, 0, 0);
238
+ const header = renderStatusLine({ icon: "warning", title: "Ls", description: args?.path || "." }, uiTheme);
239
+ return new Text([header, formatEmptyMessage("Empty directory", uiTheme)].join("\n"), 0, 0);
244
240
  }
245
241
 
246
242
  let entries: string[] = details?.entries ? [...details.entries] : [];
@@ -252,7 +248,8 @@ export const lsToolRenderer = {
252
248
  }
253
249
 
254
250
  if (entries.length === 0) {
255
- return new Text(` ${formatEmptyMessage("Empty directory", uiTheme)}`, 0, 0);
251
+ const header = renderStatusLine({ icon: "warning", title: "Ls", description: args?.path || "." }, uiTheme);
252
+ return new Text([header, formatEmptyMessage("Empty directory", uiTheme)].join("\n"), 0, 0);
256
253
  }
257
254
 
258
255
  let dirCount = details?.dirCount;
@@ -260,8 +257,11 @@ export const lsToolRenderer = {
260
257
  if (dirCount === undefined || fileCount === undefined) {
261
258
  dirCount = 0;
262
259
  fileCount = 0;
263
- for (const entry of entries) {
264
- if (entry.endsWith("/")) {
260
+ for (let i = 0; i < entries.length; i++) {
261
+ const entry = entries[i];
262
+ const raw = rawEntries?.[i] ?? entry;
263
+ const name = raw.split(" (")[0];
264
+ if (name.endsWith("/")) {
265
265
  dirCount += 1;
266
266
  } else {
267
267
  fileCount += 1;
@@ -272,62 +272,36 @@ export const lsToolRenderer = {
272
272
  const truncation = details?.meta?.truncation;
273
273
  const limits = details?.meta?.limits;
274
274
  const truncated = Boolean(details?.entryLimitReached || truncation || limits?.resultLimit || limits?.headLimit);
275
- const icon = truncated
276
- ? uiTheme.styledSymbol("status.warning", "warning")
277
- : uiTheme.styledSymbol("status.success", "success");
278
-
279
275
  const summaryText = [formatCount("dir", dirCount ?? 0), formatCount("file", fileCount ?? 0)].join(
280
276
  uiTheme.sep.dot,
281
277
  );
282
- const maxEntries = expanded ? entries.length : Math.min(entries.length, COLLAPSED_LIST_LIMIT);
283
- const hasMoreEntries = entries.length > maxEntries;
284
- const expandHint = formatExpandHint(uiTheme, expanded, hasMoreEntries);
278
+ const meta = truncated ? [summaryText, uiTheme.fg("warning", "truncated")] : [summaryText];
279
+ const header = renderStatusLine(
280
+ { icon: truncated ? "warning" : "success", title: "Ls", description: args?.path || ".", meta },
281
+ uiTheme,
282
+ );
285
283
 
286
- let text = ` ${icon} ${uiTheme.fg("dim", summaryText)}${formatTruncationSuffix(truncated, uiTheme)}${expandHint}`;
284
+ const fileLines = renderFileList(
285
+ {
286
+ files: entries.map((entry, index) => ({
287
+ path: entry,
288
+ isDirectory: (rawEntries?.[index] ?? entry).endsWith("/"),
289
+ })),
290
+ expanded,
291
+ maxCollapsed: COLLAPSED_LIST_LIMIT,
292
+ },
293
+ uiTheme,
294
+ );
287
295
 
288
296
  const truncationReasons: string[] = [];
289
- if (limits?.resultLimit) {
290
- truncationReasons.push(`entry limit ${limits.resultLimit.reached}`);
291
- }
292
- if (truncation) {
293
- truncationReasons.push(`output cap ${formatBytes(truncation.outputBytes)}`);
294
- }
295
- if (truncation?.artifactId) {
296
- truncationReasons.push(`full output: artifact://${truncation.artifactId}`);
297
- }
298
-
299
- const hasTruncation = truncationReasons.length > 0;
300
-
301
- for (let i = 0; i < maxEntries; i++) {
302
- const entry = entries[i];
303
- const rawEntry = rawEntries?.[i] ?? entry;
304
- const isLast = i === maxEntries - 1 && !hasMoreEntries && !hasTruncation;
305
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
306
- const isDir = rawEntry.endsWith("/");
307
- const entryPath = isDir ? rawEntry.slice(0, -1) : rawEntry;
308
- const lang = isDir ? undefined : getLanguageFromPath(entryPath);
309
- const entryIcon = isDir
310
- ? uiTheme.fg("accent", uiTheme.icon.folder)
311
- : uiTheme.fg("muted", uiTheme.getLangIcon(lang));
312
- const entryColor = isDir ? "accent" : "toolOutput";
313
- text += `\n ${uiTheme.fg("dim", branch)} ${entryIcon} ${uiTheme.fg(entryColor, entry)}`;
314
- }
297
+ if (limits?.resultLimit) truncationReasons.push(`entry limit ${limits.resultLimit.reached}`);
298
+ if (truncation) truncationReasons.push(`output cap ${formatBytes(truncation.outputBytes)}`);
299
+ if (truncation?.artifactId) truncationReasons.push(`full output: artifact://${truncation.artifactId}`);
315
300
 
316
- if (hasMoreEntries) {
317
- const moreEntriesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
318
- text += `\n ${uiTheme.fg("dim", moreEntriesBranch)} ${uiTheme.fg(
319
- "muted",
320
- formatMoreItems(entries.length - maxEntries, "entry", uiTheme),
321
- )}`;
322
- }
301
+ const extraLines =
302
+ truncationReasons.length > 0 ? [uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)] : [];
323
303
 
324
- if (hasTruncation) {
325
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
326
- "warning",
327
- `truncated: ${truncationReasons.join(", ")}`,
328
- )}`;
329
- }
330
-
331
- return new Text(text, 0, 0);
304
+ return new Text([header, ...fileLines, ...extraLines].join("\n"), 0, 0);
332
305
  },
306
+ mergeCallAndResult: true,
333
307
  };
@@ -3,19 +3,13 @@ import { StringEnum } from "@oh-my-pi/pi-ai";
3
3
  import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
4
4
  import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
5
5
  import type { ToolSession } from "@oh-my-pi/pi-coding-agent/sdk";
6
+ import { renderCodeCell, renderStatusLine } from "@oh-my-pi/pi-coding-agent/tui";
6
7
  import type { Component } from "@oh-my-pi/pi-tui";
7
8
  import { Text } from "@oh-my-pi/pi-tui";
8
9
  import { untilAborted } from "@oh-my-pi/pi-utils";
9
10
  import { type Static, Type } from "@sinclair/typebox";
10
11
  import { resolveToCwd } from "./path-utils";
11
- import {
12
- formatCount,
13
- formatErrorMessage,
14
- formatExpandHint,
15
- formatMeta,
16
- formatMoreItems,
17
- PREVIEW_LIMITS,
18
- } from "./render-utils";
12
+ import { formatCount, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
19
13
 
20
14
  const notebookSchema = Type.Object({
21
15
  action: StringEnum(["edit", "insert", "delete"], {
@@ -200,57 +194,31 @@ export class NotebookTool implements AgentTool<typeof notebookSchema, NotebookTo
200
194
 
201
195
  interface NotebookRenderArgs {
202
196
  action: string;
203
- notebookPath: string;
197
+ notebookPath?: string;
198
+ notebook_path?: string;
204
199
  cellNumber?: number;
200
+ cell_index?: number;
205
201
  cellType?: string;
202
+ cell_type?: string;
206
203
  content?: string;
207
204
  }
208
205
 
209
206
  const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
210
207
 
211
- function normalizeCellLines(lines: string[]): string[] {
212
- return lines.map((line) => (line.endsWith("\n") ? line.slice(0, -1) : line));
213
- }
214
-
215
- function renderCellPreview(lines: string[], expanded: boolean, uiTheme: Theme): string {
216
- const normalized = normalizeCellLines(lines);
217
- if (normalized.length === 0) {
218
- return `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", "(empty cell)")}`;
219
- }
220
-
221
- const maxLines = expanded ? normalized.length : Math.min(normalized.length, COLLAPSED_TEXT_LIMIT);
222
- let text = "";
223
-
224
- for (let i = 0; i < maxLines; i++) {
225
- const isLast = i === maxLines - 1 && (expanded || normalized.length <= maxLines);
226
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
227
- const line = normalized[i];
228
- text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("toolOutput", line)}`;
229
- }
230
-
231
- const remaining = normalized.length - maxLines;
232
- if (remaining > 0) {
233
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
234
- "muted",
235
- formatMoreItems(remaining, "line", uiTheme),
236
- )}`;
237
- }
238
-
239
- return text;
240
- }
241
-
242
208
  export const notebookToolRenderer = {
243
209
  renderCall(args: NotebookRenderArgs, uiTheme: Theme): Component {
244
- const label = uiTheme.fg("toolTitle", uiTheme.bold("Notebook"));
245
- let text = `${label} ${uiTheme.fg("accent", args.action || "?")}`;
246
-
247
210
  const meta: string[] = [];
248
- meta.push(`in ${args.notebookPath || "?"}`);
249
- if (args.cellNumber !== undefined) meta.push(`cell:${args.cellNumber}`);
250
- if (args.cellType) meta.push(`type:${args.cellType}`);
251
-
252
- text += formatMeta(meta, uiTheme);
253
-
211
+ const notebookPath = args.notebookPath ?? args.notebook_path;
212
+ const cellNumber = args.cellNumber ?? args.cell_index;
213
+ const cellType = args.cellType ?? args.cell_type;
214
+ meta.push(`in ${notebookPath || "?"}`);
215
+ if (cellNumber !== undefined) meta.push(`cell:${cellNumber}`);
216
+ if (cellType) meta.push(`type:${cellType}`);
217
+
218
+ const text = renderStatusLine(
219
+ { icon: "pending", title: "Notebook", description: args.action || "?", meta },
220
+ uiTheme,
221
+ );
254
222
  return new Text(text, 0, 0);
255
223
  },
256
224
 
@@ -258,10 +226,13 @@ export const notebookToolRenderer = {
258
226
  result: { content: Array<{ type: string; text?: string }>; details?: NotebookToolDetails },
259
227
  { expanded }: RenderResultOptions,
260
228
  uiTheme: Theme,
229
+ args?: NotebookRenderArgs,
261
230
  ): Component {
262
231
  const content = result.content?.[0];
263
232
  if (content?.type === "text" && content.text?.startsWith("Error:")) {
264
- return new Text(formatErrorMessage(content.text, uiTheme), 0, 0);
233
+ const notebookPath = args?.notebookPath ?? args?.notebook_path ?? "?";
234
+ const header = renderStatusLine({ icon: "error", title: "Notebook", description: notebookPath }, uiTheme);
235
+ return new Text([header, formatErrorMessage(content.text, uiTheme)].join("\n"), 0, 0);
265
236
  }
266
237
 
267
238
  const details = result.details;
@@ -269,25 +240,38 @@ export const notebookToolRenderer = {
269
240
  const cellIndex = details?.cellIndex;
270
241
  const cellType = details?.cellType;
271
242
  const totalCells = details?.totalCells;
272
- const cellSource = details?.cellSource;
273
- const lineCount = cellSource?.length;
274
- const canExpand = cellSource !== undefined && cellSource.length > COLLAPSED_TEXT_LIMIT;
243
+ const cellSource = details?.cellSource ?? [];
244
+ const lineCount = cellSource.length;
275
245
 
276
- const icon = uiTheme.styledSymbol("status.success", "success");
277
246
  const actionLabel = action === "insert" ? "Inserted" : action === "delete" ? "Deleted" : "Edited";
278
247
  const cellLabel = cellType || "cell";
279
- const summaryParts = [`${actionLabel} ${cellLabel} at index ${cellIndex ?? "?"}`];
280
- if (lineCount !== undefined) summaryParts.push(formatCount("line", lineCount));
248
+ const summaryParts = [`${actionLabel} ${cellLabel} ${cellIndex ?? "?"}`];
249
+ if (lineCount > 0) summaryParts.push(formatCount("line", lineCount));
281
250
  if (totalCells !== undefined) summaryParts.push(`${totalCells} total`);
282
- const summaryText = summaryParts.join(uiTheme.sep.dot);
283
251
 
284
- const expandHint = expanded || !canExpand ? "" : formatExpandHint(uiTheme);
285
- let text = `${icon} ${uiTheme.fg("dim", summaryText)}${expandHint}`;
252
+ const outputLines = summaryParts.map((part) => uiTheme.fg("dim", part));
253
+ const codeText = cellSource.join("");
254
+ const language = cellType === "markdown" ? "markdown" : undefined;
286
255
 
287
- if (cellSource) {
288
- text += renderCellPreview(cellSource, expanded, uiTheme);
289
- }
290
-
291
- return new Text(text, 0, 0);
256
+ const notebookPath = args?.notebookPath ?? args?.notebook_path;
257
+ const notebookLabel = notebookPath ? `${actionLabel} ${notebookPath}` : "Notebook";
258
+ return {
259
+ render: (width: number) =>
260
+ renderCodeCell(
261
+ {
262
+ code: codeText,
263
+ language,
264
+ title: notebookLabel,
265
+ status: "complete",
266
+ output: outputLines.join("\n"),
267
+ codeMaxLines: expanded ? Number.POSITIVE_INFINITY : COLLAPSED_TEXT_LIMIT,
268
+ expanded,
269
+ width,
270
+ },
271
+ uiTheme,
272
+ ),
273
+ invalidate: () => {},
274
+ };
292
275
  },
276
+ mergeCallAndResult: true,
293
277
  };
@@ -1,5 +1,5 @@
1
1
  import { ArtifactManager } from "@oh-my-pi/pi-coding-agent/session/artifacts";
2
- import type { ToolSession } from "./index";
2
+ import type { ToolSession } from ".";
3
3
 
4
4
  export interface TailBuffer {
5
5
  append(chunk: string): void;
@@ -6,18 +6,19 @@ import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibilit
6
6
  import { executePython, getPreludeDocs, type PythonExecutorOptions } from "@oh-my-pi/pi-coding-agent/ipy/executor";
7
7
  import type { PreludeHelper, PythonStatusEvent } from "@oh-my-pi/pi-coding-agent/ipy/kernel";
8
8
  import { truncateToVisualLines } from "@oh-my-pi/pi-coding-agent/modes/components/visual-truncate";
9
- import { highlightCode, type Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
9
+ import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
10
10
  import pythonDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/python.md" with { type: "text" };
11
11
  import { OutputSink, type OutputSummary } from "@oh-my-pi/pi-coding-agent/session/streaming-output";
12
12
  import type { OutputMeta } from "@oh-my-pi/pi-coding-agent/tools/output-meta";
13
13
  import { ToolAbortError, ToolError } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
14
+ import { getTreeBranch, getTreeContinuePrefix, renderCodeCell } from "@oh-my-pi/pi-coding-agent/tui";
14
15
  import type { Component } from "@oh-my-pi/pi-tui";
15
- import { Text, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
16
+ import { Text, truncateToWidth } from "@oh-my-pi/pi-tui";
16
17
  import { type Static, Type } from "@sinclair/typebox";
17
- import type { ToolSession } from "./index";
18
+ import type { ToolSession } from ".";
18
19
  import { allocateOutputArtifact, createTailBuffer } from "./output-utils";
19
20
  import { resolveToCwd } from "./path-utils";
20
- import { getTreeBranch, getTreeContinuePrefix, shortenPath, ToolUIKit, truncate } from "./render-utils";
21
+ import { shortenPath, ToolUIKit, truncate } from "./render-utils";
21
22
  import { toolResult } from "./tool-result";
22
23
  import { DEFAULT_MAX_BYTES } from "./truncate";
23
24
 
@@ -740,56 +741,6 @@ function renderStatusEvents(events: PythonStatusEvent[], theme: Theme, expanded:
740
741
  return lines;
741
742
  }
742
743
 
743
- function applyCellBackground(line: string, width: number, bgFn?: (text: string) => string): string {
744
- if (!bgFn) return line;
745
- if (width <= 0) return bgFn(line);
746
- const paddingNeeded = Math.max(0, width - visibleWidth(line));
747
- const padded = line + " ".repeat(paddingNeeded);
748
- return bgFn(padded);
749
- }
750
-
751
- function highlightPythonCode(code?: string): string[] {
752
- return highlightCode(code ?? "", "python");
753
- }
754
-
755
- function formatCellStatus(cell: PythonCellResult, ui: ToolUIKit, spinnerFrame?: number): string | undefined {
756
- switch (cell.status) {
757
- case "pending":
758
- return `${ui.statusIcon("pending")} ${ui.theme.fg("muted", "pending")}`;
759
- case "running":
760
- return `${ui.statusIcon("running", spinnerFrame)} ${ui.theme.fg("muted", "running")}`;
761
- case "complete":
762
- return ui.statusIcon("success");
763
- case "error":
764
- return ui.statusIcon("error");
765
- }
766
- }
767
-
768
- function formatCellHeader(
769
- cell: PythonCellResult,
770
- index: number,
771
- total: number,
772
- ui: ToolUIKit,
773
- spinnerFrame?: number,
774
- workdirLabel?: string,
775
- ): string {
776
- const indexLabel = ui.theme.fg("accent", `[${index + 1}/${total}]`);
777
- const title = cell.title ? ` ${cell.title}` : "";
778
- const metaParts: string[] = [];
779
- if (workdirLabel) {
780
- metaParts.push(ui.theme.fg("dim", workdirLabel));
781
- }
782
- if (cell.durationMs !== undefined) {
783
- metaParts.push(ui.theme.fg("dim", `(${ui.formatDuration(cell.durationMs)})`));
784
- }
785
- const statusLabel = formatCellStatus(cell, ui, spinnerFrame);
786
- if (statusLabel) {
787
- metaParts.push(statusLabel);
788
- }
789
- const meta = metaParts.length > 0 ? ` ${metaParts.join(ui.theme.fg("dim", ui.theme.sep.dot))}` : "";
790
- return `${indexLabel}${title}${meta}`;
791
- }
792
-
793
744
  function formatCellOutputLines(
794
745
  cell: PythonCellResult,
795
746
  expanded: boolean,
@@ -808,101 +759,6 @@ function formatCellOutputLines(
808
759
  return { lines: outputLines, hiddenCount };
809
760
  }
810
761
 
811
- function renderCellBlock(
812
- cell: PythonCellResult,
813
- index: number,
814
- total: number,
815
- ui: ToolUIKit,
816
- options: {
817
- expanded: boolean;
818
- previewLines: number;
819
- spinnerFrame?: number;
820
- showOutput: boolean;
821
- workdirLabel?: string;
822
- width: number;
823
- bgFn?: (text: string) => string;
824
- },
825
- ): string[] {
826
- const { expanded, previewLines, spinnerFrame, showOutput, workdirLabel, width, bgFn } = options;
827
- const h = ui.theme.boxSharp.horizontal;
828
- const v = ui.theme.boxSharp.vertical;
829
- const cap = h.repeat(3);
830
- const border = (text: string) => ui.theme.fg("dim", text);
831
- const lineWidth = Math.max(0, width);
832
-
833
- const buildBarLine = (leftChar: string, label?: string): string => {
834
- const left = border(`${leftChar}${cap}`);
835
- if (lineWidth <= 0) return left;
836
- const rawLabel = label ? ` ${label} ` : " ";
837
- const maxLabelWidth = Math.max(0, lineWidth - visibleWidth(left));
838
- const trimmedLabel = truncateToWidth(rawLabel, maxLabelWidth, ui.theme.format.ellipsis);
839
- const fillCount = Math.max(0, lineWidth - visibleWidth(left + trimmedLabel));
840
- return `${left}${trimmedLabel}${border(h.repeat(fillCount))}`;
841
- };
842
-
843
- const lines: string[] = [];
844
- lines.push(
845
- applyCellBackground(
846
- buildBarLine(ui.theme.boxSharp.topLeft, formatCellHeader(cell, index, total, ui, spinnerFrame, workdirLabel)),
847
- lineWidth,
848
- bgFn,
849
- ),
850
- );
851
-
852
- const codePrefix = border(`${v} `);
853
- const codeWidth = Math.max(0, lineWidth - visibleWidth(codePrefix));
854
- const codeLines = highlightPythonCode(cell.code);
855
- for (const line of codeLines) {
856
- const text = truncateToWidth(line, codeWidth, ui.theme.format.ellipsis);
857
- lines.push(applyCellBackground(`${codePrefix}${text}`, lineWidth, bgFn));
858
- }
859
-
860
- const statusLines = renderStatusEvents(cell.statusEvents ?? [], ui.theme, expanded);
861
- const outputContent = formatCellOutputLines(cell, expanded, previewLines, ui.theme);
862
- const hasOutput = outputContent.lines.length > 0;
863
- const hasStatus = statusLines.length > 0;
864
- const showOutputSection = showOutput && (hasOutput || hasStatus);
865
-
866
- if (showOutputSection) {
867
- lines.push(
868
- applyCellBackground(
869
- buildBarLine(ui.theme.boxSharp.teeRight, ui.theme.fg("toolTitle", "Output")),
870
- lineWidth,
871
- bgFn,
872
- ),
873
- );
874
-
875
- for (const line of outputContent.lines) {
876
- const text = truncateToWidth(line, codeWidth, ui.theme.format.ellipsis);
877
- lines.push(applyCellBackground(`${codePrefix}${text}`, lineWidth, bgFn));
878
- }
879
- if (!expanded && outputContent.hiddenCount > 0) {
880
- const hint = ui.theme.fg(
881
- "dim",
882
- `${ui.theme.format.ellipsis} ${outputContent.hiddenCount} more lines (ctrl+o to expand)`,
883
- );
884
- lines.push(
885
- applyCellBackground(
886
- `${codePrefix}${truncateToWidth(hint, codeWidth, ui.theme.format.ellipsis)}`,
887
- lineWidth,
888
- bgFn,
889
- ),
890
- );
891
- }
892
-
893
- for (const line of statusLines) {
894
- const text = truncateToWidth(line, codeWidth, ui.theme.format.ellipsis);
895
- lines.push(applyCellBackground(`${codePrefix}${text}`, lineWidth, bgFn));
896
- }
897
- }
898
-
899
- const bottomLeft = border(`${ui.theme.boxSharp.bottomLeft}${cap}`);
900
- const bottomFillCount = Math.max(0, lineWidth - visibleWidth(bottomLeft));
901
- const bottomLine = `${bottomLeft}${border(h.repeat(bottomFillCount))}`;
902
- lines.push(applyCellBackground(bottomLine, lineWidth, bgFn));
903
- return lines;
904
- }
905
-
906
762
  export const pythonToolRenderer = {
907
763
  renderCall(args: PythonRenderArgs, uiTheme: Theme): Component {
908
764
  const ui = new ToolUIKit(uiTheme);
@@ -937,23 +793,24 @@ export const pythonToolRenderer = {
937
793
  const lines: string[] = [];
938
794
  for (let i = 0; i < cells.length; i++) {
939
795
  const cell = cells[i];
940
- const cellResult: PythonCellResult = {
941
- index: i,
942
- title: cell.title,
943
- code: cell.code,
944
- output: "",
945
- status: "pending",
946
- };
947
- lines.push(
948
- ...renderCellBlock(cellResult, i, cells.length, ui, {
949
- expanded: true,
950
- previewLines: PYTHON_DEFAULT_PREVIEW_LINES,
951
- showOutput: false,
952
- workdirLabel: i === 0 ? workdirLabel : undefined,
796
+ const cellTitle = cell.title;
797
+ const combinedTitle =
798
+ cellTitle && workdirLabel ? `${workdirLabel} · ${cellTitle}` : (cellTitle ?? workdirLabel);
799
+ const cellLines = renderCodeCell(
800
+ {
801
+ code: cell.code,
802
+ language: "python",
803
+ index: i,
804
+ total: cells.length,
805
+ title: combinedTitle,
806
+ status: "pending",
953
807
  width,
954
- bgFn: (text: string) => uiTheme.bg("toolPendingBg", text),
955
- }),
808
+ codeMaxLines: PYTHON_DEFAULT_PREVIEW_LINES,
809
+ expanded: true,
810
+ },
811
+ uiTheme,
956
812
  );
813
+ lines.push(...cellLines);
957
814
  if (i < cells.length - 1) {
958
815
  lines.push("");
959
816
  }
@@ -975,7 +832,7 @@ export const pythonToolRenderer = {
975
832
 
976
833
  const expanded = renderContext?.expanded ?? options.expanded;
977
834
  const previewLines = renderContext?.previewLines ?? PYTHON_DEFAULT_PREVIEW_LINES;
978
- const output = renderContext?.output ?? (result.content?.find((c) => c.type === "text")?.text ?? "").trim();
835
+ const output = renderContext?.output ?? (result.content?.find((c) => c.type === "text")?.text ?? "").trimEnd();
979
836
 
980
837
  const jsonOutputs = details?.jsonOutputs ?? [];
981
838
  const jsonLines = jsonOutputs.flatMap((value, index) => {
@@ -1015,23 +872,42 @@ export const pythonToolRenderer = {
1015
872
  const lines: string[] = [];
1016
873
  for (let i = 0; i < cellResults.length; i++) {
1017
874
  const cell = cellResults[i];
1018
- const showOutput = cell.status !== "pending";
1019
- const bgColor =
1020
- cell.status === "error"
1021
- ? "toolErrorBg"
1022
- : cell.status === "complete"
1023
- ? "toolSuccessBg"
1024
- : "toolPendingBg";
1025
- lines.push(
1026
- ...renderCellBlock(cell, i, cellResults.length, ui, {
1027
- expanded,
1028
- previewLines,
875
+ const statusLines = renderStatusEvents(cell.statusEvents ?? [], uiTheme, expanded);
876
+ const outputContent = formatCellOutputLines(cell, expanded, previewLines, uiTheme);
877
+ const outputLines = [...outputContent.lines];
878
+ if (!expanded && outputContent.hiddenCount > 0) {
879
+ outputLines.push(
880
+ uiTheme.fg(
881
+ "dim",
882
+ `${uiTheme.format.ellipsis} ${outputContent.hiddenCount} more lines (ctrl+o to expand)`,
883
+ ),
884
+ );
885
+ }
886
+ if (statusLines.length > 0) {
887
+ if (outputLines.length > 0) {
888
+ outputLines.push(uiTheme.fg("dim", "Status"));
889
+ }
890
+ outputLines.push(...statusLines);
891
+ }
892
+ const cellLines = renderCodeCell(
893
+ {
894
+ code: cell.code,
895
+ language: "python",
896
+ index: i,
897
+ total: cellResults.length,
898
+ title: cell.title,
899
+ status: cell.status,
1029
900
  spinnerFrame: options.spinnerFrame,
1030
- showOutput,
901
+ duration: cell.durationMs,
902
+ output: outputLines.length > 0 ? outputLines.join("\n") : undefined,
903
+ outputMaxLines: outputLines.length,
904
+ codeMaxLines: expanded ? Number.POSITIVE_INFINITY : PYTHON_DEFAULT_PREVIEW_LINES,
905
+ expanded,
1031
906
  width,
1032
- bgFn: (text: string) => uiTheme.bg(bgColor, text),
1033
- }),
907
+ },
908
+ uiTheme,
1034
909
  );
910
+ lines.push(...cellLines);
1035
911
  if (i < cellResults.length - 1) {
1036
912
  lines.push("");
1037
913
  }
@@ -1066,7 +942,9 @@ export const pythonToolRenderer = {
1066
942
  }
1067
943
 
1068
944
  if (!combinedOutput && statusLines.length > 0) {
1069
- const lines = [...statusLines, timeoutLine, warningLine].filter(Boolean) as string[];
945
+ const lines = [uiTheme.fg("dim", "Status"), ...statusLines, timeoutLine, warningLine].filter(
946
+ Boolean,
947
+ ) as string[];
1070
948
  return new Text(lines.join("\n"), 0, 0);
1071
949
  }
1072
950
 
@@ -1075,7 +953,12 @@ export const pythonToolRenderer = {
1075
953
  .split("\n")
1076
954
  .map((line) => uiTheme.fg("toolOutput", line))
1077
955
  .join("\n");
1078
- const lines = [styledOutput, ...statusLines, timeoutLine, warningLine].filter(Boolean) as string[];
956
+ const lines = [
957
+ styledOutput,
958
+ ...(statusLines.length > 0 ? [uiTheme.fg("dim", "Status"), ...statusLines] : []),
959
+ timeoutLine,
960
+ warningLine,
961
+ ].filter(Boolean) as string[];
1079
962
  return new Text(lines.join("\n"), 0, 0);
1080
963
  }
1081
964
 
@@ -1107,8 +990,13 @@ export const pythonToolRenderer = {
1107
990
  outputLines.push(truncateToWidth(skippedLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
1108
991
  }
1109
992
  outputLines.push(...cachedLines);
1110
- for (const statusLine of statusLines) {
1111
- outputLines.push(truncateToWidth(statusLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
993
+ if (statusLines.length > 0) {
994
+ outputLines.push(
995
+ truncateToWidth(uiTheme.fg("dim", "Status"), width, uiTheme.fg("dim", uiTheme.format.ellipsis)),
996
+ );
997
+ for (const statusLine of statusLines) {
998
+ outputLines.push(truncateToWidth(statusLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
999
+ }
1112
1000
  }
1113
1001
  if (timeoutLine) {
1114
1002
  outputLines.push(truncateToWidth(timeoutLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));