@nghyane/arcane 0.1.13 → 0.1.15

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 (303) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +21 -70
  3. package/scripts/format-prompts.ts +1 -3
  4. package/src/cli/args.ts +2 -7
  5. package/src/cli/config-cli.ts +1 -1
  6. package/src/cli/plugin-cli.ts +1 -1
  7. package/src/cli/setup-cli.ts +1 -1
  8. package/src/cli/update-cli.ts +1 -1
  9. package/src/cli/web-search-cli.ts +1 -1
  10. package/src/cli.ts +0 -1
  11. package/src/commands/config.ts +1 -1
  12. package/src/commands/grep.ts +1 -1
  13. package/src/commands/jupyter.ts +1 -1
  14. package/src/commands/plugin.ts +1 -1
  15. package/src/commands/setup.ts +1 -1
  16. package/src/commands/shell.ts +1 -1
  17. package/src/commands/ssh.ts +1 -1
  18. package/src/commands/stats.ts +1 -1
  19. package/src/commands/update.ts +1 -1
  20. package/src/config/model-registry.ts +3 -4
  21. package/src/config/model-resolver.ts +36 -9
  22. package/src/config/prompt-templates.ts +1 -9
  23. package/src/config/settings-schema.ts +32 -88
  24. package/src/config/settings.ts +3 -4
  25. package/src/debug/index.ts +1 -1
  26. package/src/debug/log-formatting.ts +1 -1
  27. package/src/debug/log-viewer.ts +2 -2
  28. package/src/discovery/helpers.ts +13 -3
  29. package/src/exa/index.ts +1 -35
  30. package/src/exa/render.ts +30 -190
  31. package/src/export/html/index.ts +1 -1
  32. package/src/extensibility/custom-tools/loader.ts +1 -1
  33. package/src/extensibility/custom-tools/types.ts +5 -1
  34. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  35. package/src/extensibility/extensions/runner.ts +1 -1
  36. package/src/extensibility/extensions/types.ts +1 -1
  37. package/src/extensibility/extensions/wrapper.ts +7 -15
  38. package/src/extensibility/hooks/runner.ts +1 -1
  39. package/src/extensibility/hooks/types.ts +1 -1
  40. package/src/extensibility/plugins/doctor.ts +1 -1
  41. package/src/index.ts +13 -13
  42. package/src/lsp/index.ts +77 -24
  43. package/src/lsp/render.ts +34 -583
  44. package/src/lsp/types.ts +3 -3
  45. package/src/lsp/utils.ts +1 -1
  46. package/src/main.ts +1 -1
  47. package/src/mcp/tool-bridge.ts +1 -24
  48. package/src/modes/components/assistant-message.ts +7 -7
  49. package/src/modes/components/bash-execution.ts +50 -112
  50. package/src/modes/components/bordered-loader.ts +1 -1
  51. package/src/modes/components/branch-summary-message.ts +16 -10
  52. package/src/modes/components/compaction-summary-message.ts +20 -12
  53. package/src/modes/components/context-group.ts +106 -0
  54. package/src/modes/components/custom-message.ts +4 -5
  55. package/src/modes/components/diff.ts +2 -2
  56. package/src/modes/components/dynamic-border.ts +1 -1
  57. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  58. package/src/modes/components/extensions/extension-list.ts +1 -1
  59. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  60. package/src/modes/components/footer.ts +2 -2
  61. package/src/modes/components/history-search.ts +1 -1
  62. package/src/modes/components/hook-editor.ts +1 -1
  63. package/src/modes/components/hook-input.ts +1 -1
  64. package/src/modes/components/hook-message.ts +4 -5
  65. package/src/modes/components/hook-selector.ts +1 -1
  66. package/src/modes/components/index.ts +0 -2
  67. package/src/modes/components/keybinding-hints.ts +1 -1
  68. package/src/modes/components/login-dialog.ts +1 -1
  69. package/src/modes/components/mcp-add-wizard.ts +1 -1
  70. package/src/modes/components/model-selector.ts +1 -1
  71. package/src/modes/components/oauth-selector.ts +1 -1
  72. package/src/modes/components/plugin-settings.ts +1 -1
  73. package/src/modes/components/python-execution.ts +51 -91
  74. package/src/modes/components/queue-mode-selector.ts +1 -1
  75. package/src/modes/components/session-selector.ts +1 -1
  76. package/src/modes/components/settings-defs.ts +5 -10
  77. package/src/modes/components/settings-selector.ts +1 -1
  78. package/src/modes/components/show-images-selector.ts +1 -1
  79. package/src/modes/components/skill-message.ts +4 -4
  80. package/src/modes/components/status-line/segments.ts +2 -2
  81. package/src/modes/components/status-line/separators.ts +1 -1
  82. package/src/modes/components/status-line-segment-editor.ts +1 -1
  83. package/src/modes/components/status-line.ts +1 -1
  84. package/src/modes/components/theme-selector.ts +1 -1
  85. package/src/modes/components/thinking-selector.ts +1 -1
  86. package/src/modes/components/todo-display.ts +2 -4
  87. package/src/modes/components/todo-reminder.ts +4 -4
  88. package/src/modes/components/tool-execution.ts +118 -440
  89. package/src/modes/components/tool-image-display.ts +107 -0
  90. package/src/modes/components/tree-selector.ts +2 -2
  91. package/src/modes/components/ttsr-notification.ts +4 -17
  92. package/src/modes/components/user-message-selector.ts +1 -1
  93. package/src/modes/components/user-message.ts +9 -10
  94. package/src/modes/components/welcome.ts +1 -1
  95. package/src/modes/controllers/command-controller.ts +1 -1
  96. package/src/modes/controllers/event-controller.ts +58 -187
  97. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  98. package/src/modes/controllers/input-controller.ts +3 -1
  99. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  100. package/src/modes/controllers/selector-controller.ts +3 -26
  101. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  102. package/src/modes/interactive-mode.ts +3 -7
  103. package/src/modes/print-mode.ts +5 -5
  104. package/src/modes/rpc/rpc-mode.ts +1 -1
  105. package/src/modes/types.ts +1 -2
  106. package/src/modes/utils/ui-helpers.ts +34 -32
  107. package/src/patch/edit-tool.ts +742 -0
  108. package/src/patch/index.ts +32 -898
  109. package/src/patch/schemas.ts +208 -0
  110. package/src/patch/shared.ts +83 -151
  111. package/src/prompts/agents/explore.md +22 -37
  112. package/src/prompts/agents/init.md +1 -1
  113. package/src/prompts/agents/librarian.md +29 -20
  114. package/src/prompts/agents/oracle.md +9 -2
  115. package/src/prompts/agents/reviewer.md +14 -48
  116. package/src/prompts/agents/task.md +16 -8
  117. package/src/prompts/compaction/branch-summary.md +4 -1
  118. package/src/prompts/compaction/compaction-summary.md +4 -1
  119. package/src/prompts/system/subagent-system-prompt.md +1 -1
  120. package/src/prompts/system/system-prompt.md +162 -178
  121. package/src/prompts/system/verification-reminder.md +6 -0
  122. package/src/sdk.ts +0 -9
  123. package/src/session/agent-session.ts +244 -1459
  124. package/src/session/model-controller.ts +406 -0
  125. package/src/session/retry-utils.ts +71 -0
  126. package/src/session/session-manager.ts +22 -186
  127. package/src/session/session-types.ts +312 -0
  128. package/src/session/stats.ts +387 -0
  129. package/src/session/streaming-edit.ts +258 -0
  130. package/src/session/ttsr.ts +213 -0
  131. package/src/slash-commands/builtin-registry.ts +0 -8
  132. package/src/stt/recorder.ts +2 -2
  133. package/src/system-prompt.ts +1 -14
  134. package/src/task/agents.ts +7 -33
  135. package/src/task/executor.ts +50 -438
  136. package/src/task/index.ts +104 -71
  137. package/src/task/progress-tracker.ts +390 -0
  138. package/src/task/render.ts +371 -187
  139. package/src/task/subprocess-tool-registry.ts +1 -1
  140. package/src/task/types.ts +14 -47
  141. package/src/tools/ask.ts +31 -42
  142. package/src/tools/bash-interactive.ts +2 -2
  143. package/src/tools/bash-interceptor.ts +2 -2
  144. package/src/tools/bash-normalize.ts +1 -1
  145. package/src/tools/bash-skill-urls.ts +2 -2
  146. package/src/tools/bash.ts +87 -136
  147. package/src/tools/browser.ts +54 -84
  148. package/src/tools/create-tools.ts +186 -0
  149. package/src/tools/default-renderer.ts +104 -0
  150. package/src/tools/explore.ts +11 -10
  151. package/src/tools/fetch.ts +24 -114
  152. package/src/tools/find.ts +48 -132
  153. package/src/tools/gemini-image.ts +5 -15
  154. package/src/tools/github.ts +450 -0
  155. package/src/tools/grep.ts +43 -179
  156. package/src/tools/index.ts +35 -198
  157. package/src/tools/json-tree.ts +3 -3
  158. package/src/tools/librarian.ts +18 -18
  159. package/src/tools/list-limit.ts +2 -2
  160. package/src/tools/notebook.ts +35 -87
  161. package/src/tools/oracle.ts +25 -25
  162. package/src/tools/output-meta.ts +89 -4
  163. package/src/tools/output-utils.ts +2 -2
  164. package/src/tools/python.ts +86 -637
  165. package/src/tools/read.ts +36 -119
  166. package/src/tools/reviewer-tool.ts +19 -21
  167. package/src/tools/search-code.ts +128 -0
  168. package/src/tools/ssh.ts +67 -126
  169. package/src/tools/subagent-tool.ts +197 -123
  170. package/src/tools/todo-write.ts +15 -31
  171. package/src/tools/tool-errors.ts +0 -30
  172. package/src/tools/undo-edit.ts +30 -67
  173. package/src/tools/write.ts +78 -127
  174. package/src/tui/code-cell.ts +4 -4
  175. package/src/tui/file-list.ts +2 -2
  176. package/src/tui/output-block.ts +1 -1
  177. package/src/tui/status-line.ts +1 -1
  178. package/src/tui/tree-list.ts +2 -2
  179. package/src/tui/types.ts +1 -1
  180. package/src/tui/utils.ts +1 -1
  181. package/src/{tools → ui}/render-utils.ts +87 -126
  182. package/src/utils/external-editor.ts +4 -4
  183. package/src/utils/file-mentions.ts +1 -1
  184. package/src/utils/index.ts +30 -0
  185. package/src/utils/tools-manager.ts +9 -19
  186. package/src/web/github-client.ts +290 -0
  187. package/src/web/scrapers/github.ts +11 -62
  188. package/src/web/search/auth.ts +1 -3
  189. package/src/web/search/index.ts +82 -46
  190. package/src/web/search/provider.ts +11 -16
  191. package/src/web/search/providers/grep.ts +160 -0
  192. package/src/web/search/render.ts +48 -235
  193. package/src/web/search/types.ts +1 -1
  194. package/src/commands/commit.ts +0 -36
  195. package/src/commit/agentic/agent.ts +0 -311
  196. package/src/commit/agentic/fallback.ts +0 -96
  197. package/src/commit/agentic/index.ts +0 -359
  198. package/src/commit/agentic/prompts/analyze-file.md +0 -22
  199. package/src/commit/agentic/prompts/session-user.md +0 -25
  200. package/src/commit/agentic/prompts/split-confirm.md +0 -1
  201. package/src/commit/agentic/prompts/system.md +0 -38
  202. package/src/commit/agentic/state.ts +0 -69
  203. package/src/commit/agentic/tools/analyze-file.ts +0 -118
  204. package/src/commit/agentic/tools/git-file-diff.ts +0 -194
  205. package/src/commit/agentic/tools/git-hunk.ts +0 -50
  206. package/src/commit/agentic/tools/git-overview.ts +0 -84
  207. package/src/commit/agentic/tools/index.ts +0 -56
  208. package/src/commit/agentic/tools/propose-changelog.ts +0 -128
  209. package/src/commit/agentic/tools/propose-commit.ts +0 -154
  210. package/src/commit/agentic/tools/recent-commits.ts +0 -81
  211. package/src/commit/agentic/tools/split-commit.ts +0 -280
  212. package/src/commit/agentic/topo-sort.ts +0 -44
  213. package/src/commit/agentic/trivial.ts +0 -51
  214. package/src/commit/agentic/validation.ts +0 -200
  215. package/src/commit/analysis/conventional.ts +0 -165
  216. package/src/commit/analysis/index.ts +0 -4
  217. package/src/commit/analysis/scope.ts +0 -242
  218. package/src/commit/analysis/summary.ts +0 -112
  219. package/src/commit/analysis/validation.ts +0 -66
  220. package/src/commit/changelog/detect.ts +0 -37
  221. package/src/commit/changelog/generate.ts +0 -110
  222. package/src/commit/changelog/index.ts +0 -234
  223. package/src/commit/changelog/parse.ts +0 -44
  224. package/src/commit/cli.ts +0 -93
  225. package/src/commit/git/diff.ts +0 -148
  226. package/src/commit/git/errors.ts +0 -9
  227. package/src/commit/git/index.ts +0 -211
  228. package/src/commit/git/operations.ts +0 -54
  229. package/src/commit/index.ts +0 -5
  230. package/src/commit/map-reduce/index.ts +0 -64
  231. package/src/commit/map-reduce/map-phase.ts +0 -178
  232. package/src/commit/map-reduce/reduce-phase.ts +0 -145
  233. package/src/commit/map-reduce/utils.ts +0 -9
  234. package/src/commit/message.ts +0 -11
  235. package/src/commit/model-selection.ts +0 -69
  236. package/src/commit/pipeline.ts +0 -243
  237. package/src/commit/prompts/analysis-system.md +0 -148
  238. package/src/commit/prompts/analysis-user.md +0 -38
  239. package/src/commit/prompts/changelog-system.md +0 -50
  240. package/src/commit/prompts/changelog-user.md +0 -18
  241. package/src/commit/prompts/file-observer-system.md +0 -24
  242. package/src/commit/prompts/file-observer-user.md +0 -8
  243. package/src/commit/prompts/reduce-system.md +0 -50
  244. package/src/commit/prompts/reduce-user.md +0 -17
  245. package/src/commit/prompts/summary-retry.md +0 -3
  246. package/src/commit/prompts/summary-system.md +0 -38
  247. package/src/commit/prompts/summary-user.md +0 -13
  248. package/src/commit/prompts/types-description.md +0 -2
  249. package/src/commit/types.ts +0 -109
  250. package/src/commit/utils/exclusions.ts +0 -42
  251. package/src/mcp/render.ts +0 -123
  252. package/src/modes/components/agent-dashboard.ts +0 -1130
  253. package/src/modes/components/codemode-group.ts +0 -369
  254. package/src/modes/components/read-tool-group.ts +0 -119
  255. package/src/modes/components/visual-truncate.ts +0 -63
  256. package/src/prompts/system/subagent-user-prompt.md +0 -8
  257. package/src/prompts/tools/ask.md +0 -44
  258. package/src/prompts/tools/bash.md +0 -24
  259. package/src/prompts/tools/browser.md +0 -33
  260. package/src/prompts/tools/calculator.md +0 -12
  261. package/src/prompts/tools/explore.md +0 -29
  262. package/src/prompts/tools/fetch.md +0 -16
  263. package/src/prompts/tools/find.md +0 -18
  264. package/src/prompts/tools/gemini-image.md +0 -23
  265. package/src/prompts/tools/grep.md +0 -28
  266. package/src/prompts/tools/hashline.md +0 -232
  267. package/src/prompts/tools/librarian.md +0 -24
  268. package/src/prompts/tools/lsp.md +0 -28
  269. package/src/prompts/tools/oracle.md +0 -26
  270. package/src/prompts/tools/patch.md +0 -74
  271. package/src/prompts/tools/python.md +0 -66
  272. package/src/prompts/tools/read.md +0 -36
  273. package/src/prompts/tools/replace.md +0 -38
  274. package/src/prompts/tools/reviewer.md +0 -41
  275. package/src/prompts/tools/ssh.md +0 -51
  276. package/src/prompts/tools/task-summary.md +0 -28
  277. package/src/prompts/tools/task.md +0 -146
  278. package/src/prompts/tools/todo-write.md +0 -65
  279. package/src/prompts/tools/undo-edit.md +0 -7
  280. package/src/prompts/tools/web-search.md +0 -19
  281. package/src/prompts/tools/write.md +0 -18
  282. package/src/task/batch.ts +0 -102
  283. package/src/task/discovery.ts +0 -126
  284. package/src/task/parallel.ts +0 -84
  285. package/src/task/template.ts +0 -32
  286. package/src/tools/calculator.ts +0 -537
  287. package/src/tools/jtd-to-typescript.ts +0 -198
  288. package/src/tools/renderers.ts +0 -60
  289. package/src/tools/tool-result.ts +0 -86
  290. /package/src/{modes/theme → theme}/dark.json +0 -0
  291. /package/src/{modes/theme → theme}/defaults/dark-catppuccin.json +0 -0
  292. /package/src/{modes/theme → theme}/defaults/dark-dracula.json +0 -0
  293. /package/src/{modes/theme → theme}/defaults/dark-gruvbox.json +0 -0
  294. /package/src/{modes/theme → theme}/defaults/dark-solarized.json +0 -0
  295. /package/src/{modes/theme → theme}/defaults/dark-tokyo-night.json +0 -0
  296. /package/src/{modes/theme → theme}/defaults/index.ts +0 -0
  297. /package/src/{modes/theme → theme}/defaults/light-catppuccin.json +0 -0
  298. /package/src/{modes/theme → theme}/defaults/light-github.json +0 -0
  299. /package/src/{modes/theme → theme}/defaults/light-solarized.json +0 -0
  300. /package/src/{modes/theme → theme}/light.json +0 -0
  301. /package/src/{modes/theme → theme}/mermaid-cache.ts +0 -0
  302. /package/src/{modes/theme → theme}/theme-schema.json +0 -0
  303. /package/src/{modes/theme → theme}/theme.ts +0 -0
@@ -6,67 +6,43 @@ import type { Component } from "@nghyane/arcane-tui";
6
6
  import { Text } from "@nghyane/arcane-tui";
7
7
  import { getProjectDir } from "@nghyane/arcane-utils/dirs";
8
8
  import { type Static, Type } from "@sinclair/typebox";
9
- import { renderPromptTemplate } from "../config/prompt-templates";
10
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
- import { executePython, getPreludeDocs, type PythonExecutorOptions } from "../ipy/executor";
12
- import type { PreludeHelper, PythonStatusEvent } from "../ipy/kernel";
13
- import { truncateToVisualLines } from "../modes/components/visual-truncate";
14
- import type { Theme } from "../modes/theme/theme";
15
- import pythonDescription from "../prompts/tools/python.md" with { type: "text" };
10
+ import { executePython, type PythonExecutorOptions } from "../ipy/executor";
11
+ import type { PythonStatusEvent } from "../ipy/kernel";
16
12
  import { DEFAULT_MAX_BYTES, OutputSink, type OutputSummary } from "../session/streaming-output";
17
- import { getTreeBranch, getTreeContinuePrefix, renderCodeCell } from "../tui";
13
+ import type { Theme } from "../theme/theme";
14
+ import { renderCodeCell, renderStatusLine } from "../tui";
15
+ import { formatClickHint, PREVIEW_LIMITS, replaceTabs } from "../ui/render-utils";
18
16
  import type { ToolSession } from ".";
19
- import type { OutputMeta } from "./output-meta";
17
+ import { type OutputMeta, toolResult } from "./output-meta";
20
18
  import { allocateOutputArtifact, createTailBuffer } from "./output-utils";
21
19
  import { resolveToCwd } from "./path-utils";
22
- import { replaceTabs, shortenPath, ToolUIKit, truncateToWidth } from "./render-utils";
23
20
  import { ToolAbortError, ToolError } from "./tool-errors";
24
- import { toolResult } from "./tool-result";
25
21
 
26
22
  export const PYTHON_DEFAULT_PREVIEW_LINES = 10;
27
23
 
28
- type PreludeCategory = {
29
- name: string;
30
- functions: PreludeHelper[];
31
- };
32
-
33
- function groupPreludeHelpers(helpers: PreludeHelper[]): PreludeCategory[] {
34
- const categories: PreludeCategory[] = [];
35
- const byName = new Map<string, PreludeHelper[]>();
36
- for (const helper of helpers) {
37
- let bucket = byName.get(helper.category);
38
- if (!bucket) {
39
- bucket = [];
40
- byName.set(helper.category, bucket);
41
- categories.push({ name: helper.category, functions: bucket });
42
- }
43
- bucket.push(helper);
44
- }
45
- return categories;
46
- }
47
-
48
- export const pythonSchema = Type.Object({
24
+ const pythonSchema = Type.Object({
49
25
  cells: Type.Array(
50
26
  Type.Object({
51
27
  code: Type.String({ description: "Python code to execute" }),
52
- title: Type.Optional(Type.String({ description: "Cell label, e.g. 'imports', 'helper'" })),
28
+ title: Type.Optional(Type.String({ description: "Optional label for the cell" })),
53
29
  }),
54
- { description: "Cells to execute sequentially in persistent kernel" },
30
+ { description: "Code cells to execute sequentially" },
55
31
  ),
56
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 30)" })),
57
- cwd: Type.Optional(Type.String({ description: "Working directory (default: cwd)" })),
58
- reset: Type.Optional(Type.Boolean({ description: "Restart kernel before execution" })),
32
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" })),
33
+ cwd: Type.Optional(Type.String({ description: "Working directory" })),
34
+ reset: Type.Optional(Type.Boolean({ description: "Reset kernel state before execution" })),
59
35
  });
60
- export type PythonToolParams = Static<typeof pythonSchema>;
36
+ type PythonToolParams = Static<typeof pythonSchema>;
61
37
 
62
- export type PythonToolResult = {
38
+ type PythonToolResult = {
63
39
  content: Array<{ type: "text"; text: string }>;
64
40
  details: PythonToolDetails | undefined;
65
41
  };
66
42
 
67
- export type PythonProxyExecutor = (params: PythonToolParams, signal?: AbortSignal) => Promise<PythonToolResult>;
43
+ type PythonProxyExecutor = (params: PythonToolParams, signal?: AbortSignal) => Promise<PythonToolResult>;
68
44
 
69
- export interface PythonCellResult {
45
+ interface PythonCellResult {
70
46
  index: number;
71
47
  title?: string;
72
48
  code: string;
@@ -88,64 +64,14 @@ export interface PythonToolDetails {
88
64
  meta?: OutputMeta;
89
65
  }
90
66
 
91
- function formatJsonScalar(value: unknown): string {
92
- if (value === null) return "null";
93
- if (value === undefined) return "undefined";
94
- if (typeof value === "string") return JSON.stringify(value);
95
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
96
- if (typeof value === "function") return "[function]";
97
- return "[object]";
98
- }
99
-
100
- function renderJsonTree(value: unknown, theme: Theme, expanded: boolean, maxDepth = expanded ? 6 : 2): string[] {
101
- const maxItems = expanded ? 20 : 5;
102
-
103
- const renderNode = (node: unknown, prefix: string, depth: number, isLast: boolean, label?: string): string[] => {
104
- const branch = getTreeBranch(isLast, theme);
105
- const displayLabel = label ? `${label}: ` : "";
106
-
107
- if (depth >= maxDepth || node === null || typeof node !== "object") {
108
- return [`${prefix}${branch} ${displayLabel}${formatJsonScalar(node)}`];
109
- }
110
-
111
- const isArray = Array.isArray(node);
112
- const entries = isArray
113
- ? node.map((val, index) => [String(index), val] as const)
114
- : Object.entries(node as object);
115
- const header = `${prefix}${branch} ${displayLabel}${isArray ? `Array(${entries.length})` : `Object(${entries.length})`}`;
116
- const lines = [header];
117
-
118
- const childPrefix = prefix + getTreeContinuePrefix(isLast, theme);
119
- const visible = entries.slice(0, maxItems);
120
- for (let i = 0; i < visible.length; i++) {
121
- const [key, val] = visible[i];
122
- const childLast = i === visible.length - 1 && (expanded || entries.length <= maxItems);
123
- lines.push(...renderNode(val, childPrefix, depth + 1, childLast, isArray ? `[${key}]` : key));
124
- }
125
- if (!expanded && entries.length > maxItems) {
126
- const moreBranch = theme.tree.last;
127
- lines.push(`${childPrefix}${moreBranch} ${entries.length - maxItems} more item(s)`);
128
- }
129
- return lines;
130
- };
131
-
132
- return renderNode(value, "", 0, true);
133
- }
134
-
135
- export function getPythonToolDescription(): string {
136
- const helpers = getPreludeDocs();
137
- const categories = groupPreludeHelpers(helpers);
138
- return renderPromptTemplate(pythonDescription, { categories });
139
- }
140
-
141
67
  export interface PythonToolOptions {
142
68
  proxyExecutor?: PythonProxyExecutor;
143
69
  }
144
70
 
145
- export class PythonTool implements AgentTool<typeof pythonSchema> {
71
+ export class PythonTool implements AgentTool<typeof pythonSchema, any, Theme> {
146
72
  readonly name = "python";
147
73
  readonly label = "Python";
148
- readonly description: string;
74
+ description = "Execute Python code in a persistent kernel";
149
75
  readonly parameters = pythonSchema;
150
76
  readonly concurrency = "exclusive";
151
77
 
@@ -156,7 +82,6 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
156
82
  options?: PythonToolOptions,
157
83
  ) {
158
84
  this.#proxyExecutor = options?.proxyExecutor;
159
- this.description = getPythonToolDescription();
160
85
  }
161
86
 
162
87
  async execute(
@@ -480,361 +405,26 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
480
405
  }
481
406
  }
482
407
  }
483
- }
484
-
485
- interface PythonRenderArgs {
486
- cells?: Array<{ code: string; title?: string }>;
487
- timeout?: number;
488
- cwd?: string;
489
- }
490
-
491
- interface PythonRenderContext {
492
- output?: string;
493
- expanded?: boolean;
494
- previewLines?: number;
495
- timeout?: number;
496
- }
497
-
498
- /** Format a status event as a single line for display. */
499
- function formatStatusEvent(event: PythonStatusEvent, theme: Theme): string {
500
- const { op, ...data } = event;
501
-
502
- // Map operations to available theme icons
503
- type AvailableIcon = "icon.file" | "icon.folder" | "icon.git" | "icon.package";
504
- const opIcons: Record<string, AvailableIcon> = {
505
- // File I/O
506
- read: "icon.file",
507
- write: "icon.file",
508
- append: "icon.file",
509
- cat: "icon.file",
510
- touch: "icon.file",
511
- lines: "icon.file",
512
- // Navigation/Directory
513
- ls: "icon.folder",
514
- cd: "icon.folder",
515
- pwd: "icon.folder",
516
- mkdir: "icon.folder",
517
- tree: "icon.folder",
518
- stat: "icon.folder",
519
- // Search (use file icon since no search icon)
520
- find: "icon.file",
521
- grep: "icon.file",
522
- rgrep: "icon.file",
523
- glob: "icon.file",
524
- // Edit operations (use file icon)
525
- replace: "icon.file",
526
- sed: "icon.file",
527
- rsed: "icon.file",
528
- delete_lines: "icon.file",
529
- delete_matching: "icon.file",
530
- insert_at: "icon.file",
531
- // Git
532
- git_status: "icon.git",
533
- git_diff: "icon.git",
534
- git_log: "icon.git",
535
- git_show: "icon.git",
536
- git_branch: "icon.git",
537
- git_file_at: "icon.git",
538
- git_has_changes: "icon.git",
539
- // Shell/batch (use package icon)
540
- run: "icon.package",
541
- sh: "icon.package",
542
- env: "icon.package",
543
- batch: "icon.package",
544
- };
545
-
546
- const iconKey = opIcons[op] ?? "icon.file";
547
- const icon = theme.styledSymbol(iconKey, "muted");
548
-
549
- // Format the status message based on operation type
550
- const parts: string[] = [];
551
-
552
- // Error handling
553
- if (data.error) {
554
- return `${icon} ${theme.fg("warning", op)}: ${theme.fg("dim", String(data.error))}`;
555
- }
556
-
557
- // Build description based on common fields
558
- switch (op) {
559
- case "read":
560
- parts.push(`${data.chars} chars`);
561
- if (data.path) parts.push(`from ${shortenPath(String(data.path))}`);
562
- break;
563
- case "write":
564
- case "append":
565
- parts.push(`${data.chars} chars`);
566
- if (data.path) parts.push(`to ${shortenPath(String(data.path))}`);
567
- break;
568
- case "cat":
569
- parts.push(`${data.files} file${(data.files as number) !== 1 ? "s" : ""}`);
570
- parts.push(`${data.chars} chars`);
571
- break;
572
- case "find":
573
- case "glob":
574
- parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
575
- if (data.pattern) parts.push(`for "${truncateToWidth(String(data.pattern), 20)}"`);
576
- break;
577
- case "grep":
578
- parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
579
- if (data.path) parts.push(`in ${shortenPath(String(data.path))}`);
580
- break;
581
- case "rgrep":
582
- parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
583
- if (data.pattern) parts.push(`for "${truncateToWidth(String(data.pattern), 20)}"`);
584
- break;
585
- case "ls":
586
- parts.push(`${data.count} entr${(data.count as number) !== 1 ? "ies" : "y"}`);
587
- break;
588
- case "env":
589
- if (data.action === "set") {
590
- parts.push(`set ${data.key}=${truncateToWidth(String(data.value ?? ""), 30)}`);
591
- } else if (data.action === "get") {
592
- parts.push(`${data.key}=${truncateToWidth(String(data.value ?? ""), 30)}`);
593
- } else {
594
- parts.push(`${data.count} variable${(data.count as number) !== 1 ? "s" : ""}`);
595
- }
596
- break;
597
- case "stat":
598
- if (data.is_dir) {
599
- parts.push("directory");
600
- } else {
601
- parts.push(`${data.size} bytes`);
602
- }
603
- if (data.path) parts.push(shortenPath(String(data.path)));
604
- break;
605
- case "replace":
606
- case "sed":
607
- parts.push(`${data.count} replacement${(data.count as number) !== 1 ? "s" : ""}`);
608
- if (data.path) parts.push(`in ${shortenPath(String(data.path))}`);
609
- break;
610
- case "rsed":
611
- parts.push(`${data.count} replacement${(data.count as number) !== 1 ? "s" : ""}`);
612
- if (data.files) parts.push(`in ${data.files} file${(data.files as number) !== 1 ? "s" : ""}`);
613
- break;
614
- case "git_status":
615
- if (data.clean) {
616
- parts.push("clean");
617
- } else {
618
- const statusParts: string[] = [];
619
- if (data.staged) statusParts.push(`${data.staged} staged`);
620
- if (data.modified) statusParts.push(`${data.modified} modified`);
621
- if (data.untracked) statusParts.push(`${data.untracked} untracked`);
622
- parts.push(statusParts.join(", ") || "unknown");
623
- }
624
- if (data.branch) parts.push(`on ${data.branch}`);
625
- break;
626
- case "git_log":
627
- parts.push(`${data.commits} commit${(data.commits as number) !== 1 ? "s" : ""}`);
628
- break;
629
- case "git_diff":
630
- parts.push(`${data.lines} line${(data.lines as number) !== 1 ? "s" : ""}`);
631
- if (data.staged) parts.push("(staged)");
632
- break;
633
- case "diff":
634
- if (data.identical) {
635
- parts.push("files identical");
636
- } else {
637
- parts.push("files differ");
638
- }
639
- break;
640
- case "batch":
641
- parts.push(`${data.files} file${(data.files as number) !== 1 ? "s" : ""} processed`);
642
- break;
643
- case "wc":
644
- parts.push(`${data.lines}L ${data.words}W ${data.chars}C`);
645
- break;
646
- case "lines":
647
- parts.push(`${data.count} line${(data.count as number) !== 1 ? "s" : ""}`);
648
- if (data.start && data.end) parts.push(`(${data.start}-${data.end})`);
649
- break;
650
- case "delete_lines":
651
- case "delete_matching":
652
- parts.push(`${data.count} line${(data.count as number) !== 1 ? "s" : ""} deleted`);
653
- break;
654
- case "insert_at":
655
- parts.push(`${data.lines_inserted} line${(data.lines_inserted as number) !== 1 ? "s" : ""} inserted`);
656
- break;
657
- case "cd":
658
- case "pwd":
659
- case "mkdir":
660
- case "touch":
661
- if (data.path) parts.push(shortenPath(String(data.path)));
662
- break;
663
- case "rm":
664
- case "mv":
665
- case "cp":
666
- if (data.src) parts.push(`${shortenPath(String(data.src))} → ${shortenPath(String(data.dst))}`);
667
- else if (data.path) parts.push(shortenPath(String(data.path)));
668
- break;
669
- default:
670
- // Generic formatting for other operations
671
- if (data.count !== undefined) {
672
- parts.push(String(data.count));
673
- }
674
- if (data.path) {
675
- parts.push(shortenPath(String(data.path)));
676
- }
677
- }
678
-
679
- const desc = parts.length > 0 ? parts.join(" · ") : "";
680
- return `${icon} ${theme.fg("muted", op)}${desc ? ` ${theme.fg("dim", desc)}` : ""}`;
681
- }
682
-
683
- /** Format status event with expanded detail lines. */
684
- function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): string[] {
685
- const lines: string[] = [];
686
- const { op, ...data } = event;
687
-
688
- // Main status line
689
- lines.push(formatStatusEvent(event, theme));
690
-
691
- // Add detail lines for operations with list data
692
- const addItems = (items: unknown[], formatter: (item: unknown) => string, max = 5) => {
693
- const arr = Array.isArray(items) ? items : [];
694
- for (let i = 0; i < Math.min(arr.length, max); i++) {
695
- lines.push(` ${theme.fg("dim", formatter(arr[i]))}`);
696
- }
697
- if (arr.length > max) {
698
- lines.push(` ${theme.fg("dim", `… ${arr.length - max} more`)}`);
699
- }
700
- };
701
-
702
- // Add preview lines (truncated content)
703
- const addPreview = (preview: string, maxLines = 3) => {
704
- const previewLines = String(preview).split("\n").slice(0, maxLines);
705
- for (const line of previewLines) {
706
- lines.push(` ${theme.fg("toolOutput", truncateToWidth(replaceTabs(line), 80))}`);
707
- }
708
- const totalLines = String(preview).split("\n").length;
709
- if (totalLines > maxLines) {
710
- lines.push(` ${theme.fg("dim", `… ${totalLines - maxLines} more lines`)}`);
711
- }
712
- };
713
-
714
- switch (op) {
715
- case "find":
716
- case "glob":
717
- if (data.matches) addItems(data.matches as unknown[], m => String(m));
718
- break;
719
- case "ls":
720
- if (data.items) addItems(data.items as unknown[], m => String(m));
721
- break;
722
- case "grep":
723
- if (data.hits) {
724
- addItems(data.hits as unknown[], h => {
725
- const hit = h as { line: number; text: string };
726
- return `${hit.line}: ${truncateToWidth(hit.text, 60)}`;
727
- });
728
- }
729
- break;
730
- case "rgrep":
731
- if (data.hits) {
732
- addItems(data.hits as unknown[], h => {
733
- const hit = h as { file: string; line: number; text: string };
734
- return `${shortenPath(hit.file)}:${hit.line}: ${truncateToWidth(hit.text, 50)}`;
735
- });
736
- }
737
- break;
738
- case "rsed":
739
- if (data.changed) {
740
- addItems(data.changed as unknown[], c => {
741
- const change = c as { file: string; count: number };
742
- return `${shortenPath(change.file)}: ${change.count} replacement${change.count !== 1 ? "s" : ""}`;
743
- });
744
- }
745
- break;
746
- case "env":
747
- if (data.keys) addItems(data.keys as unknown[], k => String(k), 10);
748
- break;
749
- case "git_log":
750
- if (data.entries) {
751
- addItems(data.entries as unknown[], e => {
752
- const entry = e as { sha: string; subject: string };
753
- return `${entry.sha} ${truncateToWidth(entry.subject, 50)}`;
754
- });
755
- }
756
- break;
757
- case "git_status":
758
- if (data.files) addItems(data.files as unknown[], f => String(f));
759
- break;
760
- case "git_branch":
761
- if (data.branches) addItems(data.branches as unknown[], b => String(b));
762
- break;
763
- case "read":
764
- case "cat":
765
- case "head":
766
- case "tail":
767
- case "tree":
768
- case "diff":
769
- case "lines":
770
- case "git_diff":
771
- case "sh":
772
- if (data.preview) addPreview(String(data.preview));
773
- break;
774
- }
775
-
776
- return lines;
777
- }
778
408
 
779
- /** Render status events as tree lines. */
780
- function renderStatusEvents(events: PythonStatusEvent[], theme: Theme, expanded: boolean): string[] {
781
- if (events.length === 0) return [];
782
-
783
- const maxCollapsed = 3;
784
- const maxExpanded = 10;
785
- const displayCount = expanded ? Math.min(events.length, maxExpanded) : Math.min(events.length, maxCollapsed);
786
-
787
- const lines: string[] = [];
788
- for (let i = 0; i < displayCount; i++) {
789
- const isLast = i === displayCount - 1 && (expanded || events.length <= maxCollapsed);
790
- const branch = isLast ? theme.tree.last : theme.tree.branch;
791
-
792
- if (expanded) {
793
- // Show expanded details for each event
794
- const eventLines = formatStatusEventExpanded(events[i], theme);
795
- lines.push(`${theme.fg("dim", branch)} ${eventLines[0]}`);
796
- const continueBranch = isLast ? " " : `${theme.tree.vertical} `;
797
- for (let j = 1; j < eventLines.length; j++) {
798
- lines.push(`${theme.fg("dim", continueBranch)}${eventLines[j]}`);
409
+ buildRenderContext(info: {
410
+ args: PythonToolParams;
411
+ result?: { content: Array<{ type: string; text?: string }>; details?: PythonToolDetails };
412
+ expanded: boolean;
413
+ getTextOutput: () => string;
414
+ }): Record<string, unknown> {
415
+ const context: Record<string, unknown> = {};
416
+ if (info.result) {
417
+ context.output = info.getTextOutput().trimEnd();
418
+ context.expanded = info.expanded;
419
+ context.previewLines = PYTHON_DEFAULT_PREVIEW_LINES;
420
+ if (typeof info.args.timeout === "number" && Number.isFinite(info.args.timeout)) {
421
+ context.timeout = Math.max(1, Math.min(600, info.args.timeout));
799
422
  }
800
- } else {
801
- lines.push(`${theme.fg("dim", branch)} ${formatStatusEvent(events[i], theme)}`);
802
423
  }
424
+ return context;
803
425
  }
804
426
 
805
- if (!expanded && events.length > maxCollapsed) {
806
- lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `… ${events.length - maxCollapsed} more`)}`);
807
- } else if (expanded && events.length > maxExpanded) {
808
- lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `… ${events.length - maxExpanded} more`)}`);
809
- }
810
-
811
- return lines;
812
- }
813
-
814
- function formatCellOutputLines(
815
- cell: PythonCellResult,
816
- expanded: boolean,
817
- previewLines: number,
818
- theme: Theme,
819
- ): { lines: string[]; hiddenCount: number } {
820
- const rawLines = cell.output ? cell.output.split("\n") : [];
821
- const displayLines = expanded ? rawLines : rawLines.slice(-previewLines);
822
- const hiddenCount = rawLines.length - displayLines.length;
823
- const outputLines = displayLines.map(line => {
824
- const cleaned = replaceTabs(line);
825
- return cell.status === "error" ? theme.fg("error", cleaned) : theme.fg("toolOutput", cleaned);
826
- });
827
-
828
- if (outputLines.length === 0) {
829
- return { lines: [], hiddenCount: 0 };
830
- }
831
-
832
- return { lines: outputLines, hiddenCount };
833
- }
834
-
835
- export const pythonToolRenderer = {
836
427
  renderCall(args: PythonRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
837
- const ui = new ToolUIKit(uiTheme);
838
428
  const cells = args.cells ?? [];
839
429
  const cwd = getProjectDir();
840
430
  let displayWorkdir = args.cwd;
@@ -858,7 +448,7 @@ export const pythonToolRenderer = {
858
448
  if (cells.length === 0) {
859
449
  const prompt = uiTheme.fg("accent", ">>>");
860
450
  const prefix = workdirLabel ? `${uiTheme.fg("dim", `${workdirLabel} && `)}` : "";
861
- const text = ui.title(`${prompt} ${prefix}…`);
451
+ const text = uiTheme.fg("toolTitle", uiTheme.bold(`${prompt} ${prefix}…`));
862
452
  return new Text(text, 0, 0);
863
453
  }
864
454
 
@@ -903,216 +493,75 @@ export const pythonToolRenderer = {
903
493
  cached = undefined;
904
494
  },
905
495
  };
906
- },
496
+ }
907
497
 
908
498
  renderResult(
909
499
  result: { content: Array<{ type: string; text?: string }>; details?: PythonToolDetails },
910
500
  options: RenderResultOptions & { renderContext?: PythonRenderContext },
911
501
  uiTheme: Theme,
912
502
  ): Component {
913
- const ui = new ToolUIKit(uiTheme);
914
503
  const details = result.details;
915
-
916
- const output =
917
- options.renderContext?.output ?? (result.content?.find(c => c.type === "text")?.text ?? "").trimEnd();
918
-
919
- const jsonOutputs = details?.jsonOutputs ?? [];
920
- const jsonLines = jsonOutputs.flatMap((value, index) => {
921
- const header = `JSON output ${index + 1}`;
922
- const treeLines = renderJsonTree(value, uiTheme, options.renderContext?.expanded ?? options.expanded);
923
- return [header, ...treeLines];
924
- });
925
-
504
+ const output = (
505
+ options.renderContext?.output ??
506
+ result.content?.find(c => c.type === "text")?.text ??
507
+ ""
508
+ ).trimEnd();
509
+ const outputLines = output ? output.split("\n") : [];
510
+ const total = outputLines.length;
511
+ const isError = details?.cells?.some(c => c.status === "error") ?? false;
926
512
  const truncation = details?.meta?.truncation;
927
- const timeoutSeconds = options.renderContext?.timeout;
928
- const timeoutLine =
929
- typeof timeoutSeconds === "number"
930
- ? uiTheme.fg("dim", ui.wrapBrackets(`Timeout: ${timeoutSeconds}s`))
931
- : undefined;
932
- let warningLine: string | undefined;
933
- if (truncation) {
934
- const warnings: string[] = [];
935
- if (truncation.artifactId) {
936
- warnings.push(`Full output: artifact://${truncation.artifactId}`);
937
- }
938
- if (truncation.truncatedBy === "lines") {
939
- warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
940
- } else {
941
- warnings.push(
942
- `Truncated: ${truncation.outputLines} lines shown (${ui.formatBytes(truncation.outputBytes)} limit)`,
943
- );
944
- }
945
- if (warnings.length > 0) {
946
- warningLine = uiTheme.fg("warning", ui.wrapBrackets(warnings.join(". ")));
947
- }
513
+ const expanded = options.renderContext?.expanded ?? options.expanded;
514
+
515
+ // Build header
516
+ const cellCount = details?.cells?.length ?? 0;
517
+ const codePreview = details?.cells?.[0]?.code?.split("\n")[0]?.slice(0, 60) ?? "\u2026";
518
+ const meta: string[] = [];
519
+ if (cellCount > 1) meta.push(`${cellCount} cells`);
520
+ if (total > 0) meta.push(`${total} lines`);
521
+ if (details?.cells?.some(c => c.durationMs)) {
522
+ const totalMs = details.cells.reduce((sum, c) => sum + (c.durationMs ?? 0), 0);
523
+ if (totalMs > 0) meta.push(`${(totalMs / 1000).toFixed(1)}s`);
948
524
  }
949
525
 
950
- const cellResults = details?.cells;
951
- if (cellResults && cellResults.length > 0) {
952
- // Cache state following Box pattern
953
- let cached: { key: string; width: number; result: string[] } | undefined;
954
-
955
- return {
956
- render: (width: number): string[] => {
957
- // Read mutable state at render time
958
- const expanded = options.renderContext?.expanded ?? options.expanded;
959
- const previewLines = options.renderContext?.previewLines ?? PYTHON_DEFAULT_PREVIEW_LINES;
960
- const key = `${expanded}|${previewLines}|${options.spinnerFrame}`;
961
- if (cached && cached.key === key && cached.width === width) {
962
- return cached.result;
963
- }
964
-
965
- const lines: string[] = [];
966
- for (let i = 0; i < cellResults.length; i++) {
967
- const cell = cellResults[i];
968
- const statusLines = renderStatusEvents(cell.statusEvents ?? [], uiTheme, expanded);
969
- const outputContent = formatCellOutputLines(cell, expanded, previewLines, uiTheme);
970
- const outputLines = [...outputContent.lines];
971
- if (!expanded && outputContent.hiddenCount > 0) {
972
- outputLines.push(
973
- uiTheme.fg("dim", `… ${outputContent.hiddenCount} more lines (ctrl+o to expand)`),
974
- );
975
- }
976
- if (statusLines.length > 0) {
977
- if (outputLines.length > 0) {
978
- outputLines.push(uiTheme.fg("dim", "Status"));
979
- }
980
- outputLines.push(...statusLines);
981
- }
982
- const cellLines = renderCodeCell(
983
- {
984
- code: cell.code,
985
- language: "python",
986
- index: i,
987
- total: cellResults.length,
988
- title: cell.title,
989
- status: cell.status,
990
- spinnerFrame: options.spinnerFrame,
991
- duration: cell.durationMs,
992
- output: outputLines.length > 0 ? outputLines.join("\n") : undefined,
993
- outputMaxLines: outputLines.length,
994
- codeMaxLines: expanded ? Number.POSITIVE_INFINITY : PYTHON_DEFAULT_PREVIEW_LINES,
995
- expanded,
996
- width,
997
- },
998
- uiTheme,
999
- );
1000
- lines.push(...cellLines);
1001
- if (i < cellResults.length - 1) {
1002
- lines.push("");
1003
- }
1004
- }
1005
- if (jsonLines.length > 0) {
1006
- if (lines.length > 0) {
1007
- lines.push("");
1008
- }
1009
- lines.push(...jsonLines);
1010
- }
1011
- if (timeoutLine) {
1012
- lines.push(timeoutLine);
1013
- }
1014
- if (warningLine) {
1015
- lines.push(warningLine);
1016
- }
1017
- cached = { key, width, result: lines };
1018
- return lines;
1019
- },
1020
- invalidate: () => {
1021
- cached = undefined;
1022
- },
1023
- };
1024
- }
1025
-
1026
- const displayOutput = output;
1027
- const combinedOutput = [displayOutput, ...jsonLines].filter(Boolean).join("\n");
1028
-
1029
- const statusEvents = details?.statusEvents ?? [];
1030
- const statusLines = renderStatusEvents(
1031
- statusEvents,
526
+ const header = renderStatusLine(
527
+ { icon: isError ? "error" : "success", title: "Python", description: `>>> ${codePreview}`, meta },
1032
528
  uiTheme,
1033
- options.renderContext?.expanded ?? options.expanded,
1034
529
  );
1035
530
 
1036
- if (!combinedOutput && statusLines.length === 0) {
1037
- const lines = [timeoutLine, warningLine].filter(Boolean) as string[];
1038
- return new Text(lines.join("\n"), 0, 0);
1039
- }
531
+ // Tree-style body
532
+ const showAll = isError || expanded;
533
+ const displayLines = showAll ? outputLines : outputLines.slice(-PREVIEW_LIMITS.OUTPUT_COLLAPSED);
534
+ const skipped = total - displayLines.length;
1040
535
 
1041
- if (!combinedOutput && statusLines.length > 0) {
1042
- const lines = [uiTheme.fg("dim", "Status"), ...statusLines, timeoutLine, warningLine].filter(
1043
- Boolean,
1044
- ) as string[];
1045
- return new Text(lines.join("\n"), 0, 0);
536
+ const bodyLines: string[] = [];
537
+ if (skipped > 0) {
538
+ bodyLines.push(uiTheme.fg("dim", `\u2026 (${skipped} earlier lines)`));
1046
539
  }
1047
-
1048
- if (options.renderContext?.expanded ?? options.expanded) {
1049
- const styledOutput = combinedOutput
1050
- .split("\n")
1051
- .map(line => uiTheme.fg("toolOutput", line))
1052
- .join("\n");
1053
- const lines = [
1054
- styledOutput,
1055
- ...(statusLines.length > 0 ? [uiTheme.fg("dim", "Status"), ...statusLines] : []),
1056
- timeoutLine,
1057
- warningLine,
1058
- ].filter(Boolean) as string[];
1059
- return new Text(lines.join("\n"), 0, 0);
540
+ const hasTruncation = Boolean(truncation);
541
+ for (let i = 0; i < displayLines.length; i++) {
542
+ bodyLines.push(uiTheme.fg("toolOutput", replaceTabs(displayLines[i])));
543
+ }
544
+ if (hasTruncation) {
545
+ bodyLines.push(uiTheme.fg("warning", "output truncated"));
546
+ }
547
+ if (!showAll && skipped > 0) {
548
+ bodyLines.push(formatClickHint(uiTheme));
1060
549
  }
1061
550
 
1062
- const styledOutput = combinedOutput
1063
- .split("\n")
1064
- .map(line => uiTheme.fg("toolOutput", line))
1065
- .join("\n");
1066
- const textContent = `\n${styledOutput}`;
551
+ const lines = bodyLines.length > 0 ? [header, ...bodyLines] : [header];
552
+ return new Text(lines.join("\n"), 0, 0);
553
+ }
554
+ }
1067
555
 
1068
- let cachedWidth: number | undefined;
1069
- let cachedLines: string[] | undefined;
1070
- let cachedSkipped: number | undefined;
1071
- let cachedPreviewLines: number | undefined;
556
+ interface PythonRenderArgs {
557
+ cells?: Array<{ code: string; title?: string }>;
558
+ timeout?: number;
559
+ cwd?: string;
560
+ }
1072
561
 
1073
- return {
1074
- render: (width: number): string[] => {
1075
- // Read mutable state at render time
1076
- const previewLines = options.renderContext?.previewLines ?? PYTHON_DEFAULT_PREVIEW_LINES;
1077
- if (cachedLines === undefined || cachedWidth !== width || cachedPreviewLines !== previewLines) {
1078
- const result = truncateToVisualLines(textContent, previewLines, width);
1079
- cachedLines = result.visualLines;
1080
- cachedSkipped = result.skippedCount;
1081
- cachedWidth = width;
1082
- cachedPreviewLines = previewLines;
1083
- }
1084
- const outputLines: string[] = [];
1085
- if (cachedSkipped && cachedSkipped > 0) {
1086
- outputLines.push("");
1087
- const skippedLine = uiTheme.fg(
1088
- "dim",
1089
- `… (${cachedSkipped} earlier lines, showing ${cachedLines.length} of ${cachedSkipped + cachedLines.length}) (ctrl+o to expand)`,
1090
- );
1091
- outputLines.push(truncateToWidth(skippedLine, width));
1092
- }
1093
- outputLines.push(...cachedLines);
1094
- if (statusLines.length > 0) {
1095
- outputLines.push(truncateToWidth(uiTheme.fg("dim", "Status"), width));
1096
- for (const statusLine of statusLines) {
1097
- outputLines.push(truncateToWidth(statusLine, width));
1098
- }
1099
- }
1100
- if (timeoutLine) {
1101
- outputLines.push(truncateToWidth(timeoutLine, width));
1102
- }
1103
- if (warningLine) {
1104
- outputLines.push(truncateToWidth(warningLine, width));
1105
- }
1106
- return outputLines;
1107
- },
1108
- invalidate: () => {
1109
- cachedWidth = undefined;
1110
- cachedLines = undefined;
1111
- cachedSkipped = undefined;
1112
- cachedPreviewLines = undefined;
1113
- },
1114
- };
1115
- },
1116
- mergeCallAndResult: true,
1117
- inline: true,
1118
- };
562
+ interface PythonRenderContext {
563
+ output?: string;
564
+ expanded?: boolean;
565
+ previewLines?: number;
566
+ timeout?: number;
567
+ }