@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,384 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, type FSWatcher, readFileSync, watch } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
5
+ import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
6
+ import type { AgentSession } from "../../../core/agent-session";
7
+ import type { StatusLineSegmentOptions, StatusLineSettings } from "../../../core/settings-manager";
8
+ import { theme } from "../theme/theme";
9
+ import { getPreset } from "./status-line/presets";
10
+ import { renderSegment, type SegmentContext } from "./status-line/segments";
11
+ import { getSeparator } from "./status-line/separators";
12
+
13
+ // ═══════════════════════════════════════════════════════════════════════════
14
+ // Rendering Helpers
15
+ // ═══════════════════════════════════════════════════════════════════════════
16
+
17
+ /** Sanitize text for display in a single-line status */
18
+ function sanitizeStatusText(text: string): string {
19
+ return text
20
+ .replace(/[\r\n\t]/g, " ")
21
+ .replace(/ +/g, " ")
22
+ .trim();
23
+ }
24
+
25
+ /** Find the git root directory by walking up from cwd */
26
+ function findGitHeadPath(): string | null {
27
+ let dir = process.cwd();
28
+ while (true) {
29
+ const gitHeadPath = join(dir, ".git", "HEAD");
30
+ if (existsSync(gitHeadPath)) {
31
+ return gitHeadPath;
32
+ }
33
+ const parent = dirname(dir);
34
+ if (parent === dir) {
35
+ return null;
36
+ }
37
+ dir = parent;
38
+ }
39
+ }
40
+
41
+ // ═══════════════════════════════════════════════════════════════════════════
42
+ // StatusLineComponent
43
+ // ═══════════════════════════════════════════════════════════════════════════
44
+
45
+ export class StatusLineComponent implements Component {
46
+ private session: AgentSession;
47
+ private settings: StatusLineSettings = {};
48
+ private cachedBranch: string | null | undefined = undefined;
49
+ private gitWatcher: FSWatcher | null = null;
50
+ private onBranchChange: (() => void) | null = null;
51
+ private autoCompactEnabled: boolean = true;
52
+ private hookStatuses: Map<string, string> = new Map();
53
+ private subagentCount: number = 0;
54
+ private sessionStartTime: number = Date.now();
55
+
56
+ // Git status caching (1s TTL)
57
+ private cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
58
+ private gitStatusLastFetch = 0;
59
+
60
+ constructor(session: AgentSession) {
61
+ this.session = session;
62
+ // Load initial settings
63
+ this.settings = session.settingsManager?.getStatusLineSettings() ?? {};
64
+ }
65
+
66
+ updateSettings(settings: StatusLineSettings): void {
67
+ this.settings = settings;
68
+ }
69
+
70
+ setAutoCompactEnabled(enabled: boolean): void {
71
+ this.autoCompactEnabled = enabled;
72
+ }
73
+
74
+ setSubagentCount(count: number): void {
75
+ this.subagentCount = count;
76
+ }
77
+
78
+ setSessionStartTime(time: number): void {
79
+ this.sessionStartTime = time;
80
+ }
81
+
82
+ setHookStatus(key: string, text: string | undefined): void {
83
+ if (text === undefined) {
84
+ this.hookStatuses.delete(key);
85
+ } else {
86
+ this.hookStatuses.set(key, text);
87
+ }
88
+ }
89
+
90
+ watchBranch(onBranchChange: () => void): void {
91
+ this.onBranchChange = onBranchChange;
92
+ this.setupGitWatcher();
93
+ }
94
+
95
+ private setupGitWatcher(): void {
96
+ if (this.gitWatcher) {
97
+ this.gitWatcher.close();
98
+ this.gitWatcher = null;
99
+ }
100
+
101
+ const gitHeadPath = findGitHeadPath();
102
+ if (!gitHeadPath) return;
103
+
104
+ try {
105
+ this.gitWatcher = watch(gitHeadPath, () => {
106
+ this.cachedBranch = undefined;
107
+ if (this.onBranchChange) {
108
+ this.onBranchChange();
109
+ }
110
+ });
111
+ } catch {
112
+ // Silently fail
113
+ }
114
+ }
115
+
116
+ dispose(): void {
117
+ if (this.gitWatcher) {
118
+ this.gitWatcher.close();
119
+ this.gitWatcher = null;
120
+ }
121
+ }
122
+
123
+ invalidate(): void {
124
+ this.cachedBranch = undefined;
125
+ }
126
+
127
+ private getCurrentBranch(): string | null {
128
+ if (this.cachedBranch !== undefined) {
129
+ return this.cachedBranch;
130
+ }
131
+
132
+ try {
133
+ const gitHeadPath = findGitHeadPath();
134
+ if (!gitHeadPath) {
135
+ this.cachedBranch = null;
136
+ return null;
137
+ }
138
+ const content = readFileSync(gitHeadPath, "utf8").trim();
139
+
140
+ if (content.startsWith("ref: refs/heads/")) {
141
+ this.cachedBranch = content.slice(16);
142
+ } else {
143
+ this.cachedBranch = "detached";
144
+ }
145
+ } catch {
146
+ this.cachedBranch = null;
147
+ }
148
+
149
+ return this.cachedBranch;
150
+ }
151
+
152
+ private getGitStatus(): { staged: number; unstaged: number; untracked: number } | null {
153
+ const now = Date.now();
154
+ if (now - this.gitStatusLastFetch < 1000) {
155
+ return this.cachedGitStatus;
156
+ }
157
+
158
+ try {
159
+ const output = execSync("git status --porcelain 2>/dev/null", {
160
+ encoding: "utf8",
161
+ timeout: 1000,
162
+ stdio: ["pipe", "pipe", "pipe"],
163
+ });
164
+
165
+ let staged = 0;
166
+ let unstaged = 0;
167
+ let untracked = 0;
168
+
169
+ for (const line of output.split("\n")) {
170
+ if (!line) continue;
171
+ const x = line[0];
172
+ const y = line[1];
173
+
174
+ if (x === "?" && y === "?") {
175
+ untracked++;
176
+ continue;
177
+ }
178
+
179
+ if (x && x !== " " && x !== "?") {
180
+ staged++;
181
+ }
182
+
183
+ if (y && y !== " ") {
184
+ unstaged++;
185
+ }
186
+ }
187
+
188
+ this.cachedGitStatus = { staged, unstaged, untracked };
189
+ this.gitStatusLastFetch = now;
190
+ return this.cachedGitStatus;
191
+ } catch {
192
+ this.cachedGitStatus = null;
193
+ this.gitStatusLastFetch = now;
194
+ return null;
195
+ }
196
+ }
197
+
198
+ private buildSegmentContext(width: number): SegmentContext {
199
+ const state = this.session.state;
200
+
201
+ // Get usage statistics
202
+ const usageStats = this.session.sessionManager?.getUsageStatistics() ?? {
203
+ input: 0,
204
+ output: 0,
205
+ cacheRead: 0,
206
+ cacheWrite: 0,
207
+ cost: 0,
208
+ };
209
+
210
+ // Get context percentage
211
+ const lastAssistantMessage = state.messages
212
+ .slice()
213
+ .reverse()
214
+ .find((m) => m.role === "assistant" && m.stopReason !== "aborted") as AssistantMessage | undefined;
215
+
216
+ const contextTokens = lastAssistantMessage
217
+ ? lastAssistantMessage.usage.input +
218
+ lastAssistantMessage.usage.output +
219
+ lastAssistantMessage.usage.cacheRead +
220
+ lastAssistantMessage.usage.cacheWrite
221
+ : 0;
222
+ const contextWindow = state.model?.contextWindow || 0;
223
+ const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
224
+
225
+ return {
226
+ session: this.session,
227
+ width,
228
+ options: this.resolveSettings().segmentOptions ?? {},
229
+ usageStats,
230
+ contextPercent,
231
+ contextWindow,
232
+ autoCompactEnabled: this.autoCompactEnabled,
233
+ subagentCount: this.subagentCount,
234
+ sessionStartTime: this.sessionStartTime,
235
+ git: {
236
+ branch: this.getCurrentBranch(),
237
+ status: this.getGitStatus(),
238
+ },
239
+ };
240
+ }
241
+
242
+ private resolveSettings(): Required<
243
+ Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
244
+ > &
245
+ StatusLineSettings {
246
+ const preset = this.settings.preset ?? "default";
247
+ const presetDef = getPreset(preset);
248
+ const mergedSegmentOptions: StatusLineSettings["segmentOptions"] = {};
249
+
250
+ for (const [segment, options] of Object.entries(presetDef.segmentOptions ?? {})) {
251
+ mergedSegmentOptions[segment as keyof StatusLineSegmentOptions] = { ...(options as Record<string, unknown>) };
252
+ }
253
+
254
+ for (const [segment, options] of Object.entries(this.settings.segmentOptions ?? {})) {
255
+ const current = mergedSegmentOptions[segment as keyof StatusLineSegmentOptions] ?? {};
256
+ mergedSegmentOptions[segment as keyof StatusLineSegmentOptions] = {
257
+ ...(current as Record<string, unknown>),
258
+ ...(options as Record<string, unknown>),
259
+ };
260
+ }
261
+
262
+ return {
263
+ ...this.settings,
264
+ leftSegments: this.settings.leftSegments ?? presetDef.leftSegments,
265
+ rightSegments: this.settings.rightSegments ?? presetDef.rightSegments,
266
+ separator: this.settings.separator ?? presetDef.separator,
267
+ segmentOptions: mergedSegmentOptions,
268
+ };
269
+ }
270
+
271
+ private buildStatusLine(width: number): string {
272
+ const ctx = this.buildSegmentContext(width);
273
+ const effectiveSettings = this.resolveSettings();
274
+ const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
275
+
276
+ const bgAnsi = theme.getBgAnsi("statusLineBg");
277
+ const fgAnsi = theme.getFgAnsi("text");
278
+ const sepAnsi = theme.getFgAnsi("statusLineSep");
279
+
280
+ // Collect visible segment contents
281
+ const leftParts: string[] = [];
282
+ for (const segId of effectiveSettings.leftSegments) {
283
+ const rendered = renderSegment(segId, ctx);
284
+ if (rendered.visible && rendered.content) {
285
+ leftParts.push(rendered.content);
286
+ }
287
+ }
288
+
289
+ const rightParts: string[] = [];
290
+ for (const segId of effectiveSettings.rightSegments) {
291
+ const rendered = renderSegment(segId, ctx);
292
+ if (rendered.visible && rendered.content) {
293
+ rightParts.push(rendered.content);
294
+ }
295
+ }
296
+
297
+ const topFillWidth = width > 0 ? Math.max(0, width - 4) : 0;
298
+ const left = [...leftParts];
299
+ const right = [...rightParts];
300
+
301
+ const leftSepWidth = visibleWidth(separatorDef.left);
302
+ const rightSepWidth = visibleWidth(separatorDef.right);
303
+ const leftCapWidth = separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.right) : 0;
304
+ const rightCapWidth = separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.left) : 0;
305
+
306
+ const groupWidth = (parts: string[], capWidth: number, sepWidth: number): number => {
307
+ if (parts.length === 0) return 0;
308
+ const partsWidth = parts.reduce((sum, part) => sum + visibleWidth(part), 0);
309
+ const sepTotal = Math.max(0, parts.length - 1) * (sepWidth + 2);
310
+ return partsWidth + sepTotal + 2 + capWidth;
311
+ };
312
+
313
+ let leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
314
+ let rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
315
+ const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
316
+
317
+ if (topFillWidth > 0) {
318
+ while (totalWidth() > topFillWidth && right.length > 0) {
319
+ right.pop();
320
+ rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
321
+ }
322
+ while (totalWidth() > topFillWidth && left.length > 0) {
323
+ left.pop();
324
+ leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
325
+ }
326
+ }
327
+
328
+ const renderGroup = (parts: string[], direction: "left" | "right"): string => {
329
+ if (parts.length === 0) return "";
330
+ const sep = direction === "left" ? separatorDef.left : separatorDef.right;
331
+ const cap = separatorDef.endCaps
332
+ ? direction === "left"
333
+ ? separatorDef.endCaps.right
334
+ : separatorDef.endCaps.left
335
+ : "";
336
+ const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : sepAnsi;
337
+ const capText = cap ? `${capPrefix}${cap}\x1b[0m` : "";
338
+
339
+ let content = bgAnsi + fgAnsi;
340
+ content += ` ${parts.join(` ${sepAnsi}${sep}${fgAnsi} `)} `;
341
+ content += "\x1b[0m";
342
+
343
+ if (capText) {
344
+ return direction === "right" ? capText + content : content + capText;
345
+ }
346
+ return content;
347
+ };
348
+
349
+ const leftGroup = renderGroup(left, "left");
350
+ const rightGroup = renderGroup(right, "right");
351
+ if (!leftGroup && !rightGroup) return "";
352
+
353
+ if (topFillWidth === 0 || left.length === 0 || right.length === 0) {
354
+ return leftGroup + (leftGroup && rightGroup ? " " : "") + rightGroup;
355
+ }
356
+
357
+ leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
358
+ rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
359
+ const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
360
+ return leftGroup + " ".repeat(gapWidth) + rightGroup;
361
+ }
362
+
363
+ getTopBorder(width: number): { content: string; width: number } {
364
+ const content = this.buildStatusLine(width);
365
+ return {
366
+ content,
367
+ width: visibleWidth(content),
368
+ };
369
+ }
370
+
371
+ render(width: number): string[] {
372
+ // Only render hook statuses - main status is in editor's top border
373
+ const showHooks = this.settings.showHookStatus ?? true;
374
+ if (!showHooks || this.hookStatuses.size === 0) {
375
+ return [];
376
+ }
377
+
378
+ const sortedStatuses = Array.from(this.hookStatuses.entries())
379
+ .sort(([a], [b]) => a.localeCompare(b))
380
+ .map(([, text]) => sanitizeStatusText(text));
381
+ const hookLine = sortedStatuses.join(" ");
382
+ return [truncateToWidth(hookLine, width, theme.fg("statusLineSep", theme.format.ellipsis))];
383
+ }
384
+ }
@@ -0,0 +1,62 @@
1
+ import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
2
+ import { getAvailableThemes, getSelectListTheme } from "../theme/theme";
3
+ import { DynamicBorder } from "./dynamic-border";
4
+
5
+ /**
6
+ * Component that renders a theme selector
7
+ */
8
+ export class ThemeSelectorComponent extends Container {
9
+ private selectList: SelectList;
10
+ private onPreview: (themeName: string) => void;
11
+
12
+ constructor(
13
+ currentTheme: string,
14
+ onSelect: (themeName: string) => void,
15
+ onCancel: () => void,
16
+ onPreview: (themeName: string) => void,
17
+ ) {
18
+ super();
19
+ this.onPreview = onPreview;
20
+
21
+ // Get available themes and create select items
22
+ const themes = getAvailableThemes();
23
+ const themeItems: SelectItem[] = themes.map((name) => ({
24
+ value: name,
25
+ label: name,
26
+ description: name === currentTheme ? "(current)" : undefined,
27
+ }));
28
+
29
+ // Add top border
30
+ this.addChild(new DynamicBorder());
31
+
32
+ // Create selector
33
+ this.selectList = new SelectList(themeItems, 10, getSelectListTheme());
34
+
35
+ // Preselect current theme
36
+ const currentIndex = themes.indexOf(currentTheme);
37
+ if (currentIndex !== -1) {
38
+ this.selectList.setSelectedIndex(currentIndex);
39
+ }
40
+
41
+ this.selectList.onSelect = (item) => {
42
+ onSelect(item.value);
43
+ };
44
+
45
+ this.selectList.onCancel = () => {
46
+ onCancel();
47
+ };
48
+
49
+ this.selectList.onSelectionChange = (item) => {
50
+ this.onPreview(item.value);
51
+ };
52
+
53
+ this.addChild(this.selectList);
54
+
55
+ // Add bottom border
56
+ this.addChild(new DynamicBorder());
57
+ }
58
+
59
+ getSelectList(): SelectList {
60
+ return this.selectList;
61
+ }
62
+ }
@@ -0,0 +1,64 @@
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
+ import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
3
+ import { getSelectListTheme } from "../theme/theme";
4
+ import { DynamicBorder } from "./dynamic-border";
5
+
6
+ const LEVEL_DESCRIPTIONS: Record<ThinkingLevel, string> = {
7
+ off: "No reasoning",
8
+ minimal: "Very brief reasoning (~1k tokens)",
9
+ low: "Light reasoning (~2k tokens)",
10
+ medium: "Moderate reasoning (~8k tokens)",
11
+ high: "Deep reasoning (~16k tokens)",
12
+ xhigh: "Maximum reasoning (~32k tokens)",
13
+ };
14
+
15
+ /**
16
+ * Component that renders a thinking level selector with borders
17
+ */
18
+ export class ThinkingSelectorComponent extends Container {
19
+ private selectList: SelectList;
20
+
21
+ constructor(
22
+ currentLevel: ThinkingLevel,
23
+ availableLevels: ThinkingLevel[],
24
+ onSelect: (level: ThinkingLevel) => void,
25
+ onCancel: () => void,
26
+ ) {
27
+ super();
28
+
29
+ const thinkingLevels: SelectItem[] = availableLevels.map((level) => ({
30
+ value: level,
31
+ label: level,
32
+ description: LEVEL_DESCRIPTIONS[level],
33
+ }));
34
+
35
+ // Add top border
36
+ this.addChild(new DynamicBorder());
37
+
38
+ // Create selector
39
+ this.selectList = new SelectList(thinkingLevels, thinkingLevels.length, getSelectListTheme());
40
+
41
+ // Preselect current level
42
+ const currentIndex = thinkingLevels.findIndex((item) => item.value === currentLevel);
43
+ if (currentIndex !== -1) {
44
+ this.selectList.setSelectedIndex(currentIndex);
45
+ }
46
+
47
+ this.selectList.onSelect = (item) => {
48
+ onSelect(item.value as ThinkingLevel);
49
+ };
50
+
51
+ this.selectList.onCancel = () => {
52
+ onCancel();
53
+ };
54
+
55
+ this.addChild(this.selectList);
56
+
57
+ // Add bottom border
58
+ this.addChild(new DynamicBorder());
59
+ }
60
+
61
+ getSelectList(): SelectList {
62
+ return this.selectList;
63
+ }
64
+ }