@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,82 @@
1
+ import { Box, Container, Spacer, Text } from "@oh-my-pi/pi-tui";
2
+ import type { Rule } from "../../../capability/rule";
3
+ import { theme } from "../theme/theme";
4
+
5
+ /**
6
+ * Component that renders a TTSR (Time Traveling Stream Rules) notification.
7
+ * Shows when a rule violation is detected and the stream is being rewound.
8
+ */
9
+ export class TtsrNotificationComponent extends Container {
10
+ private rules: Rule[];
11
+ private box: Box;
12
+ private _expanded = false;
13
+
14
+ constructor(rules: Rule[]) {
15
+ super();
16
+ this.rules = rules;
17
+
18
+ this.addChild(new Spacer(1));
19
+
20
+ // Use inverse warning color for yellow background effect
21
+ this.box = new Box(1, 1, (t) => theme.inverse(theme.fg("warning", t)));
22
+ this.addChild(this.box);
23
+
24
+ this.rebuild();
25
+ }
26
+
27
+ setExpanded(expanded: boolean): void {
28
+ if (this._expanded !== expanded) {
29
+ this._expanded = expanded;
30
+ this.rebuild();
31
+ }
32
+ }
33
+
34
+ isExpanded(): boolean {
35
+ return this._expanded;
36
+ }
37
+
38
+ private rebuild(): void {
39
+ this.box.clear();
40
+
41
+ // Build header: warning symbol + rule name + rewind icon
42
+ const ruleNames = this.rules.map((r) => theme.bold(r.name)).join(", ");
43
+ const label = this.rules.length === 1 ? "rule" : "rules";
44
+ const header = `${theme.icon.warning} Injecting ${label}: ${ruleNames}`;
45
+
46
+ // Create header with rewind icon on the right
47
+ const rewindIcon = theme.icon.rewind;
48
+
49
+ this.box.addChild(new Text(`${header} ${rewindIcon}`, 0, 0));
50
+
51
+ // Show description(s) - italic and truncated
52
+ for (const rule of this.rules) {
53
+ const desc = rule.description || rule.content;
54
+ if (desc) {
55
+ this.box.addChild(new Spacer(1));
56
+
57
+ let displayText = desc.trim();
58
+ if (!this._expanded) {
59
+ // Truncate to first 2 lines
60
+ const lines = displayText.split("\n");
61
+ if (lines.length > 2) {
62
+ displayText = `${lines.slice(0, 2).join("\n")}${theme.format.ellipsis}`;
63
+ }
64
+ }
65
+
66
+ // Use italic for subtle distinction (fg colors conflict with inverse)
67
+ this.box.addChild(new Text(theme.italic(displayText), 0, 0));
68
+ }
69
+ }
70
+
71
+ // Show expand hint if collapsed and there's more content
72
+ if (!this._expanded) {
73
+ const hasMoreContent = this.rules.some((r) => {
74
+ const desc = r.description || r.content;
75
+ return desc && desc.split("\n").length > 2;
76
+ });
77
+ if (hasMoreContent) {
78
+ this.box.addChild(new Text(theme.italic(" (ctrl+o to expand)"), 0, 0));
79
+ }
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,159 @@
1
+ import {
2
+ type Component,
3
+ Container,
4
+ isArrowDown,
5
+ isArrowUp,
6
+ isCtrlC,
7
+ isEnter,
8
+ isEscape,
9
+ Spacer,
10
+ Text,
11
+ truncateToWidth,
12
+ } from "@oh-my-pi/pi-tui";
13
+ import { theme } from "../theme/theme";
14
+ import { DynamicBorder } from "./dynamic-border";
15
+
16
+ interface UserMessageItem {
17
+ id: string; // Entry ID in the session
18
+ text: string; // The message text
19
+ timestamp?: string; // Optional timestamp if available
20
+ }
21
+
22
+ /**
23
+ * Custom user message list component with selection
24
+ */
25
+ class UserMessageList implements Component {
26
+ private messages: UserMessageItem[] = [];
27
+ private selectedIndex: number = 0;
28
+ public onSelect?: (entryId: string) => void;
29
+ public onCancel?: () => void;
30
+ private maxVisible: number = 10; // Max messages visible
31
+
32
+ constructor(messages: UserMessageItem[]) {
33
+ // Store messages in chronological order (oldest to newest)
34
+ this.messages = messages;
35
+ // Start with the last (most recent) message selected
36
+ this.selectedIndex = Math.max(0, messages.length - 1);
37
+ }
38
+
39
+ invalidate(): void {
40
+ // No cached state to invalidate currently
41
+ }
42
+
43
+ render(width: number): string[] {
44
+ const lines: string[] = [];
45
+
46
+ if (this.messages.length === 0) {
47
+ lines.push(theme.fg("muted", " No user messages found"));
48
+ return lines;
49
+ }
50
+
51
+ // Calculate visible range with scrolling
52
+ const startIndex = Math.max(
53
+ 0,
54
+ Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.messages.length - this.maxVisible),
55
+ );
56
+ const endIndex = Math.min(startIndex + this.maxVisible, this.messages.length);
57
+
58
+ // Render visible messages (2 lines per message + blank line)
59
+ for (let i = startIndex; i < endIndex; i++) {
60
+ const message = this.messages[i];
61
+ const isSelected = i === this.selectedIndex;
62
+
63
+ // Normalize message to single line
64
+ const normalizedMessage = message.text.replace(/\n/g, " ").trim();
65
+
66
+ // First line: cursor + message
67
+ const cursor = isSelected ? theme.fg("accent", "› ") : " ";
68
+ const maxMsgWidth = width - 2; // Account for cursor (2 chars)
69
+ const truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth);
70
+ const messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);
71
+
72
+ lines.push(messageLine);
73
+
74
+ // Second line: metadata (position in history)
75
+ const position = i + 1;
76
+ const metadata = ` Message ${position} of ${this.messages.length}`;
77
+ const metadataLine = theme.fg("muted", metadata);
78
+ lines.push(metadataLine);
79
+ lines.push(""); // Blank line between messages
80
+ }
81
+
82
+ // Add scroll indicator if needed
83
+ if (startIndex > 0 || endIndex < this.messages.length) {
84
+ const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.messages.length})`);
85
+ lines.push(scrollInfo);
86
+ }
87
+
88
+ return lines;
89
+ }
90
+
91
+ handleInput(keyData: string): void {
92
+ // Up arrow - go to previous (older) message, wrap to bottom when at top
93
+ if (isArrowUp(keyData)) {
94
+ this.selectedIndex = this.selectedIndex === 0 ? this.messages.length - 1 : this.selectedIndex - 1;
95
+ }
96
+ // Down arrow - go to next (newer) message, wrap to top when at bottom
97
+ else if (isArrowDown(keyData)) {
98
+ this.selectedIndex = this.selectedIndex === this.messages.length - 1 ? 0 : this.selectedIndex + 1;
99
+ }
100
+ // Enter - select message and branch
101
+ else if (isEnter(keyData)) {
102
+ const selected = this.messages[this.selectedIndex];
103
+ if (selected && this.onSelect) {
104
+ this.onSelect(selected.id);
105
+ }
106
+ }
107
+ // Escape - cancel
108
+ else if (isEscape(keyData)) {
109
+ if (this.onCancel) {
110
+ this.onCancel();
111
+ }
112
+ }
113
+ // Ctrl+C - cancel
114
+ else if (isCtrlC(keyData)) {
115
+ if (this.onCancel) {
116
+ this.onCancel();
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Component that renders a user message selector for branching
124
+ */
125
+ export class UserMessageSelectorComponent extends Container {
126
+ private messageList: UserMessageList;
127
+
128
+ constructor(messages: UserMessageItem[], onSelect: (entryId: string) => void, onCancel: () => void) {
129
+ super();
130
+
131
+ // Add header
132
+ this.addChild(new Spacer(1));
133
+ this.addChild(new Text(theme.bold("Branch from Message"), 1, 0));
134
+ this.addChild(new Text(theme.fg("muted", "Select a message to create a new branch from that point"), 1, 0));
135
+ this.addChild(new Spacer(1));
136
+ this.addChild(new DynamicBorder());
137
+ this.addChild(new Spacer(1));
138
+
139
+ // Create message list
140
+ this.messageList = new UserMessageList(messages);
141
+ this.messageList.onSelect = onSelect;
142
+ this.messageList.onCancel = onCancel;
143
+
144
+ this.addChild(this.messageList);
145
+
146
+ // Add bottom border
147
+ this.addChild(new Spacer(1));
148
+ this.addChild(new DynamicBorder());
149
+
150
+ // Auto-cancel if no messages
151
+ if (messages.length === 0) {
152
+ setTimeout(() => onCancel(), 100);
153
+ }
154
+ }
155
+
156
+ getMessageList(): UserMessageList {
157
+ return this.messageList;
158
+ }
159
+ }
@@ -0,0 +1,18 @@
1
+ import { Container, Markdown, Spacer } from "@oh-my-pi/pi-tui";
2
+ import { getMarkdownTheme, theme } from "../theme/theme";
3
+
4
+ /**
5
+ * Component that renders a user message
6
+ */
7
+ export class UserMessageComponent extends Container {
8
+ constructor(text: string) {
9
+ super();
10
+ this.addChild(new Spacer(1));
11
+ this.addChild(
12
+ new Markdown(text, 1, 1, getMarkdownTheme(), {
13
+ bgColor: (text: string) => theme.bg("userMessageBg", text),
14
+ color: (text: string) => theme.fg("userMessageText", text),
15
+ }),
16
+ );
17
+ }
18
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Shared utility for truncating text to visual lines (accounting for line wrapping).
3
+ * Used by both tool-execution.ts and bash-execution.ts for consistent behavior.
4
+ */
5
+
6
+ import { Text } from "@oh-my-pi/pi-tui";
7
+
8
+ export interface VisualTruncateResult {
9
+ /** The visual lines to display */
10
+ visualLines: string[];
11
+ /** Number of visual lines that were skipped (hidden) */
12
+ skippedCount: number;
13
+ }
14
+
15
+ /**
16
+ * Truncate text to a maximum number of visual lines (from the end).
17
+ * This accounts for line wrapping based on terminal width.
18
+ *
19
+ * @param text - The text content (may contain newlines)
20
+ * @param maxVisualLines - Maximum number of visual lines to show
21
+ * @param width - Terminal/render width
22
+ * @param paddingX - Horizontal padding for Text component (default 0).
23
+ * Use 0 when result will be placed in a Box (Box adds its own padding).
24
+ * Use 1 when result will be placed in a plain Container.
25
+ * @returns The truncated visual lines and count of skipped lines
26
+ */
27
+ export function truncateToVisualLines(
28
+ text: string,
29
+ maxVisualLines: number,
30
+ width: number,
31
+ paddingX: number = 0,
32
+ ): VisualTruncateResult {
33
+ if (!text) {
34
+ return { visualLines: [], skippedCount: 0 };
35
+ }
36
+
37
+ // Create a temporary Text component to render and get visual lines
38
+ const tempText = new Text(text, paddingX, 0);
39
+ const allVisualLines = tempText.render(width);
40
+
41
+ if (allVisualLines.length <= maxVisualLines) {
42
+ return { visualLines: allVisualLines, skippedCount: 0 };
43
+ }
44
+
45
+ // Take the last N visual lines
46
+ const truncatedLines = allVisualLines.slice(-maxVisualLines);
47
+ const skippedCount = allVisualLines.length - maxVisualLines;
48
+
49
+ return { visualLines: truncatedLines, skippedCount };
50
+ }
@@ -0,0 +1,228 @@
1
+ import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
2
+ import { APP_NAME } from "../../../config";
3
+ import { theme } from "../theme/theme";
4
+
5
+ export interface RecentSession {
6
+ name: string;
7
+ timeAgo: string;
8
+ }
9
+
10
+ export interface LspServerInfo {
11
+ name: string;
12
+ status: "ready" | "error" | "connecting";
13
+ fileTypes: string[];
14
+ }
15
+
16
+ /**
17
+ * Premium welcome screen with block-based OMP logo and two-column layout.
18
+ */
19
+ export class WelcomeComponent implements Component {
20
+ private version: string;
21
+ private modelName: string;
22
+ private providerName: string;
23
+ private recentSessions: RecentSession[];
24
+ private lspServers: LspServerInfo[];
25
+
26
+ constructor(
27
+ version: string,
28
+ modelName: string,
29
+ providerName: string,
30
+ recentSessions: RecentSession[] = [],
31
+ lspServers: LspServerInfo[] = [],
32
+ ) {
33
+ this.version = version;
34
+ this.modelName = modelName;
35
+ this.providerName = providerName;
36
+ this.recentSessions = recentSessions;
37
+ this.lspServers = lspServers;
38
+ }
39
+
40
+ invalidate(): void {}
41
+
42
+ setModel(modelName: string, providerName: string): void {
43
+ this.modelName = modelName;
44
+ this.providerName = providerName;
45
+ }
46
+
47
+ setRecentSessions(sessions: RecentSession[]): void {
48
+ this.recentSessions = sessions;
49
+ }
50
+
51
+ setLspServers(servers: LspServerInfo[]): void {
52
+ this.lspServers = servers;
53
+ }
54
+
55
+ render(termWidth: number): string[] {
56
+ // Box dimensions - responsive with min/max
57
+ const minWidth = 80;
58
+ const maxWidth = 100;
59
+ const boxWidth = Math.max(minWidth, Math.min(termWidth - 2, maxWidth));
60
+ const leftCol = 26;
61
+ const rightCol = boxWidth - leftCol - 3; // 3 = │ + │ + │
62
+
63
+ // Block-based OMP logo (gradient: magenta → cyan)
64
+ // biome-ignore format: preserve ASCII art layout
65
+ const piLogo = ["▀████████████▀", " ╘███ ███ ", " ███ ███ ", " ███ ███ ", " ▄███▄ ▄███▄ "];
66
+
67
+ // Apply gradient to logo
68
+ const logoColored = piLogo.map((line) => this.gradientLine(line));
69
+
70
+ // Left column - centered content
71
+ const leftLines = [
72
+ "",
73
+ this.centerText(theme.bold("Welcome back!"), leftCol),
74
+ "",
75
+ ...logoColored.map((l) => this.centerText(l, leftCol)),
76
+ "",
77
+ this.centerText(theme.fg("muted", this.modelName), leftCol),
78
+ this.centerText(theme.fg("borderMuted", this.providerName), leftCol),
79
+ ];
80
+
81
+ // Right column separator
82
+ const separatorWidth = rightCol - 2; // padding on each side
83
+ const separator = ` ${theme.fg("dim", theme.boxRound.horizontal.repeat(separatorWidth))}`;
84
+
85
+ // Recent sessions content
86
+ const sessionLines: string[] = [];
87
+ if (this.recentSessions.length === 0) {
88
+ sessionLines.push(` ${theme.fg("dim", "No recent sessions")}`);
89
+ } else {
90
+ for (const session of this.recentSessions.slice(0, 3)) {
91
+ sessionLines.push(
92
+ ` ${theme.fg("dim", `${theme.md.bullet} `)}${theme.fg("muted", session.name)}${theme.fg("dim", ` (${session.timeAgo})`)}`,
93
+ );
94
+ }
95
+ }
96
+
97
+ // LSP servers content
98
+ const lspLines: string[] = [];
99
+ if (this.lspServers.length === 0) {
100
+ lspLines.push(` ${theme.fg("dim", "No LSP servers")}`);
101
+ } else {
102
+ for (const server of this.lspServers) {
103
+ const icon =
104
+ server.status === "ready"
105
+ ? theme.styledSymbol("status.success", "success")
106
+ : server.status === "connecting"
107
+ ? theme.styledSymbol("status.disabled", "warning")
108
+ : theme.styledSymbol("status.error", "error");
109
+ const exts = server.fileTypes.slice(0, 3).join(" ");
110
+ lspLines.push(` ${icon} ${theme.fg("muted", server.name)} ${theme.fg("dim", exts)}`);
111
+ }
112
+ }
113
+
114
+ // Right column
115
+ const rightLines = [
116
+ ` ${theme.bold(theme.fg("accent", "Tips"))}`,
117
+ ` ${theme.fg("dim", "?")}${theme.fg("muted", " for keyboard shortcuts")}`,
118
+ ` ${theme.fg("dim", "/")}${theme.fg("muted", " for commands")}`,
119
+ ` ${theme.fg("dim", "!")}${theme.fg("muted", " to run bash")}`,
120
+ separator,
121
+ ` ${theme.bold(theme.fg("accent", "LSP Servers"))}`,
122
+ ...lspLines,
123
+ separator,
124
+ ` ${theme.bold(theme.fg("accent", "Recent sessions"))}`,
125
+ ...sessionLines,
126
+ "",
127
+ ];
128
+
129
+ // Border characters (dim)
130
+ const hChar = theme.boxRound.horizontal;
131
+ const h = theme.fg("dim", hChar);
132
+ const v = theme.fg("dim", theme.boxRound.vertical);
133
+ const tl = theme.fg("dim", theme.boxRound.topLeft);
134
+ const tr = theme.fg("dim", theme.boxRound.topRight);
135
+ const bl = theme.fg("dim", theme.boxRound.bottomLeft);
136
+ const br = theme.fg("dim", theme.boxRound.bottomRight);
137
+
138
+ const lines: string[] = [];
139
+
140
+ // Top border with embedded title
141
+ const title = ` ${APP_NAME} v${this.version} `;
142
+ const titlePrefixRaw = hChar.repeat(3);
143
+ const titleStyled = theme.fg("dim", titlePrefixRaw) + theme.fg("muted", title);
144
+ const titleVisLen = visibleWidth(titlePrefixRaw) + visibleWidth(title);
145
+ const afterTitle = boxWidth - 2 - titleVisLen;
146
+ const afterTitleText = afterTitle > 0 ? theme.fg("dim", hChar.repeat(afterTitle)) : "";
147
+ lines.push(tl + titleStyled + afterTitleText + tr);
148
+
149
+ // Content rows
150
+ const maxRows = Math.max(leftLines.length, rightLines.length);
151
+ for (let i = 0; i < maxRows; i++) {
152
+ const left = this.fitToWidth(leftLines[i] ?? "", leftCol);
153
+ const right = this.fitToWidth(rightLines[i] ?? "", rightCol);
154
+ lines.push(v + left + v + right + v);
155
+ }
156
+
157
+ // Bottom border
158
+ lines.push(bl + h.repeat(leftCol) + theme.fg("dim", theme.boxSharp.teeUp) + h.repeat(rightCol) + br);
159
+
160
+ return lines;
161
+ }
162
+
163
+ /** Center text within a given width */
164
+ private centerText(text: string, width: number): string {
165
+ const visLen = visibleWidth(text);
166
+ if (visLen >= width) {
167
+ return truncateToWidth(text, width, theme.format.ellipsis);
168
+ }
169
+ const leftPad = Math.floor((width - visLen) / 2);
170
+ const rightPad = width - visLen - leftPad;
171
+ return " ".repeat(leftPad) + text + " ".repeat(rightPad);
172
+ }
173
+
174
+ /** Apply magenta→cyan gradient to a string */
175
+ private gradientLine(line: string): string {
176
+ const colors = [
177
+ "\x1b[38;5;199m", // bright magenta
178
+ "\x1b[38;5;171m", // magenta-purple
179
+ "\x1b[38;5;135m", // purple
180
+ "\x1b[38;5;99m", // purple-blue
181
+ "\x1b[38;5;75m", // cyan-blue
182
+ "\x1b[38;5;51m", // bright cyan
183
+ ];
184
+ const reset = "\x1b[0m";
185
+
186
+ let result = "";
187
+ let colorIdx = 0;
188
+ const step = Math.max(1, Math.floor(line.length / colors.length));
189
+
190
+ for (let i = 0; i < line.length; i++) {
191
+ if (i > 0 && i % step === 0 && colorIdx < colors.length - 1) {
192
+ colorIdx++;
193
+ }
194
+ const char = line[i];
195
+ if (char !== " ") {
196
+ result += colors[colorIdx] + char + reset;
197
+ } else {
198
+ result += char;
199
+ }
200
+ }
201
+ return result;
202
+ }
203
+
204
+ /** Fit string to exact width with ANSI-aware truncation/padding */
205
+ private fitToWidth(str: string, width: number): string {
206
+ const visLen = visibleWidth(str);
207
+ if (visLen > width) {
208
+ const ellipsis = theme.format.ellipsis;
209
+ const ellipsisWidth = visibleWidth(ellipsis);
210
+ const maxWidth = Math.max(0, width - ellipsisWidth);
211
+ let truncated = "";
212
+ let currentWidth = 0;
213
+ let inEscape = false;
214
+ for (const char of str) {
215
+ if (char === "\x1b") inEscape = true;
216
+ if (inEscape) {
217
+ truncated += char;
218
+ if (char === "m") inEscape = false;
219
+ } else if (currentWidth < maxWidth) {
220
+ truncated += char;
221
+ currentWidth++;
222
+ }
223
+ }
224
+ return `${truncated}${ellipsis}`;
225
+ }
226
+ return str + " ".repeat(width - visLen);
227
+ }
228
+ }