@oh-my-pi/pi-coding-agent 0.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 (337) hide show
  1. package/CHANGELOG.md +1629 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +541 -0
  6. package/docs/extension-loading.md +1004 -0
  7. package/docs/hooks.md +867 -0
  8. package/docs/rpc.md +1040 -0
  9. package/docs/sdk.md +994 -0
  10. package/docs/session-tree-plan.md +441 -0
  11. package/docs/session.md +240 -0
  12. package/docs/skills.md +290 -0
  13. package/docs/theme.md +670 -0
  14. package/docs/tree.md +197 -0
  15. package/docs/tui.md +341 -0
  16. package/examples/README.md +21 -0
  17. package/examples/custom-tools/README.md +124 -0
  18. package/examples/custom-tools/hello/index.ts +20 -0
  19. package/examples/custom-tools/question/index.ts +84 -0
  20. package/examples/custom-tools/subagent/README.md +172 -0
  21. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +89 -0
  57. package/src/bun-imports.d.ts +16 -0
  58. package/src/capability/context-file.ts +40 -0
  59. package/src/capability/extension.ts +48 -0
  60. package/src/capability/hook.ts +40 -0
  61. package/src/capability/index.ts +616 -0
  62. package/src/capability/instruction.ts +37 -0
  63. package/src/capability/mcp.ts +52 -0
  64. package/src/capability/prompt.ts +35 -0
  65. package/src/capability/rule.ts +56 -0
  66. package/src/capability/settings.ts +35 -0
  67. package/src/capability/skill.ts +49 -0
  68. package/src/capability/slash-command.ts +40 -0
  69. package/src/capability/system-prompt.ts +35 -0
  70. package/src/capability/tool.ts +38 -0
  71. package/src/capability/types.ts +166 -0
  72. package/src/cli/args.ts +259 -0
  73. package/src/cli/file-processor.ts +121 -0
  74. package/src/cli/list-models.ts +104 -0
  75. package/src/cli/plugin-cli.ts +661 -0
  76. package/src/cli/session-picker.ts +41 -0
  77. package/src/cli/update-cli.ts +274 -0
  78. package/src/cli.ts +10 -0
  79. package/src/config.ts +391 -0
  80. package/src/core/agent-session.ts +2178 -0
  81. package/src/core/auth-storage.ts +258 -0
  82. package/src/core/bash-executor.ts +197 -0
  83. package/src/core/compaction/branch-summarization.ts +315 -0
  84. package/src/core/compaction/compaction.ts +664 -0
  85. package/src/core/compaction/index.ts +7 -0
  86. package/src/core/compaction/utils.ts +153 -0
  87. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  88. package/src/core/custom-commands/index.ts +15 -0
  89. package/src/core/custom-commands/loader.ts +226 -0
  90. package/src/core/custom-commands/types.ts +112 -0
  91. package/src/core/custom-tools/index.ts +22 -0
  92. package/src/core/custom-tools/loader.ts +248 -0
  93. package/src/core/custom-tools/types.ts +185 -0
  94. package/src/core/custom-tools/wrapper.ts +29 -0
  95. package/src/core/exec.ts +139 -0
  96. package/src/core/export-html/index.ts +159 -0
  97. package/src/core/export-html/template.css +774 -0
  98. package/src/core/export-html/template.generated.ts +2 -0
  99. package/src/core/export-html/template.html +45 -0
  100. package/src/core/export-html/template.js +1185 -0
  101. package/src/core/export-html/template.macro.ts +24 -0
  102. package/src/core/file-mentions.ts +54 -0
  103. package/src/core/hooks/index.ts +16 -0
  104. package/src/core/hooks/loader.ts +288 -0
  105. package/src/core/hooks/runner.ts +434 -0
  106. package/src/core/hooks/tool-wrapper.ts +98 -0
  107. package/src/core/hooks/types.ts +770 -0
  108. package/src/core/index.ts +53 -0
  109. package/src/core/logger.ts +112 -0
  110. package/src/core/mcp/client.ts +185 -0
  111. package/src/core/mcp/config.ts +248 -0
  112. package/src/core/mcp/index.ts +45 -0
  113. package/src/core/mcp/loader.ts +99 -0
  114. package/src/core/mcp/manager.ts +235 -0
  115. package/src/core/mcp/tool-bridge.ts +156 -0
  116. package/src/core/mcp/transports/http.ts +316 -0
  117. package/src/core/mcp/transports/index.ts +6 -0
  118. package/src/core/mcp/transports/stdio.ts +252 -0
  119. package/src/core/mcp/types.ts +228 -0
  120. package/src/core/messages.ts +211 -0
  121. package/src/core/model-registry.ts +334 -0
  122. package/src/core/model-resolver.ts +494 -0
  123. package/src/core/plugins/doctor.ts +67 -0
  124. package/src/core/plugins/index.ts +38 -0
  125. package/src/core/plugins/installer.ts +189 -0
  126. package/src/core/plugins/loader.ts +339 -0
  127. package/src/core/plugins/manager.ts +672 -0
  128. package/src/core/plugins/parser.ts +105 -0
  129. package/src/core/plugins/paths.ts +37 -0
  130. package/src/core/plugins/types.ts +190 -0
  131. package/src/core/sdk.ts +900 -0
  132. package/src/core/session-manager.ts +1837 -0
  133. package/src/core/settings-manager.ts +860 -0
  134. package/src/core/skills.ts +352 -0
  135. package/src/core/slash-commands.ts +132 -0
  136. package/src/core/system-prompt.ts +442 -0
  137. package/src/core/timings.ts +25 -0
  138. package/src/core/title-generator.ts +110 -0
  139. package/src/core/tools/ask.ts +193 -0
  140. package/src/core/tools/bash-interceptor.ts +120 -0
  141. package/src/core/tools/bash.ts +91 -0
  142. package/src/core/tools/context.ts +32 -0
  143. package/src/core/tools/edit-diff.ts +487 -0
  144. package/src/core/tools/edit.ts +140 -0
  145. package/src/core/tools/exa/company.ts +59 -0
  146. package/src/core/tools/exa/index.ts +63 -0
  147. package/src/core/tools/exa/linkedin.ts +59 -0
  148. package/src/core/tools/exa/mcp-client.ts +368 -0
  149. package/src/core/tools/exa/render.ts +200 -0
  150. package/src/core/tools/exa/researcher.ts +90 -0
  151. package/src/core/tools/exa/search.ts +338 -0
  152. package/src/core/tools/exa/types.ts +167 -0
  153. package/src/core/tools/exa/websets.ts +248 -0
  154. package/src/core/tools/find.ts +244 -0
  155. package/src/core/tools/grep.ts +584 -0
  156. package/src/core/tools/index.ts +283 -0
  157. package/src/core/tools/ls.ts +142 -0
  158. package/src/core/tools/lsp/client.ts +767 -0
  159. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  160. package/src/core/tools/lsp/clients/index.ts +49 -0
  161. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  162. package/src/core/tools/lsp/config.ts +845 -0
  163. package/src/core/tools/lsp/edits.ts +110 -0
  164. package/src/core/tools/lsp/index.ts +1364 -0
  165. package/src/core/tools/lsp/render.ts +560 -0
  166. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  167. package/src/core/tools/lsp/types.ts +495 -0
  168. package/src/core/tools/lsp/utils.ts +526 -0
  169. package/src/core/tools/notebook.ts +182 -0
  170. package/src/core/tools/output.ts +198 -0
  171. package/src/core/tools/path-utils.ts +61 -0
  172. package/src/core/tools/read.ts +507 -0
  173. package/src/core/tools/renderers.ts +820 -0
  174. package/src/core/tools/review.ts +275 -0
  175. package/src/core/tools/rulebook.ts +124 -0
  176. package/src/core/tools/task/agents.ts +158 -0
  177. package/src/core/tools/task/artifacts.ts +114 -0
  178. package/src/core/tools/task/commands.ts +157 -0
  179. package/src/core/tools/task/discovery.ts +217 -0
  180. package/src/core/tools/task/executor.ts +531 -0
  181. package/src/core/tools/task/index.ts +548 -0
  182. package/src/core/tools/task/model-resolver.ts +176 -0
  183. package/src/core/tools/task/parallel.ts +38 -0
  184. package/src/core/tools/task/render.ts +502 -0
  185. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  186. package/src/core/tools/task/types.ts +142 -0
  187. package/src/core/tools/truncate.ts +265 -0
  188. package/src/core/tools/web-fetch.ts +2511 -0
  189. package/src/core/tools/web-search/auth.ts +199 -0
  190. package/src/core/tools/web-search/index.ts +583 -0
  191. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  192. package/src/core/tools/web-search/providers/exa.ts +196 -0
  193. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  194. package/src/core/tools/web-search/render.ts +372 -0
  195. package/src/core/tools/web-search/types.ts +180 -0
  196. package/src/core/tools/write.ts +63 -0
  197. package/src/core/ttsr.ts +211 -0
  198. package/src/core/utils.ts +187 -0
  199. package/src/discovery/agents-md.ts +75 -0
  200. package/src/discovery/builtin.ts +647 -0
  201. package/src/discovery/claude.ts +623 -0
  202. package/src/discovery/cline.ts +104 -0
  203. package/src/discovery/codex.ts +571 -0
  204. package/src/discovery/cursor.ts +266 -0
  205. package/src/discovery/gemini.ts +368 -0
  206. package/src/discovery/github.ts +120 -0
  207. package/src/discovery/helpers.test.ts +127 -0
  208. package/src/discovery/helpers.ts +249 -0
  209. package/src/discovery/index.ts +84 -0
  210. package/src/discovery/mcp-json.ts +127 -0
  211. package/src/discovery/vscode.ts +99 -0
  212. package/src/discovery/windsurf.ts +219 -0
  213. package/src/index.ts +192 -0
  214. package/src/main.ts +507 -0
  215. package/src/migrations.ts +156 -0
  216. package/src/modes/cleanup.ts +23 -0
  217. package/src/modes/index.ts +48 -0
  218. package/src/modes/interactive/components/armin.ts +382 -0
  219. package/src/modes/interactive/components/assistant-message.ts +86 -0
  220. package/src/modes/interactive/components/bash-execution.ts +199 -0
  221. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  222. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  223. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  224. package/src/modes/interactive/components/custom-editor.ts +122 -0
  225. package/src/modes/interactive/components/diff.ts +147 -0
  226. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  227. package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
  228. package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
  229. package/src/modes/interactive/components/extensions/index.ts +9 -0
  230. package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
  231. package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
  232. package/src/modes/interactive/components/extensions/types.ts +191 -0
  233. package/src/modes/interactive/components/hook-editor.ts +117 -0
  234. package/src/modes/interactive/components/hook-input.ts +64 -0
  235. package/src/modes/interactive/components/hook-message.ts +96 -0
  236. package/src/modes/interactive/components/hook-selector.ts +91 -0
  237. package/src/modes/interactive/components/model-selector.ts +560 -0
  238. package/src/modes/interactive/components/oauth-selector.ts +136 -0
  239. package/src/modes/interactive/components/plugin-settings.ts +481 -0
  240. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  241. package/src/modes/interactive/components/session-selector.ts +220 -0
  242. package/src/modes/interactive/components/settings-defs.ts +597 -0
  243. package/src/modes/interactive/components/settings-selector.ts +545 -0
  244. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  245. package/src/modes/interactive/components/status-line/index.ts +4 -0
  246. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  247. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  248. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  249. package/src/modes/interactive/components/status-line/types.ts +81 -0
  250. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  251. package/src/modes/interactive/components/status-line.ts +384 -0
  252. package/src/modes/interactive/components/theme-selector.ts +62 -0
  253. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  254. package/src/modes/interactive/components/tool-execution.ts +946 -0
  255. package/src/modes/interactive/components/tree-selector.ts +877 -0
  256. package/src/modes/interactive/components/ttsr-notification.ts +82 -0
  257. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  258. package/src/modes/interactive/components/user-message.ts +18 -0
  259. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  260. package/src/modes/interactive/components/welcome.ts +228 -0
  261. package/src/modes/interactive/interactive-mode.ts +2669 -0
  262. package/src/modes/interactive/theme/dark.json +102 -0
  263. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  264. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  265. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  266. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  267. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  268. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  269. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  270. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  271. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  272. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  273. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  274. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  275. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  276. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  277. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  278. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  279. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  280. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  281. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  282. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  283. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  284. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  285. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  286. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  287. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  288. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  289. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  290. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  291. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  292. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  293. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  294. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  295. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  296. package/src/modes/interactive/theme/light.json +99 -0
  297. package/src/modes/interactive/theme/theme-schema.json +424 -0
  298. package/src/modes/interactive/theme/theme.ts +2211 -0
  299. package/src/modes/print-mode.ts +163 -0
  300. package/src/modes/rpc/rpc-client.ts +527 -0
  301. package/src/modes/rpc/rpc-mode.ts +494 -0
  302. package/src/modes/rpc/rpc-types.ts +203 -0
  303. package/src/prompts/architect-plan.md +10 -0
  304. package/src/prompts/branch-summary-preamble.md +3 -0
  305. package/src/prompts/branch-summary.md +28 -0
  306. package/src/prompts/browser.md +71 -0
  307. package/src/prompts/compaction-summary.md +34 -0
  308. package/src/prompts/compaction-turn-prefix.md +16 -0
  309. package/src/prompts/compaction-update-summary.md +41 -0
  310. package/src/prompts/explore.md +82 -0
  311. package/src/prompts/implement-with-critic.md +11 -0
  312. package/src/prompts/implement.md +11 -0
  313. package/src/prompts/init.md +30 -0
  314. package/src/prompts/plan.md +54 -0
  315. package/src/prompts/reviewer.md +81 -0
  316. package/src/prompts/summarization-system.md +3 -0
  317. package/src/prompts/system-prompt.md +27 -0
  318. package/src/prompts/task.md +56 -0
  319. package/src/prompts/title-system.md +8 -0
  320. package/src/prompts/tools/ask.md +24 -0
  321. package/src/prompts/tools/bash.md +23 -0
  322. package/src/prompts/tools/edit.md +9 -0
  323. package/src/prompts/tools/find.md +6 -0
  324. package/src/prompts/tools/grep.md +12 -0
  325. package/src/prompts/tools/lsp.md +14 -0
  326. package/src/prompts/tools/output.md +23 -0
  327. package/src/prompts/tools/read.md +25 -0
  328. package/src/prompts/tools/web-fetch.md +8 -0
  329. package/src/prompts/tools/web-search.md +10 -0
  330. package/src/prompts/tools/write.md +10 -0
  331. package/src/utils/changelog.ts +99 -0
  332. package/src/utils/clipboard.ts +265 -0
  333. package/src/utils/fuzzy.ts +108 -0
  334. package/src/utils/mime.ts +30 -0
  335. package/src/utils/shell-snapshot.ts +218 -0
  336. package/src/utils/shell.ts +364 -0
  337. package/src/utils/tools-manager.ts +265 -0
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Component for displaying bash command execution with streaming output.
3
+ */
4
+
5
+ import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
6
+ import stripAnsi from "strip-ansi";
7
+ import {
8
+ DEFAULT_MAX_BYTES,
9
+ DEFAULT_MAX_LINES,
10
+ type TruncationResult,
11
+ truncateTail,
12
+ } from "../../../core/tools/truncate";
13
+ import { getSymbolTheme, theme } from "../theme/theme";
14
+ import { DynamicBorder } from "./dynamic-border";
15
+ import { truncateToVisualLines } from "./visual-truncate";
16
+
17
+ // Preview line limit when not expanded (matches tool execution behavior)
18
+ const PREVIEW_LINES = 20;
19
+
20
+ export class BashExecutionComponent extends Container {
21
+ private command: string;
22
+ private outputLines: string[] = [];
23
+ private status: "running" | "complete" | "cancelled" | "error" = "running";
24
+ private exitCode: number | undefined = undefined;
25
+ private loader: Loader;
26
+ private truncationResult?: TruncationResult;
27
+ private fullOutputPath?: string;
28
+ private expanded = false;
29
+ private contentContainer: Container;
30
+ private ui: TUI;
31
+
32
+ constructor(command: string, ui: TUI) {
33
+ super();
34
+ this.command = command;
35
+ this.ui = ui;
36
+
37
+ const borderColor = (str: string) => theme.fg("bashMode", str);
38
+
39
+ // Add spacer
40
+ this.addChild(new Spacer(1));
41
+
42
+ // Top border
43
+ this.addChild(new DynamicBorder(borderColor));
44
+
45
+ // Content container (holds dynamic content between borders)
46
+ this.contentContainer = new Container();
47
+ this.addChild(this.contentContainer);
48
+
49
+ // Command header
50
+ const header = new Text(theme.fg("bashMode", theme.bold(`$ ${command}`)), 1, 0);
51
+ this.contentContainer.addChild(header);
52
+
53
+ // Loader
54
+ this.loader = new Loader(
55
+ ui,
56
+ (spinner) => theme.fg("bashMode", spinner),
57
+ (text) => theme.fg("muted", text),
58
+ `Running${theme.format.ellipsis} (esc to cancel)`,
59
+ getSymbolTheme().spinnerFrames,
60
+ );
61
+ this.contentContainer.addChild(this.loader);
62
+
63
+ // Bottom border
64
+ this.addChild(new DynamicBorder(borderColor));
65
+ }
66
+
67
+ /**
68
+ * Set whether the output is expanded (shows full output) or collapsed (preview only).
69
+ */
70
+ setExpanded(expanded: boolean): void {
71
+ this.expanded = expanded;
72
+ this.updateDisplay();
73
+ }
74
+
75
+ appendOutput(chunk: string): void {
76
+ // Strip ANSI codes and normalize line endings
77
+ // Note: binary data is already sanitized in tui-renderer.ts executeBashCommand
78
+ const clean = stripAnsi(chunk).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
79
+
80
+ // Append to output lines
81
+ const newLines = clean.split("\n");
82
+ if (this.outputLines.length > 0 && newLines.length > 0) {
83
+ // Append first chunk to last line (incomplete line continuation)
84
+ this.outputLines[this.outputLines.length - 1] += newLines[0];
85
+ this.outputLines.push(...newLines.slice(1));
86
+ } else {
87
+ this.outputLines.push(...newLines);
88
+ }
89
+
90
+ this.updateDisplay();
91
+ }
92
+
93
+ setComplete(
94
+ exitCode: number | undefined,
95
+ cancelled: boolean,
96
+ truncationResult?: TruncationResult,
97
+ fullOutputPath?: string,
98
+ ): void {
99
+ this.exitCode = exitCode;
100
+ this.status = cancelled
101
+ ? "cancelled"
102
+ : exitCode !== 0 && exitCode !== undefined && exitCode !== null
103
+ ? "error"
104
+ : "complete";
105
+ this.truncationResult = truncationResult;
106
+ this.fullOutputPath = fullOutputPath;
107
+
108
+ // Stop loader
109
+ this.loader.stop();
110
+
111
+ this.updateDisplay();
112
+ }
113
+
114
+ private updateDisplay(): void {
115
+ // Apply truncation for LLM context limits (same limits as bash tool)
116
+ const fullOutput = this.outputLines.join("\n");
117
+ const contextTruncation = truncateTail(fullOutput, {
118
+ maxLines: DEFAULT_MAX_LINES,
119
+ maxBytes: DEFAULT_MAX_BYTES,
120
+ });
121
+
122
+ // Get the lines to potentially display (after context truncation)
123
+ const availableLines = contextTruncation.content ? contextTruncation.content.split("\n") : [];
124
+
125
+ // Apply preview truncation based on expanded state
126
+ const previewLogicalLines = availableLines.slice(-PREVIEW_LINES);
127
+ const hiddenLineCount = availableLines.length - previewLogicalLines.length;
128
+
129
+ // Rebuild content container
130
+ this.contentContainer.clear();
131
+
132
+ // Command header
133
+ const header = new Text(theme.fg("bashMode", theme.bold(`$ ${this.command}`)), 1, 0);
134
+ this.contentContainer.addChild(header);
135
+
136
+ // Output
137
+ if (availableLines.length > 0) {
138
+ if (this.expanded) {
139
+ // Show all lines
140
+ const displayText = availableLines.map((line) => theme.fg("muted", line)).join("\n");
141
+ this.contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
142
+ } else {
143
+ // Use shared visual truncation utility
144
+ const styledOutput = previewLogicalLines.map((line) => theme.fg("muted", line)).join("\n");
145
+ const { visualLines } = truncateToVisualLines(
146
+ `\n${styledOutput}`,
147
+ PREVIEW_LINES,
148
+ this.ui.terminal.columns,
149
+ 1, // padding
150
+ );
151
+ this.contentContainer.addChild({ render: () => visualLines, invalidate: () => {} });
152
+ }
153
+ }
154
+
155
+ // Loader or status
156
+ if (this.status === "running") {
157
+ this.contentContainer.addChild(this.loader);
158
+ } else {
159
+ const statusParts: string[] = [];
160
+
161
+ // Show how many lines are hidden (collapsed preview)
162
+ if (hiddenLineCount > 0) {
163
+ statusParts.push(
164
+ theme.fg("dim", `${theme.format.ellipsis} ${hiddenLineCount} more lines (ctrl+o to expand)`),
165
+ );
166
+ }
167
+
168
+ if (this.status === "cancelled") {
169
+ statusParts.push(theme.fg("warning", "(cancelled)"));
170
+ } else if (this.status === "error") {
171
+ statusParts.push(theme.fg("error", `(exit ${this.exitCode})`));
172
+ }
173
+
174
+ // Add truncation warning (context truncation, not preview truncation)
175
+ const wasTruncated = this.truncationResult?.truncated || contextTruncation.truncated;
176
+ if (wasTruncated && this.fullOutputPath) {
177
+ statusParts.push(theme.fg("warning", `Output truncated. Full output: ${this.fullOutputPath}`));
178
+ }
179
+
180
+ if (statusParts.length > 0) {
181
+ this.contentContainer.addChild(new Text(`\n${statusParts.join("\n")}`, 1, 0));
182
+ }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Get the raw output for creating BashExecutionMessage.
188
+ */
189
+ getOutput(): string {
190
+ return this.outputLines.join("\n");
191
+ }
192
+
193
+ /**
194
+ * Get the command that was executed.
195
+ */
196
+ getCommand(): string {
197
+ return this.command;
198
+ }
199
+ }
@@ -0,0 +1,41 @@
1
+ import { CancellableLoader, Container, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
2
+ import type { Theme } from "../theme/theme";
3
+ import { DynamicBorder } from "./dynamic-border";
4
+
5
+ /** Loader wrapped with borders for hook UI */
6
+ export class BorderedLoader extends Container {
7
+ private loader: CancellableLoader;
8
+
9
+ constructor(tui: TUI, theme: Theme, message: string) {
10
+ super();
11
+ const borderColor = (s: string) => theme.fg("border", s);
12
+ this.addChild(new DynamicBorder(borderColor));
13
+ this.loader = new CancellableLoader(
14
+ tui,
15
+ (s) => theme.fg("accent", s),
16
+ (s) => theme.fg("muted", s),
17
+ message,
18
+ );
19
+ this.addChild(this.loader);
20
+ this.addChild(new Spacer(1));
21
+ this.addChild(new Text(theme.fg("muted", "esc cancel"), 1, 0));
22
+ this.addChild(new Spacer(1));
23
+ this.addChild(new DynamicBorder(borderColor));
24
+ }
25
+
26
+ get signal(): AbortSignal {
27
+ return this.loader.signal;
28
+ }
29
+
30
+ set onAbort(fn: (() => void) | undefined) {
31
+ this.loader.onAbort = fn;
32
+ }
33
+
34
+ handleInput(data: string): void {
35
+ this.loader.handleInput(data);
36
+ }
37
+
38
+ dispose(): void {
39
+ this.loader.dispose();
40
+ }
41
+ }
@@ -0,0 +1,42 @@
1
+ import { Box, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
2
+ import type { BranchSummaryMessage } from "../../../core/messages";
3
+ import { getMarkdownTheme, theme } from "../theme/theme";
4
+
5
+ /**
6
+ * Component that renders a branch summary message with collapsed/expanded state.
7
+ * Uses same background color as hook messages for visual consistency.
8
+ */
9
+ export class BranchSummaryMessageComponent extends Box {
10
+ private expanded = false;
11
+ private message: BranchSummaryMessage;
12
+
13
+ constructor(message: BranchSummaryMessage) {
14
+ super(1, 1, (t) => theme.bg("customMessageBg", t));
15
+ this.message = message;
16
+ this.updateDisplay();
17
+ }
18
+
19
+ setExpanded(expanded: boolean): void {
20
+ this.expanded = expanded;
21
+ this.updateDisplay();
22
+ }
23
+
24
+ private updateDisplay(): void {
25
+ this.clear();
26
+
27
+ const label = theme.fg("customMessageLabel", theme.bold("[branch]"));
28
+ this.addChild(new Text(label, 0, 0));
29
+ this.addChild(new Spacer(1));
30
+
31
+ if (this.expanded) {
32
+ const header = "**Branch Summary**\n\n";
33
+ this.addChild(
34
+ new Markdown(header + this.message.summary, 0, 0, getMarkdownTheme(), {
35
+ color: (text: string) => theme.fg("customMessageText", text),
36
+ }),
37
+ );
38
+ } else {
39
+ this.addChild(new Text(theme.fg("customMessageText", "Branch summary (ctrl+o to expand)"), 0, 0));
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,45 @@
1
+ import { Box, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
2
+ import type { CompactionSummaryMessage } from "../../../core/messages";
3
+ import { getMarkdownTheme, theme } from "../theme/theme";
4
+
5
+ /**
6
+ * Component that renders a compaction message with collapsed/expanded state.
7
+ * Uses same background color as hook messages for visual consistency.
8
+ */
9
+ export class CompactionSummaryMessageComponent extends Box {
10
+ private expanded = false;
11
+ private message: CompactionSummaryMessage;
12
+
13
+ constructor(message: CompactionSummaryMessage) {
14
+ super(1, 1, (t) => theme.bg("customMessageBg", t));
15
+ this.message = message;
16
+ this.updateDisplay();
17
+ }
18
+
19
+ setExpanded(expanded: boolean): void {
20
+ this.expanded = expanded;
21
+ this.updateDisplay();
22
+ }
23
+
24
+ private updateDisplay(): void {
25
+ this.clear();
26
+
27
+ const tokenStr = this.message.tokensBefore.toLocaleString();
28
+ const label = theme.fg("customMessageLabel", theme.bold("[compaction]"));
29
+ this.addChild(new Text(label, 0, 0));
30
+ this.addChild(new Spacer(1));
31
+
32
+ if (this.expanded) {
33
+ const header = `**Compacted from ${tokenStr} tokens**\n\n`;
34
+ this.addChild(
35
+ new Markdown(header + this.message.summary, 0, 0, getMarkdownTheme(), {
36
+ color: (text: string) => theme.fg("customMessageText", text),
37
+ }),
38
+ );
39
+ } else {
40
+ this.addChild(
41
+ new Text(theme.fg("customMessageText", `Compacted from ${tokenStr} tokens (ctrl+o to expand)`), 0, 0),
42
+ );
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,122 @@
1
+ import {
2
+ Editor,
3
+ isCtrlC,
4
+ isCtrlD,
5
+ isCtrlG,
6
+ isCtrlL,
7
+ isCtrlO,
8
+ isCtrlP,
9
+ isCtrlT,
10
+ isCtrlV,
11
+ isCtrlZ,
12
+ isEscape,
13
+ isShiftCtrlP,
14
+ isShiftTab,
15
+ } from "@oh-my-pi/pi-tui";
16
+
17
+ /**
18
+ * Custom editor that handles Escape and Ctrl+C keys for coding-agent
19
+ */
20
+ export class CustomEditor extends Editor {
21
+ public onEscape?: () => void;
22
+ public onCtrlC?: () => void;
23
+ public onCtrlD?: () => void;
24
+ public onShiftTab?: () => void;
25
+ public onCtrlP?: () => void;
26
+ public onShiftCtrlP?: () => void;
27
+ public onCtrlL?: () => void;
28
+ public onCtrlO?: () => void;
29
+ public onCtrlT?: () => void;
30
+ public onCtrlG?: () => void;
31
+ public onCtrlZ?: () => void;
32
+ public onQuestionMark?: () => void;
33
+ /** Called when Ctrl+V is pressed. Returns true if handled (image found), false to fall through to text paste. */
34
+ public onCtrlV?: () => Promise<boolean>;
35
+
36
+ handleInput(data: string): void {
37
+ // Intercept Ctrl+V for image paste (async - fires and handles result)
38
+ if (isCtrlV(data) && this.onCtrlV) {
39
+ void this.onCtrlV();
40
+ return;
41
+ }
42
+
43
+ // Intercept Ctrl+G for external editor
44
+ if (isCtrlG(data) && this.onCtrlG) {
45
+ this.onCtrlG();
46
+ return;
47
+ }
48
+
49
+ // Intercept Ctrl+Z for suspend
50
+ if (isCtrlZ(data) && this.onCtrlZ) {
51
+ this.onCtrlZ();
52
+ return;
53
+ }
54
+
55
+ // Intercept Ctrl+T for thinking block visibility toggle
56
+ if (isCtrlT(data) && this.onCtrlT) {
57
+ this.onCtrlT();
58
+ return;
59
+ }
60
+
61
+ // Intercept Ctrl+L for model selector
62
+ if (isCtrlL(data) && this.onCtrlL) {
63
+ this.onCtrlL();
64
+ return;
65
+ }
66
+
67
+ // Intercept Ctrl+O for tool output expansion
68
+ if (isCtrlO(data) && this.onCtrlO) {
69
+ this.onCtrlO();
70
+ return;
71
+ }
72
+
73
+ // Intercept Shift+Ctrl+P for backward model cycling (check before Ctrl+P)
74
+ if (isShiftCtrlP(data) && this.onShiftCtrlP) {
75
+ this.onShiftCtrlP();
76
+ return;
77
+ }
78
+
79
+ // Intercept Ctrl+P for model cycling
80
+ if (isCtrlP(data) && this.onCtrlP) {
81
+ this.onCtrlP();
82
+ return;
83
+ }
84
+
85
+ // Intercept Shift+Tab for thinking level cycling
86
+ if (isShiftTab(data) && this.onShiftTab) {
87
+ this.onShiftTab();
88
+ return;
89
+ }
90
+
91
+ // Intercept Escape key - but only if autocomplete is NOT active
92
+ // (let parent handle escape for autocomplete cancellation)
93
+ if (isEscape(data) && this.onEscape && !this.isShowingAutocomplete()) {
94
+ this.onEscape();
95
+ return;
96
+ }
97
+
98
+ // Intercept Ctrl+C
99
+ if (isCtrlC(data) && this.onCtrlC) {
100
+ this.onCtrlC();
101
+ return;
102
+ }
103
+
104
+ // Intercept Ctrl+D (only when editor is empty)
105
+ if (isCtrlD(data)) {
106
+ if (this.getText().length === 0 && this.onCtrlD) {
107
+ this.onCtrlD();
108
+ }
109
+ // Always consume Ctrl+D (don't pass to parent)
110
+ return;
111
+ }
112
+
113
+ // Intercept ? when editor is empty to show hotkeys
114
+ if (data === "?" && this.getText().length === 0 && this.onQuestionMark) {
115
+ this.onQuestionMark();
116
+ return;
117
+ }
118
+
119
+ // Pass to parent for normal handling
120
+ super.handleInput(data);
121
+ }
122
+ }
@@ -0,0 +1,147 @@
1
+ import * as Diff from "diff";
2
+ import { theme } from "../theme/theme";
3
+
4
+ /**
5
+ * Parse diff line to extract prefix, line number, and content.
6
+ * Format: "+123 content" or "-123 content" or " 123 content" or " ..."
7
+ */
8
+ function parseDiffLine(line: string): { prefix: string; lineNum: string; content: string } | null {
9
+ const match = line.match(/^([+-\s])(\s*\d*)\s(.*)$/);
10
+ if (!match) return null;
11
+ return { prefix: match[1], lineNum: match[2], content: match[3] };
12
+ }
13
+
14
+ /**
15
+ * Replace tabs with spaces for consistent rendering.
16
+ */
17
+ function replaceTabs(text: string): string {
18
+ return text.replace(/\t/g, " ");
19
+ }
20
+
21
+ /**
22
+ * Compute word-level diff and render with inverse on changed parts.
23
+ * Uses diffWords which groups whitespace with adjacent words for cleaner highlighting.
24
+ * Strips leading whitespace from inverse to avoid highlighting indentation.
25
+ */
26
+ function renderIntraLineDiff(oldContent: string, newContent: string): { removedLine: string; addedLine: string } {
27
+ const wordDiff = Diff.diffWords(oldContent, newContent);
28
+
29
+ let removedLine = "";
30
+ let addedLine = "";
31
+ let isFirstRemoved = true;
32
+ let isFirstAdded = true;
33
+
34
+ for (const part of wordDiff) {
35
+ if (part.removed) {
36
+ let value = part.value;
37
+ // Strip leading whitespace from the first removed part
38
+ if (isFirstRemoved) {
39
+ const leadingWs = value.match(/^(\s*)/)?.[1] || "";
40
+ value = value.slice(leadingWs.length);
41
+ removedLine += leadingWs;
42
+ isFirstRemoved = false;
43
+ }
44
+ if (value) {
45
+ removedLine += theme.inverse(value);
46
+ }
47
+ } else if (part.added) {
48
+ let value = part.value;
49
+ // Strip leading whitespace from the first added part
50
+ if (isFirstAdded) {
51
+ const leadingWs = value.match(/^(\s*)/)?.[1] || "";
52
+ value = value.slice(leadingWs.length);
53
+ addedLine += leadingWs;
54
+ isFirstAdded = false;
55
+ }
56
+ if (value) {
57
+ addedLine += theme.inverse(value);
58
+ }
59
+ } else {
60
+ removedLine += part.value;
61
+ addedLine += part.value;
62
+ }
63
+ }
64
+
65
+ return { removedLine, addedLine };
66
+ }
67
+
68
+ export interface RenderDiffOptions {
69
+ /** File path (unused, kept for API compatibility) */
70
+ filePath?: string;
71
+ }
72
+
73
+ /**
74
+ * Render a diff string with colored lines and intra-line change highlighting.
75
+ * - Context lines: dim/gray
76
+ * - Removed lines: red, with inverse on changed tokens
77
+ * - Added lines: green, with inverse on changed tokens
78
+ */
79
+ export function renderDiff(diffText: string, _options: RenderDiffOptions = {}): string {
80
+ const lines = diffText.split("\n");
81
+ const result: string[] = [];
82
+
83
+ let i = 0;
84
+ while (i < lines.length) {
85
+ const line = lines[i];
86
+ const parsed = parseDiffLine(line);
87
+
88
+ if (!parsed) {
89
+ result.push(theme.fg("toolDiffContext", line));
90
+ i++;
91
+ continue;
92
+ }
93
+
94
+ if (parsed.prefix === "-") {
95
+ // Collect consecutive removed lines
96
+ const removedLines: { lineNum: string; content: string }[] = [];
97
+ while (i < lines.length) {
98
+ const p = parseDiffLine(lines[i]);
99
+ if (!p || p.prefix !== "-") break;
100
+ removedLines.push({ lineNum: p.lineNum, content: p.content });
101
+ i++;
102
+ }
103
+
104
+ // Collect consecutive added lines
105
+ const addedLines: { lineNum: string; content: string }[] = [];
106
+ while (i < lines.length) {
107
+ const p = parseDiffLine(lines[i]);
108
+ if (!p || p.prefix !== "+") break;
109
+ addedLines.push({ lineNum: p.lineNum, content: p.content });
110
+ i++;
111
+ }
112
+
113
+ // Only do intra-line diffing when there's exactly one removed and one added line
114
+ // (indicating a single line modification). Otherwise, show lines as-is.
115
+ if (removedLines.length === 1 && addedLines.length === 1) {
116
+ const removed = removedLines[0];
117
+ const added = addedLines[0];
118
+
119
+ const { removedLine, addedLine } = renderIntraLineDiff(
120
+ replaceTabs(removed.content),
121
+ replaceTabs(added.content),
122
+ );
123
+
124
+ result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${removedLine}`));
125
+ result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${addedLine}`));
126
+ } else {
127
+ // Show all removed lines first, then all added lines
128
+ for (const removed of removedLines) {
129
+ result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`));
130
+ }
131
+ for (const added of addedLines) {
132
+ result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`));
133
+ }
134
+ }
135
+ } else if (parsed.prefix === "+") {
136
+ // Standalone added line
137
+ result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`));
138
+ i++;
139
+ } else {
140
+ // Context line
141
+ result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`));
142
+ i++;
143
+ }
144
+ }
145
+
146
+ return result.join("\n");
147
+ }
@@ -0,0 +1,25 @@
1
+ import type { Component } from "@oh-my-pi/pi-tui";
2
+ import { theme } from "../theme/theme";
3
+
4
+ /**
5
+ * Dynamic border component that adjusts to viewport width.
6
+ *
7
+ * Note: When used from hooks loaded via jiti, the global `theme` may be undefined
8
+ * because jiti creates a separate module cache. Always pass an explicit color
9
+ * function when using DynamicBorder in components exported for hook use.
10
+ */
11
+ export class DynamicBorder implements Component {
12
+ private color: (str: string) => string;
13
+
14
+ constructor(color: (str: string) => string = (str) => theme.fg("border", str)) {
15
+ this.color = color;
16
+ }
17
+
18
+ invalidate(): void {
19
+ // No cached state to invalidate currently
20
+ }
21
+
22
+ render(width: number): string[] {
23
+ return [this.color(theme.boxSharp.horizontal.repeat(Math.max(1, width)))];
24
+ }
25
+ }