@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,48 @@
1
+ /**
2
+ * Run modes for the coding agent.
3
+ */
4
+
5
+ import { emergencyTerminalRestore } from "@oh-my-pi/pi-tui";
6
+ import { runAsyncCleanup } from "./cleanup";
7
+
8
+ /**
9
+ * Install handlers that restore terminal state on crash/signal.
10
+ * Must be called before entering interactive mode.
11
+ */
12
+ export function installTerminalCrashHandlers(): void {
13
+ const cleanup = () => {
14
+ emergencyTerminalRestore();
15
+ };
16
+
17
+ // Signals - run async cleanup before exit
18
+ process.on("SIGINT", () => {
19
+ cleanup();
20
+ void runAsyncCleanup().finally(() => process.exit(128 + 2));
21
+ });
22
+ process.on("SIGTERM", () => {
23
+ cleanup();
24
+ void runAsyncCleanup().finally(() => process.exit(128 + 15));
25
+ });
26
+ process.on("SIGHUP", () => {
27
+ cleanup();
28
+ void runAsyncCleanup().finally(() => process.exit(128 + 1));
29
+ });
30
+
31
+ // Crashes - exit immediately (async cleanup may not be safe in corrupted state)
32
+ process.on("uncaughtException", (err) => {
33
+ cleanup();
34
+ console.error("Uncaught exception:", err);
35
+ process.exit(1);
36
+ });
37
+ process.on("unhandledRejection", (reason) => {
38
+ cleanup();
39
+ console.error("Unhandled rejection:", reason);
40
+ process.exit(1);
41
+ });
42
+ }
43
+
44
+ export { InteractiveMode } from "./interactive/interactive-mode";
45
+ export { runPrintMode } from "./print-mode";
46
+ export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client";
47
+ export { runRpcMode } from "./rpc/rpc-mode";
48
+ export type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc/rpc-types";
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Armin says hi! A fun easter egg with animated XBM art.
3
+ */
4
+
5
+ import type { Component, TUI } from "@oh-my-pi/pi-tui";
6
+ import { theme } from "../theme/theme";
7
+
8
+ // XBM image: 31x36 pixels, LSB first, 1=background, 0=foreground
9
+ const WIDTH = 31;
10
+ const HEIGHT = 36;
11
+ const BITS = [
12
+ 0xff, 0xff, 0xff, 0x7f, 0xff, 0xf0, 0xff, 0x7f, 0xff, 0xed, 0xff, 0x7f, 0xff, 0xdb, 0xff, 0x7f, 0xff, 0xb7, 0xff,
13
+ 0x7f, 0xff, 0x77, 0xfe, 0x7f, 0x3f, 0xf8, 0xfe, 0x7f, 0xdf, 0xff, 0xfe, 0x7f, 0xdf, 0x3f, 0xfc, 0x7f, 0x9f, 0xc3,
14
+ 0xfb, 0x7f, 0x6f, 0xfc, 0xf4, 0x7f, 0xf7, 0x0f, 0xf7, 0x7f, 0xf7, 0xff, 0xf7, 0x7f, 0xf7, 0xff, 0xe3, 0x7f, 0xf7,
15
+ 0x07, 0xe8, 0x7f, 0xef, 0xf8, 0x67, 0x70, 0x0f, 0xff, 0xbb, 0x6f, 0xf1, 0x00, 0xd0, 0x5b, 0xfd, 0x3f, 0xec, 0x53,
16
+ 0xc1, 0xff, 0xef, 0x57, 0x9f, 0xfd, 0xee, 0x5f, 0x9f, 0xfc, 0xae, 0x5f, 0x1f, 0x78, 0xac, 0x5f, 0x3f, 0x00, 0x50,
17
+ 0x6c, 0x7f, 0x00, 0xdc, 0x77, 0xff, 0xc0, 0x3f, 0x78, 0xff, 0x01, 0xf8, 0x7f, 0xff, 0x03, 0x9c, 0x78, 0xff, 0x07,
18
+ 0x8c, 0x7c, 0xff, 0x0f, 0xce, 0x78, 0xff, 0xff, 0xcf, 0x7f, 0xff, 0xff, 0xcf, 0x78, 0xff, 0xff, 0xdf, 0x78, 0xff,
19
+ 0xff, 0xdf, 0x7d, 0xff, 0xff, 0x3f, 0x7e, 0xff, 0xff, 0xff, 0x7f,
20
+ ];
21
+
22
+ const BYTES_PER_ROW = Math.ceil(WIDTH / 8);
23
+ const DISPLAY_HEIGHT = Math.ceil(HEIGHT / 2); // Half-block rendering
24
+
25
+ type Effect = "typewriter" | "scanline" | "rain" | "fade" | "crt" | "glitch" | "dissolve";
26
+
27
+ const EFFECTS: Effect[] = ["typewriter", "scanline", "rain", "fade", "crt", "glitch", "dissolve"];
28
+
29
+ // Get pixel at (x, y): true = foreground, false = background
30
+ function getPixel(x: number, y: number): boolean {
31
+ if (y >= HEIGHT) return false;
32
+ const byteIndex = y * BYTES_PER_ROW + Math.floor(x / 8);
33
+ const bitIndex = x % 8;
34
+ return ((BITS[byteIndex] >> bitIndex) & 1) === 0;
35
+ }
36
+
37
+ // Get the character for a cell (2 vertical pixels packed)
38
+ function getChar(x: number, row: number): string {
39
+ const upper = getPixel(x, row * 2);
40
+ const lower = getPixel(x, row * 2 + 1);
41
+ if (upper && lower) return "█";
42
+ if (upper) return "▀";
43
+ if (lower) return "▄";
44
+ return " ";
45
+ }
46
+
47
+ // Build the final image grid
48
+ function buildFinalGrid(): string[][] {
49
+ const grid: string[][] = [];
50
+ for (let row = 0; row < DISPLAY_HEIGHT; row++) {
51
+ const line: string[] = [];
52
+ for (let x = 0; x < WIDTH; x++) {
53
+ line.push(getChar(x, row));
54
+ }
55
+ grid.push(line);
56
+ }
57
+ return grid;
58
+ }
59
+
60
+ export class ArminComponent implements Component {
61
+ private ui: TUI;
62
+ private interval: ReturnType<typeof setInterval> | null = null;
63
+ private effect: Effect;
64
+ private finalGrid: string[][];
65
+ private currentGrid: string[][];
66
+ private effectState: Record<string, unknown> = {};
67
+ private cachedLines: string[] = [];
68
+ private cachedWidth = 0;
69
+ private gridVersion = 0;
70
+ private cachedVersion = -1;
71
+
72
+ constructor(ui: TUI) {
73
+ this.ui = ui;
74
+ this.effect = EFFECTS[Math.floor(Math.random() * EFFECTS.length)];
75
+ this.finalGrid = buildFinalGrid();
76
+ this.currentGrid = this.createEmptyGrid();
77
+
78
+ this.initEffect();
79
+ this.startAnimation();
80
+ }
81
+
82
+ invalidate(): void {
83
+ this.cachedWidth = 0;
84
+ }
85
+
86
+ render(width: number): string[] {
87
+ if (width === this.cachedWidth && this.cachedVersion === this.gridVersion) {
88
+ return this.cachedLines;
89
+ }
90
+
91
+ const padding = 1;
92
+ const availableWidth = width - padding;
93
+
94
+ this.cachedLines = this.currentGrid.map((row) => {
95
+ // Clip row to available width before applying color
96
+ const clipped = row.slice(0, availableWidth).join("");
97
+ const padRight = Math.max(0, width - padding - clipped.length);
98
+ return ` ${theme.fg("accent", clipped)}${" ".repeat(padRight)}`;
99
+ });
100
+
101
+ // Add "ARMIN SAYS HI" at the end
102
+ const message = "ARMIN SAYS HI";
103
+ const msgPadRight = Math.max(0, width - padding - message.length);
104
+ this.cachedLines.push(` ${theme.fg("accent", message)}${" ".repeat(msgPadRight)}`);
105
+
106
+ this.cachedWidth = width;
107
+ this.cachedVersion = this.gridVersion;
108
+
109
+ return this.cachedLines;
110
+ }
111
+
112
+ private createEmptyGrid(): string[][] {
113
+ return Array.from({ length: DISPLAY_HEIGHT }, () => Array(WIDTH).fill(" "));
114
+ }
115
+
116
+ private initEffect(): void {
117
+ switch (this.effect) {
118
+ case "typewriter":
119
+ this.effectState = { pos: 0 };
120
+ break;
121
+ case "scanline":
122
+ this.effectState = { row: 0 };
123
+ break;
124
+ case "rain":
125
+ // Track falling position for each column
126
+ this.effectState = {
127
+ drops: Array.from({ length: WIDTH }, () => ({
128
+ y: -Math.floor(Math.random() * DISPLAY_HEIGHT * 2),
129
+ settled: 0,
130
+ })),
131
+ };
132
+ break;
133
+ case "fade": {
134
+ // Shuffle all pixel positions
135
+ const positions: [number, number][] = [];
136
+ for (let row = 0; row < DISPLAY_HEIGHT; row++) {
137
+ for (let x = 0; x < WIDTH; x++) {
138
+ positions.push([row, x]);
139
+ }
140
+ }
141
+ // Fisher-Yates shuffle
142
+ for (let i = positions.length - 1; i > 0; i--) {
143
+ const j = Math.floor(Math.random() * (i + 1));
144
+ [positions[i], positions[j]] = [positions[j], positions[i]];
145
+ }
146
+ this.effectState = { positions, idx: 0 };
147
+ break;
148
+ }
149
+ case "crt":
150
+ this.effectState = { expansion: 0 };
151
+ break;
152
+ case "glitch":
153
+ this.effectState = { phase: 0, glitchFrames: 8 };
154
+ break;
155
+ case "dissolve": {
156
+ // Start with random noise
157
+ this.currentGrid = Array.from({ length: DISPLAY_HEIGHT }, () =>
158
+ Array.from({ length: WIDTH }, () => {
159
+ const chars = [" ", "░", "▒", "▓", "█", "▀", "▄"];
160
+ return chars[Math.floor(Math.random() * chars.length)];
161
+ }),
162
+ );
163
+ // Shuffle positions for gradual resolve
164
+ const dissolvePositions: [number, number][] = [];
165
+ for (let row = 0; row < DISPLAY_HEIGHT; row++) {
166
+ for (let x = 0; x < WIDTH; x++) {
167
+ dissolvePositions.push([row, x]);
168
+ }
169
+ }
170
+ for (let i = dissolvePositions.length - 1; i > 0; i--) {
171
+ const j = Math.floor(Math.random() * (i + 1));
172
+ [dissolvePositions[i], dissolvePositions[j]] = [dissolvePositions[j], dissolvePositions[i]];
173
+ }
174
+ this.effectState = { positions: dissolvePositions, idx: 0 };
175
+ break;
176
+ }
177
+ }
178
+ }
179
+
180
+ private startAnimation(): void {
181
+ const fps = this.effect === "glitch" ? 60 : 30;
182
+ this.interval = setInterval(() => {
183
+ const done = this.tickEffect();
184
+ this.updateDisplay();
185
+ this.ui.requestRender();
186
+ if (done) {
187
+ this.stopAnimation();
188
+ }
189
+ }, 1000 / fps);
190
+ }
191
+
192
+ private stopAnimation(): void {
193
+ if (this.interval) {
194
+ clearInterval(this.interval);
195
+ this.interval = null;
196
+ }
197
+ }
198
+
199
+ private tickEffect(): boolean {
200
+ switch (this.effect) {
201
+ case "typewriter":
202
+ return this.tickTypewriter();
203
+ case "scanline":
204
+ return this.tickScanline();
205
+ case "rain":
206
+ return this.tickRain();
207
+ case "fade":
208
+ return this.tickFade();
209
+ case "crt":
210
+ return this.tickCrt();
211
+ case "glitch":
212
+ return this.tickGlitch();
213
+ case "dissolve":
214
+ return this.tickDissolve();
215
+ default:
216
+ return true;
217
+ }
218
+ }
219
+
220
+ private tickTypewriter(): boolean {
221
+ const state = this.effectState as { pos: number };
222
+ const pixelsPerFrame = 3;
223
+
224
+ for (let i = 0; i < pixelsPerFrame; i++) {
225
+ const row = Math.floor(state.pos / WIDTH);
226
+ const x = state.pos % WIDTH;
227
+ if (row >= DISPLAY_HEIGHT) return true;
228
+ this.currentGrid[row][x] = this.finalGrid[row][x];
229
+ state.pos++;
230
+ }
231
+ return false;
232
+ }
233
+
234
+ private tickScanline(): boolean {
235
+ const state = this.effectState as { row: number };
236
+ if (state.row >= DISPLAY_HEIGHT) return true;
237
+
238
+ // Copy row
239
+ for (let x = 0; x < WIDTH; x++) {
240
+ this.currentGrid[state.row][x] = this.finalGrid[state.row][x];
241
+ }
242
+ state.row++;
243
+ return false;
244
+ }
245
+
246
+ private tickRain(): boolean {
247
+ const state = this.effectState as {
248
+ drops: { y: number; settled: number }[];
249
+ };
250
+
251
+ let allSettled = true;
252
+ this.currentGrid = this.createEmptyGrid();
253
+
254
+ for (let x = 0; x < WIDTH; x++) {
255
+ const drop = state.drops[x];
256
+
257
+ // Draw settled pixels
258
+ for (let row = DISPLAY_HEIGHT - 1; row >= DISPLAY_HEIGHT - drop.settled; row--) {
259
+ if (row >= 0) {
260
+ this.currentGrid[row][x] = this.finalGrid[row][x];
261
+ }
262
+ }
263
+
264
+ // Check if this column is done
265
+ if (drop.settled >= DISPLAY_HEIGHT) continue;
266
+
267
+ allSettled = false;
268
+
269
+ // Find the target row for this column (lowest non-space pixel)
270
+ let targetRow = -1;
271
+ for (let row = DISPLAY_HEIGHT - 1 - drop.settled; row >= 0; row--) {
272
+ if (this.finalGrid[row][x] !== " ") {
273
+ targetRow = row;
274
+ break;
275
+ }
276
+ }
277
+
278
+ // Move drop down
279
+ drop.y++;
280
+
281
+ // Draw falling drop
282
+ if (drop.y >= 0 && drop.y < DISPLAY_HEIGHT) {
283
+ if (targetRow >= 0 && drop.y >= targetRow) {
284
+ // Settle
285
+ drop.settled = DISPLAY_HEIGHT - targetRow;
286
+ drop.y = -Math.floor(Math.random() * 5) - 1;
287
+ } else {
288
+ // Still falling
289
+ this.currentGrid[drop.y][x] = "▓";
290
+ }
291
+ }
292
+ }
293
+
294
+ return allSettled;
295
+ }
296
+
297
+ private tickFade(): boolean {
298
+ const state = this.effectState as { positions: [number, number][]; idx: number };
299
+ const pixelsPerFrame = 15;
300
+
301
+ for (let i = 0; i < pixelsPerFrame; i++) {
302
+ if (state.idx >= state.positions.length) return true;
303
+ const [row, x] = state.positions[state.idx];
304
+ this.currentGrid[row][x] = this.finalGrid[row][x];
305
+ state.idx++;
306
+ }
307
+ return false;
308
+ }
309
+
310
+ private tickCrt(): boolean {
311
+ const state = this.effectState as { expansion: number };
312
+ const midRow = Math.floor(DISPLAY_HEIGHT / 2);
313
+
314
+ this.currentGrid = this.createEmptyGrid();
315
+
316
+ // Draw from middle expanding outward
317
+ const top = midRow - state.expansion;
318
+ const bottom = midRow + state.expansion;
319
+
320
+ for (let row = Math.max(0, top); row <= Math.min(DISPLAY_HEIGHT - 1, bottom); row++) {
321
+ for (let x = 0; x < WIDTH; x++) {
322
+ this.currentGrid[row][x] = this.finalGrid[row][x];
323
+ }
324
+ }
325
+
326
+ state.expansion++;
327
+ return state.expansion > DISPLAY_HEIGHT;
328
+ }
329
+
330
+ private tickGlitch(): boolean {
331
+ const state = this.effectState as { phase: number; glitchFrames: number };
332
+
333
+ if (state.phase < state.glitchFrames) {
334
+ // Glitch phase: show corrupted version
335
+ this.currentGrid = this.finalGrid.map((row) => {
336
+ const offset = Math.floor(Math.random() * 7) - 3;
337
+ const glitchRow = [...row];
338
+
339
+ // Random horizontal offset
340
+ if (Math.random() < 0.3) {
341
+ const shifted = glitchRow.slice(offset).concat(glitchRow.slice(0, offset));
342
+ return shifted.slice(0, WIDTH);
343
+ }
344
+
345
+ // Random vertical swap
346
+ if (Math.random() < 0.2) {
347
+ const swapRow = Math.floor(Math.random() * DISPLAY_HEIGHT);
348
+ return [...this.finalGrid[swapRow]];
349
+ }
350
+
351
+ return glitchRow;
352
+ });
353
+ state.phase++;
354
+ return false;
355
+ }
356
+
357
+ // Final frame: show clean image
358
+ this.currentGrid = this.finalGrid.map((row) => [...row]);
359
+ return true;
360
+ }
361
+
362
+ private tickDissolve(): boolean {
363
+ const state = this.effectState as { positions: [number, number][]; idx: number };
364
+ const pixelsPerFrame = 20;
365
+
366
+ for (let i = 0; i < pixelsPerFrame; i++) {
367
+ if (state.idx >= state.positions.length) return true;
368
+ const [row, x] = state.positions[state.idx];
369
+ this.currentGrid[row][x] = this.finalGrid[row][x];
370
+ state.idx++;
371
+ }
372
+ return false;
373
+ }
374
+
375
+ private updateDisplay(): void {
376
+ this.gridVersion++;
377
+ }
378
+
379
+ dispose(): void {
380
+ this.stopAnimation();
381
+ }
382
+ }
@@ -0,0 +1,86 @@
1
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
+ import { Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
3
+ import { getMarkdownTheme, theme } from "../theme/theme";
4
+
5
+ /**
6
+ * Component that renders a complete assistant message
7
+ */
8
+ export class AssistantMessageComponent extends Container {
9
+ private contentContainer: Container;
10
+ private hideThinkingBlock: boolean;
11
+
12
+ constructor(message?: AssistantMessage, hideThinkingBlock = false) {
13
+ super();
14
+
15
+ this.hideThinkingBlock = hideThinkingBlock;
16
+
17
+ // Container for text/thinking content
18
+ this.contentContainer = new Container();
19
+ this.addChild(this.contentContainer);
20
+
21
+ if (message) {
22
+ this.updateContent(message);
23
+ }
24
+ }
25
+
26
+ setHideThinkingBlock(hide: boolean): void {
27
+ this.hideThinkingBlock = hide;
28
+ }
29
+
30
+ updateContent(message: AssistantMessage): void {
31
+ // Clear content container
32
+ this.contentContainer.clear();
33
+
34
+ if (
35
+ message.content.length > 0 &&
36
+ message.content.some(
37
+ (c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()),
38
+ )
39
+ ) {
40
+ this.contentContainer.addChild(new Spacer(1));
41
+ }
42
+
43
+ // Render content in order
44
+ for (let i = 0; i < message.content.length; i++) {
45
+ const content = message.content[i];
46
+ if (content.type === "text" && content.text.trim()) {
47
+ // Assistant text messages with no background - trim the text
48
+ // Set paddingY=0 to avoid extra spacing before tool executions
49
+ this.contentContainer.addChild(new Markdown(content.text.trim(), 1, 0, getMarkdownTheme()));
50
+ } else if (content.type === "thinking" && content.thinking.trim()) {
51
+ // Check if there's text content after this thinking block
52
+ const hasTextAfter = message.content.slice(i + 1).some((c) => c.type === "text" && c.text.trim());
53
+
54
+ if (this.hideThinkingBlock) {
55
+ // Show static "Thinking..." label when hidden
56
+ this.contentContainer.addChild(new Text(theme.italic(theme.fg("thinkingText", "Thinking...")), 1, 0));
57
+ if (hasTextAfter) {
58
+ this.contentContainer.addChild(new Spacer(1));
59
+ }
60
+ } else {
61
+ // Thinking traces in thinkingText color, italic
62
+ this.contentContainer.addChild(
63
+ new Markdown(content.thinking.trim(), 1, 0, getMarkdownTheme(), {
64
+ color: (text: string) => theme.fg("thinkingText", text),
65
+ italic: true,
66
+ }),
67
+ );
68
+ this.contentContainer.addChild(new Spacer(1));
69
+ }
70
+ }
71
+ }
72
+
73
+ // Check if aborted - show after partial content
74
+ // But only if there are no tool calls (tool execution components will show the error)
75
+ const hasToolCalls = message.content.some((c) => c.type === "toolCall");
76
+ if (!hasToolCalls) {
77
+ if (message.stopReason === "aborted") {
78
+ this.contentContainer.addChild(new Text(theme.fg("error", "\nAborted"), 1, 0));
79
+ } else if (message.stopReason === "error") {
80
+ const errorMsg = message.errorMessage || "Unknown error";
81
+ this.contentContainer.addChild(new Spacer(1));
82
+ this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
83
+ }
84
+ }
85
+ }
86
+ }