@mariozechner/pi-coding-agent 0.30.2 → 0.31.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 (297) hide show
  1. package/CHANGELOG.md +244 -1
  2. package/README.md +105 -84
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +5 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/file-processor.d.ts +3 -3
  7. package/dist/cli/file-processor.d.ts.map +1 -1
  8. package/dist/cli/file-processor.js +7 -10
  9. package/dist/cli/file-processor.js.map +1 -1
  10. package/dist/config.d.ts +9 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +18 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/core/agent-session.d.ts +73 -34
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +464 -210
  17. package/dist/core/agent-session.js.map +1 -1
  18. package/dist/core/auth-storage.d.ts +2 -2
  19. package/dist/core/auth-storage.d.ts.map +1 -1
  20. package/dist/core/auth-storage.js +2 -2
  21. package/dist/core/auth-storage.js.map +1 -1
  22. package/dist/core/bash-executor.d.ts +2 -2
  23. package/dist/core/bash-executor.d.ts.map +1 -1
  24. package/dist/core/bash-executor.js +2 -2
  25. package/dist/core/bash-executor.js.map +1 -1
  26. package/dist/core/compaction/branch-summarization.d.ts +84 -0
  27. package/dist/core/compaction/branch-summarization.d.ts.map +1 -0
  28. package/dist/core/compaction/branch-summarization.js +233 -0
  29. package/dist/core/compaction/branch-summarization.js.map +1 -0
  30. package/dist/core/{compaction.d.ts → compaction/compaction.d.ts} +38 -19
  31. package/dist/core/compaction/compaction.d.ts.map +1 -0
  32. package/dist/core/compaction/compaction.js +558 -0
  33. package/dist/core/compaction/compaction.js.map +1 -0
  34. package/dist/core/compaction/index.d.ts +7 -0
  35. package/dist/core/compaction/index.d.ts.map +1 -0
  36. package/dist/core/compaction/index.js +7 -0
  37. package/dist/core/compaction/index.js.map +1 -0
  38. package/dist/core/compaction/utils.d.ts +35 -0
  39. package/dist/core/compaction/utils.d.ts.map +1 -0
  40. package/dist/core/compaction/utils.js +138 -0
  41. package/dist/core/compaction/utils.js.map +1 -0
  42. package/dist/core/custom-tools/index.d.ts +2 -1
  43. package/dist/core/custom-tools/index.d.ts.map +1 -1
  44. package/dist/core/custom-tools/index.js +1 -0
  45. package/dist/core/custom-tools/index.js.map +1 -1
  46. package/dist/core/custom-tools/loader.d.ts.map +1 -1
  47. package/dist/core/custom-tools/loader.js +13 -80
  48. package/dist/core/custom-tools/loader.js.map +1 -1
  49. package/dist/core/custom-tools/types.d.ts +84 -59
  50. package/dist/core/custom-tools/types.d.ts.map +1 -1
  51. package/dist/core/custom-tools/types.js.map +1 -1
  52. package/dist/core/custom-tools/wrapper.d.ts +15 -0
  53. package/dist/core/custom-tools/wrapper.d.ts.map +1 -0
  54. package/dist/core/custom-tools/wrapper.js +23 -0
  55. package/dist/core/custom-tools/wrapper.js.map +1 -0
  56. package/dist/core/exec.d.ts +29 -0
  57. package/dist/core/exec.d.ts.map +1 -0
  58. package/dist/core/exec.js +71 -0
  59. package/dist/core/exec.js.map +1 -0
  60. package/dist/core/export-html/index.d.ts +17 -0
  61. package/dist/core/export-html/index.d.ts.map +1 -0
  62. package/dist/core/export-html/index.js +171 -0
  63. package/dist/core/export-html/index.js.map +1 -0
  64. package/dist/core/export-html/template.css +781 -0
  65. package/dist/core/export-html/template.html +54 -0
  66. package/dist/core/export-html/template.js +1185 -0
  67. package/dist/core/export-html/vendor/highlight.min.js +1213 -0
  68. package/dist/core/export-html/vendor/marked.min.js +6 -0
  69. package/dist/core/hooks/index.d.ts +4 -4
  70. package/dist/core/hooks/index.d.ts.map +1 -1
  71. package/dist/core/hooks/index.js +3 -3
  72. package/dist/core/hooks/index.js.map +1 -1
  73. package/dist/core/hooks/loader.d.ts +40 -5
  74. package/dist/core/hooks/loader.d.ts.map +1 -1
  75. package/dist/core/hooks/loader.js +43 -10
  76. package/dist/core/hooks/loader.js.map +1 -1
  77. package/dist/core/hooks/runner.d.ts +94 -18
  78. package/dist/core/hooks/runner.d.ts.map +1 -1
  79. package/dist/core/hooks/runner.js +199 -120
  80. package/dist/core/hooks/runner.js.map +1 -1
  81. package/dist/core/hooks/tool-wrapper.d.ts +1 -1
  82. package/dist/core/hooks/tool-wrapper.d.ts.map +1 -1
  83. package/dist/core/hooks/tool-wrapper.js +36 -19
  84. package/dist/core/hooks/tool-wrapper.js.map +1 -1
  85. package/dist/core/hooks/types.d.ts +407 -96
  86. package/dist/core/hooks/types.d.ts.map +1 -1
  87. package/dist/core/hooks/types.js.map +1 -1
  88. package/dist/core/index.d.ts +4 -3
  89. package/dist/core/index.d.ts.map +1 -1
  90. package/dist/core/index.js.map +1 -1
  91. package/dist/core/messages.d.ts +44 -12
  92. package/dist/core/messages.d.ts.map +1 -1
  93. package/dist/core/messages.js +82 -34
  94. package/dist/core/messages.js.map +1 -1
  95. package/dist/core/model-registry.d.ts +5 -5
  96. package/dist/core/model-registry.d.ts.map +1 -1
  97. package/dist/core/model-registry.js +7 -7
  98. package/dist/core/model-registry.js.map +1 -1
  99. package/dist/core/model-resolver.d.ts +7 -7
  100. package/dist/core/model-resolver.d.ts.map +1 -1
  101. package/dist/core/model-resolver.js +45 -14
  102. package/dist/core/model-resolver.js.map +1 -1
  103. package/dist/core/sdk.d.ts +7 -10
  104. package/dist/core/sdk.d.ts.map +1 -1
  105. package/dist/core/sdk.js +88 -32
  106. package/dist/core/sdk.js.map +1 -1
  107. package/dist/core/session-manager.d.ts +202 -36
  108. package/dist/core/session-manager.d.ts.map +1 -1
  109. package/dist/core/session-manager.js +565 -133
  110. package/dist/core/session-manager.js.map +1 -1
  111. package/dist/core/settings-manager.d.ts +9 -3
  112. package/dist/core/settings-manager.d.ts.map +1 -1
  113. package/dist/core/settings-manager.js +13 -12
  114. package/dist/core/settings-manager.js.map +1 -1
  115. package/dist/core/system-prompt.d.ts.map +1 -1
  116. package/dist/core/system-prompt.js +6 -3
  117. package/dist/core/system-prompt.js.map +1 -1
  118. package/dist/core/tools/bash.d.ts +1 -1
  119. package/dist/core/tools/bash.d.ts.map +1 -1
  120. package/dist/core/tools/bash.js.map +1 -1
  121. package/dist/core/tools/edit-diff.d.ts +33 -0
  122. package/dist/core/tools/edit-diff.d.ts.map +1 -0
  123. package/dist/core/tools/edit-diff.js +171 -0
  124. package/dist/core/tools/edit-diff.js.map +1 -0
  125. package/dist/core/tools/edit.d.ts +7 -1
  126. package/dist/core/tools/edit.d.ts.map +1 -1
  127. package/dist/core/tools/edit.js +20 -95
  128. package/dist/core/tools/edit.js.map +1 -1
  129. package/dist/core/tools/find.d.ts +1 -1
  130. package/dist/core/tools/find.d.ts.map +1 -1
  131. package/dist/core/tools/find.js.map +1 -1
  132. package/dist/core/tools/grep.d.ts +1 -1
  133. package/dist/core/tools/grep.d.ts.map +1 -1
  134. package/dist/core/tools/grep.js.map +1 -1
  135. package/dist/core/tools/index.d.ts +1 -1
  136. package/dist/core/tools/index.d.ts.map +1 -1
  137. package/dist/core/tools/index.js.map +1 -1
  138. package/dist/core/tools/ls.d.ts +1 -1
  139. package/dist/core/tools/ls.d.ts.map +1 -1
  140. package/dist/core/tools/ls.js.map +1 -1
  141. package/dist/core/tools/read.d.ts +1 -1
  142. package/dist/core/tools/read.d.ts.map +1 -1
  143. package/dist/core/tools/read.js.map +1 -1
  144. package/dist/core/tools/write.d.ts +1 -1
  145. package/dist/core/tools/write.d.ts.map +1 -1
  146. package/dist/core/tools/write.js.map +1 -1
  147. package/dist/index.d.ts +8 -7
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +5 -5
  150. package/dist/index.js.map +1 -1
  151. package/dist/main.d.ts.map +1 -1
  152. package/dist/main.js +22 -21
  153. package/dist/main.js.map +1 -1
  154. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  155. package/dist/modes/interactive/components/assistant-message.js +3 -4
  156. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  157. package/dist/modes/interactive/components/bash-execution.d.ts +1 -1
  158. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  159. package/dist/modes/interactive/components/bash-execution.js +6 -2
  160. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  161. package/dist/modes/interactive/components/bordered-loader.d.ts +12 -0
  162. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -0
  163. package/dist/modes/interactive/components/bordered-loader.js +30 -0
  164. package/dist/modes/interactive/components/bordered-loader.js.map +1 -0
  165. package/dist/modes/interactive/components/branch-summary-message.d.ts +14 -0
  166. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -0
  167. package/dist/modes/interactive/components/branch-summary-message.js +35 -0
  168. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -0
  169. package/dist/modes/interactive/components/compaction-summary-message.d.ts +14 -0
  170. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -0
  171. package/dist/modes/interactive/components/compaction-summary-message.js +36 -0
  172. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -0
  173. package/dist/modes/interactive/components/dynamic-border.d.ts +5 -1
  174. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  175. package/dist/modes/interactive/components/dynamic-border.js +5 -1
  176. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  177. package/dist/modes/interactive/components/footer.d.ts +12 -6
  178. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  179. package/dist/modes/interactive/components/footer.js +57 -25
  180. package/dist/modes/interactive/components/footer.js.map +1 -1
  181. package/dist/modes/interactive/components/hook-editor.d.ts +15 -0
  182. package/dist/modes/interactive/components/hook-editor.d.ts.map +1 -0
  183. package/dist/modes/interactive/components/hook-editor.js +95 -0
  184. package/dist/modes/interactive/components/hook-editor.js.map +1 -0
  185. package/dist/modes/interactive/components/hook-message.d.ts +18 -0
  186. package/dist/modes/interactive/components/hook-message.d.ts.map +1 -0
  187. package/dist/modes/interactive/components/hook-message.js +80 -0
  188. package/dist/modes/interactive/components/hook-message.js.map +1 -0
  189. package/dist/modes/interactive/components/model-selector.d.ts +3 -3
  190. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  191. package/dist/modes/interactive/components/model-selector.js +1 -1
  192. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  193. package/dist/modes/interactive/components/tool-execution.d.ts +15 -2
  194. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  195. package/dist/modes/interactive/components/tool-execution.js +70 -21
  196. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  197. package/dist/modes/interactive/components/tree-selector.d.ts +52 -0
  198. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -0
  199. package/dist/modes/interactive/components/tree-selector.js +745 -0
  200. package/dist/modes/interactive/components/tree-selector.js.map +1 -0
  201. package/dist/modes/interactive/components/user-message-selector.d.ts +3 -3
  202. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  203. package/dist/modes/interactive/components/user-message-selector.js +1 -1
  204. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  205. package/dist/modes/interactive/components/user-message.d.ts +1 -1
  206. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  207. package/dist/modes/interactive/components/user-message.js +2 -5
  208. package/dist/modes/interactive/components/user-message.js.map +1 -1
  209. package/dist/modes/interactive/interactive-mode.d.ts +29 -12
  210. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  211. package/dist/modes/interactive/interactive-mode.js +589 -208
  212. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  213. package/dist/modes/interactive/theme/dark.json +13 -1
  214. package/dist/modes/interactive/theme/light.json +13 -1
  215. package/dist/modes/interactive/theme/theme-schema.json +34 -0
  216. package/dist/modes/interactive/theme/theme.d.ts +20 -2
  217. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  218. package/dist/modes/interactive/theme/theme.js +135 -2
  219. package/dist/modes/interactive/theme/theme.js.map +1 -1
  220. package/dist/modes/print-mode.d.ts +3 -3
  221. package/dist/modes/print-mode.d.ts.map +1 -1
  222. package/dist/modes/print-mode.js +26 -20
  223. package/dist/modes/print-mode.js.map +1 -1
  224. package/dist/modes/rpc/rpc-client.d.ts +13 -10
  225. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  226. package/dist/modes/rpc/rpc-client.js +11 -10
  227. package/dist/modes/rpc/rpc-client.js.map +1 -1
  228. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  229. package/dist/modes/rpc/rpc-mode.js +88 -35
  230. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  231. package/dist/modes/rpc/rpc-types.d.ts +30 -11
  232. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  233. package/dist/modes/rpc/rpc-types.js.map +1 -1
  234. package/dist/utils/shell.d.ts +4 -2
  235. package/dist/utils/shell.d.ts.map +1 -1
  236. package/dist/utils/shell.js +36 -7
  237. package/dist/utils/shell.js.map +1 -1
  238. package/dist/utils/tools-manager.d.ts +1 -1
  239. package/dist/utils/tools-manager.d.ts.map +1 -1
  240. package/dist/utils/tools-manager.js +2 -2
  241. package/dist/utils/tools-manager.js.map +1 -1
  242. package/docs/compaction.md +388 -0
  243. package/docs/custom-tools.md +146 -43
  244. package/docs/extension-loading.md +1004 -0
  245. package/docs/hooks.md +562 -596
  246. package/docs/rpc.md +33 -19
  247. package/docs/sdk.md +93 -21
  248. package/docs/session-tree-plan.md +441 -0
  249. package/docs/session.md +172 -21
  250. package/docs/skills.md +2 -0
  251. package/docs/theme.md +31 -2
  252. package/docs/tree.md +197 -0
  253. package/docs/tui.md +343 -0
  254. package/examples/README.md +1 -9
  255. package/examples/custom-tools/hello/index.ts +4 -3
  256. package/examples/custom-tools/question/index.ts +4 -4
  257. package/examples/custom-tools/subagent/index.ts +7 -6
  258. package/examples/custom-tools/todo/index.ts +11 -5
  259. package/examples/hooks/README.md +29 -71
  260. package/examples/hooks/auto-commit-on-exit.ts +8 -9
  261. package/examples/hooks/confirm-destructive.ts +29 -30
  262. package/examples/hooks/custom-compaction.ts +20 -21
  263. package/examples/hooks/dirty-repo-guard.ts +41 -40
  264. package/examples/hooks/file-trigger.ts +10 -5
  265. package/examples/hooks/git-checkpoint.ts +16 -12
  266. package/examples/hooks/handoff.ts +150 -0
  267. package/examples/hooks/permission-gate.ts +1 -1
  268. package/examples/hooks/protected-paths.ts +1 -1
  269. package/examples/hooks/qna.ts +119 -0
  270. package/examples/hooks/snake.ts +343 -0
  271. package/examples/hooks/status-line.ts +40 -0
  272. package/examples/sdk/01-minimal.ts +1 -1
  273. package/examples/sdk/02-custom-model.ts +1 -1
  274. package/examples/sdk/03-custom-prompt.ts +1 -1
  275. package/examples/sdk/04-skills.ts +1 -1
  276. package/examples/sdk/05-tools.ts +4 -4
  277. package/examples/sdk/06-hooks.ts +1 -1
  278. package/examples/sdk/07-context-files.ts +1 -1
  279. package/examples/sdk/08-slash-commands.ts +6 -1
  280. package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
  281. package/examples/sdk/10-settings.ts +1 -1
  282. package/examples/sdk/11-sessions.ts +1 -1
  283. package/examples/sdk/12-full-control.ts +4 -7
  284. package/package.json +6 -6
  285. package/dist/core/compaction.d.ts.map +0 -1
  286. package/dist/core/compaction.js +0 -412
  287. package/dist/core/compaction.js.map +0 -1
  288. package/dist/core/export-html.d.ts +0 -23
  289. package/dist/core/export-html.d.ts.map +0 -1
  290. package/dist/core/export-html.js +0 -1185
  291. package/dist/core/export-html.js.map +0 -1
  292. package/dist/modes/interactive/components/compaction.d.ts +0 -15
  293. package/dist/modes/interactive/components/compaction.d.ts.map +0 -1
  294. package/dist/modes/interactive/components/compaction.js +0 -41
  295. package/dist/modes/interactive/components/compaction.js.map +0 -1
  296. package/docs/hooks-v2.md +0 -385
  297. package/docs/session-tree.md +0 -452
@@ -0,0 +1,35 @@
1
+ import { Box, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
2
+ import { getMarkdownTheme, theme } from "../theme/theme.js";
3
+ /**
4
+ * Component that renders a branch summary message with collapsed/expanded state.
5
+ * Uses same background color as hook messages for visual consistency.
6
+ */
7
+ export class BranchSummaryMessageComponent extends Box {
8
+ expanded = false;
9
+ message;
10
+ constructor(message) {
11
+ super(1, 1, (t) => theme.bg("customMessageBg", t));
12
+ this.message = message;
13
+ this.updateDisplay();
14
+ }
15
+ setExpanded(expanded) {
16
+ this.expanded = expanded;
17
+ this.updateDisplay();
18
+ }
19
+ updateDisplay() {
20
+ this.clear();
21
+ const label = theme.fg("customMessageLabel", `\x1b[1m[branch]\x1b[22m`);
22
+ this.addChild(new Text(label, 0, 0));
23
+ this.addChild(new Spacer(1));
24
+ if (this.expanded) {
25
+ const header = "**Branch Summary**\n\n";
26
+ this.addChild(new Markdown(header + this.message.summary, 0, 0, getMarkdownTheme(), {
27
+ color: (text) => theme.fg("customMessageText", text),
28
+ }));
29
+ }
30
+ else {
31
+ this.addChild(new Text(theme.fg("customMessageText", "Branch summary (ctrl+o to expand)"), 0, 0));
32
+ }
33
+ }
34
+ }
35
+ //# sourceMappingURL=branch-summary-message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branch-summary-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/branch-summary-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAEnE,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE5D;;;GAGG;AACH,MAAM,OAAO,6BAA8B,SAAQ,GAAG;IAC7C,QAAQ,GAAG,KAAK,CAAC;IACjB,OAAO,CAAuB;IAEtC,YAAY,OAA6B,EAAE;QAC1C,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,CAAC;QACnD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,aAAa,EAAE,CAAC;IAAA,CACrB;IAED,WAAW,CAAC,QAAiB,EAAQ;QACpC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,aAAa,EAAE,CAAC;IAAA,CACrB;IAEO,aAAa,GAAS;QAC7B,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,MAAM,KAAK,GAAG,KAAK,CAAC,EAAE,CAAC,oBAAoB,EAAE,yBAAyB,CAAC,CAAC;QACxE,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7B,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,MAAM,MAAM,GAAG,wBAAwB,CAAC;YACxC,IAAI,CAAC,QAAQ,CACZ,IAAI,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,gBAAgB,EAAE,EAAE;gBACrE,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,mBAAmB,EAAE,IAAI,CAAC;aAC5D,CAAC,CACF,CAAC;QACH,CAAC;aAAM,CAAC;YACP,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,mBAAmB,EAAE,mCAAmC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACnG,CAAC;IAAA,CACD;CACD","sourcesContent":["import { Box, Markdown, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport type { BranchSummaryMessage } from \"../../../core/messages.js\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\n\n/**\n * Component that renders a branch summary message with collapsed/expanded state.\n * Uses same background color as hook messages for visual consistency.\n */\nexport class BranchSummaryMessageComponent extends Box {\n\tprivate expanded = false;\n\tprivate message: BranchSummaryMessage;\n\n\tconstructor(message: BranchSummaryMessage) {\n\t\tsuper(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\t\tthis.message = message;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tthis.clear();\n\n\t\tconst label = theme.fg(\"customMessageLabel\", `\\x1b[1m[branch]\\x1b[22m`);\n\t\tthis.addChild(new Text(label, 0, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\tif (this.expanded) {\n\t\t\tconst header = \"**Branch Summary**\\n\\n\";\n\t\t\tthis.addChild(\n\t\t\t\tnew Markdown(header + this.message.summary, 0, 0, getMarkdownTheme(), {\n\t\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t\t}),\n\t\t\t);\n\t\t} else {\n\t\t\tthis.addChild(new Text(theme.fg(\"customMessageText\", \"Branch summary (ctrl+o to expand)\"), 0, 0));\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import { Box } from "@mariozechner/pi-tui";
2
+ import type { CompactionSummaryMessage } from "../../../core/messages.js";
3
+ /**
4
+ * Component that renders a compaction message with collapsed/expanded state.
5
+ * Uses same background color as hook messages for visual consistency.
6
+ */
7
+ export declare class CompactionSummaryMessageComponent extends Box {
8
+ private expanded;
9
+ private message;
10
+ constructor(message: CompactionSummaryMessage);
11
+ setExpanded(expanded: boolean): void;
12
+ private updateDisplay;
13
+ }
14
+ //# sourceMappingURL=compaction-summary-message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compaction-summary-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/compaction-summary-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAA0B,MAAM,sBAAsB,CAAC;AACnE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAG1E;;;GAGG;AACH,qBAAa,iCAAkC,SAAQ,GAAG;IACzD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAA2B;IAE1C,YAAY,OAAO,EAAE,wBAAwB,EAI5C;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAGnC;IAED,OAAO,CAAC,aAAa;CAqBrB","sourcesContent":["import { Box, Markdown, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport type { CompactionSummaryMessage } from \"../../../core/messages.js\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\n\n/**\n * Component that renders a compaction message with collapsed/expanded state.\n * Uses same background color as hook messages for visual consistency.\n */\nexport class CompactionSummaryMessageComponent extends Box {\n\tprivate expanded = false;\n\tprivate message: CompactionSummaryMessage;\n\n\tconstructor(message: CompactionSummaryMessage) {\n\t\tsuper(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\t\tthis.message = message;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tthis.clear();\n\n\t\tconst tokenStr = this.message.tokensBefore.toLocaleString();\n\t\tconst label = theme.fg(\"customMessageLabel\", `\\x1b[1m[compaction]\\x1b[22m`);\n\t\tthis.addChild(new Text(label, 0, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\tif (this.expanded) {\n\t\t\tconst header = `**Compacted from ${tokenStr} tokens**\\n\\n`;\n\t\t\tthis.addChild(\n\t\t\t\tnew Markdown(header + this.message.summary, 0, 0, getMarkdownTheme(), {\n\t\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t\t}),\n\t\t\t);\n\t\t} else {\n\t\t\tthis.addChild(\n\t\t\t\tnew Text(theme.fg(\"customMessageText\", `Compacted from ${tokenStr} tokens (ctrl+o to expand)`), 0, 0),\n\t\t\t);\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import { Box, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
2
+ import { getMarkdownTheme, theme } from "../theme/theme.js";
3
+ /**
4
+ * Component that renders a compaction message with collapsed/expanded state.
5
+ * Uses same background color as hook messages for visual consistency.
6
+ */
7
+ export class CompactionSummaryMessageComponent extends Box {
8
+ expanded = false;
9
+ message;
10
+ constructor(message) {
11
+ super(1, 1, (t) => theme.bg("customMessageBg", t));
12
+ this.message = message;
13
+ this.updateDisplay();
14
+ }
15
+ setExpanded(expanded) {
16
+ this.expanded = expanded;
17
+ this.updateDisplay();
18
+ }
19
+ updateDisplay() {
20
+ this.clear();
21
+ const tokenStr = this.message.tokensBefore.toLocaleString();
22
+ const label = theme.fg("customMessageLabel", `\x1b[1m[compaction]\x1b[22m`);
23
+ this.addChild(new Text(label, 0, 0));
24
+ this.addChild(new Spacer(1));
25
+ if (this.expanded) {
26
+ const header = `**Compacted from ${tokenStr} tokens**\n\n`;
27
+ this.addChild(new Markdown(header + this.message.summary, 0, 0, getMarkdownTheme(), {
28
+ color: (text) => theme.fg("customMessageText", text),
29
+ }));
30
+ }
31
+ else {
32
+ this.addChild(new Text(theme.fg("customMessageText", `Compacted from ${tokenStr} tokens (ctrl+o to expand)`), 0, 0));
33
+ }
34
+ }
35
+ }
36
+ //# sourceMappingURL=compaction-summary-message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compaction-summary-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/compaction-summary-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAEnE,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE5D;;;GAGG;AACH,MAAM,OAAO,iCAAkC,SAAQ,GAAG;IACjD,QAAQ,GAAG,KAAK,CAAC;IACjB,OAAO,CAA2B;IAE1C,YAAY,OAAiC,EAAE;QAC9C,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,CAAC;QACnD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,aAAa,EAAE,CAAC;IAAA,CACrB;IAED,WAAW,CAAC,QAAiB,EAAQ;QACpC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,aAAa,EAAE,CAAC;IAAA,CACrB;IAEO,aAAa,GAAS;QAC7B,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC;QAC5D,MAAM,KAAK,GAAG,KAAK,CAAC,EAAE,CAAC,oBAAoB,EAAE,6BAA6B,CAAC,CAAC;QAC5E,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7B,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,MAAM,MAAM,GAAG,oBAAoB,QAAQ,eAAe,CAAC;YAC3D,IAAI,CAAC,QAAQ,CACZ,IAAI,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,gBAAgB,EAAE,EAAE;gBACrE,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,mBAAmB,EAAE,IAAI,CAAC;aAC5D,CAAC,CACF,CAAC;QACH,CAAC;aAAM,CAAC;YACP,IAAI,CAAC,QAAQ,CACZ,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,mBAAmB,EAAE,kBAAkB,QAAQ,4BAA4B,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CACrG,CAAC;QACH,CAAC;IAAA,CACD;CACD","sourcesContent":["import { Box, Markdown, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport type { CompactionSummaryMessage } from \"../../../core/messages.js\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\n\n/**\n * Component that renders a compaction message with collapsed/expanded state.\n * Uses same background color as hook messages for visual consistency.\n */\nexport class CompactionSummaryMessageComponent extends Box {\n\tprivate expanded = false;\n\tprivate message: CompactionSummaryMessage;\n\n\tconstructor(message: CompactionSummaryMessage) {\n\t\tsuper(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\t\tthis.message = message;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tthis.clear();\n\n\t\tconst tokenStr = this.message.tokensBefore.toLocaleString();\n\t\tconst label = theme.fg(\"customMessageLabel\", `\\x1b[1m[compaction]\\x1b[22m`);\n\t\tthis.addChild(new Text(label, 0, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\tif (this.expanded) {\n\t\t\tconst header = `**Compacted from ${tokenStr} tokens**\\n\\n`;\n\t\t\tthis.addChild(\n\t\t\t\tnew Markdown(header + this.message.summary, 0, 0, getMarkdownTheme(), {\n\t\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t\t}),\n\t\t\t);\n\t\t} else {\n\t\t\tthis.addChild(\n\t\t\t\tnew Text(theme.fg(\"customMessageText\", `Compacted from ${tokenStr} tokens (ctrl+o to expand)`), 0, 0),\n\t\t\t);\n\t\t}\n\t}\n}\n"]}
@@ -1,6 +1,10 @@
1
1
  import type { Component } from "@mariozechner/pi-tui";
2
2
  /**
3
- * Dynamic border component that adjusts to viewport width
3
+ * Dynamic border component that adjusts to viewport width.
4
+ *
5
+ * Note: When used from hooks loaded via jiti, the global `theme` may be undefined
6
+ * because jiti creates a separate module cache. Always pass an explicit color
7
+ * function when using DynamicBorder in components exported for hook use.
4
8
  */
5
9
  export declare class DynamicBorder implements Component {
6
10
  private color;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-border.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/dynamic-border.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGtD;;GAEG;AACH,qBAAa,aAAc,YAAW,SAAS;IAC9C,OAAO,CAAC,KAAK,CAA0B;IAEvC,YAAY,KAAK,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAyC,EAE5E;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9B;CACD","sourcesContent":["import type { Component } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Dynamic border component that adjusts to viewport width\n */\nexport class DynamicBorder implements Component {\n\tprivate color: (str: string) => string;\n\n\tconstructor(color: (str: string) => string = (str) => theme.fg(\"border\", str)) {\n\t\tthis.color = color;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\treturn [this.color(\"─\".repeat(Math.max(1, width)))];\n\t}\n}\n"]}
1
+ {"version":3,"file":"dynamic-border.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/dynamic-border.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGtD;;;;;;GAMG;AACH,qBAAa,aAAc,YAAW,SAAS;IAC9C,OAAO,CAAC,KAAK,CAA0B;IAEvC,YAAY,KAAK,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAyC,EAE5E;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9B;CACD","sourcesContent":["import type { Component } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Dynamic border component that adjusts to viewport width.\n *\n * Note: When used from hooks loaded via jiti, the global `theme` may be undefined\n * because jiti creates a separate module cache. Always pass an explicit color\n * function when using DynamicBorder in components exported for hook use.\n */\nexport class DynamicBorder implements Component {\n\tprivate color: (str: string) => string;\n\n\tconstructor(color: (str: string) => string = (str) => theme.fg(\"border\", str)) {\n\t\tthis.color = color;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\treturn [this.color(\"─\".repeat(Math.max(1, width)))];\n\t}\n}\n"]}
@@ -1,6 +1,10 @@
1
1
  import { theme } from "../theme/theme.js";
2
2
  /**
3
- * Dynamic border component that adjusts to viewport width
3
+ * Dynamic border component that adjusts to viewport width.
4
+ *
5
+ * Note: When used from hooks loaded via jiti, the global `theme` may be undefined
6
+ * because jiti creates a separate module cache. Always pass an explicit color
7
+ * function when using DynamicBorder in components exported for hook use.
4
8
  */
5
9
  export class DynamicBorder {
6
10
  color;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-border.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/dynamic-border.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C;;GAEG;AACH,MAAM,OAAO,aAAa;IACjB,KAAK,CAA0B;IAEvC,YAAY,KAAK,GAA4B,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE;QAC9E,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IAAA,CACnB;IAED,UAAU,GAAS;QAClB,0CAA0C;IADvB,CAEnB;IAED,MAAM,CAAC,KAAa,EAAY;QAC/B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAAA,CACpD;CACD","sourcesContent":["import type { Component } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Dynamic border component that adjusts to viewport width\n */\nexport class DynamicBorder implements Component {\n\tprivate color: (str: string) => string;\n\n\tconstructor(color: (str: string) => string = (str) => theme.fg(\"border\", str)) {\n\t\tthis.color = color;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\treturn [this.color(\"─\".repeat(Math.max(1, width)))];\n\t}\n}\n"]}
1
+ {"version":3,"file":"dynamic-border.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/dynamic-border.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,OAAO,aAAa;IACjB,KAAK,CAA0B;IAEvC,YAAY,KAAK,GAA4B,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE;QAC9E,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IAAA,CACnB;IAED,UAAU,GAAS;QAClB,0CAA0C;IADvB,CAEnB;IAED,MAAM,CAAC,KAAa,EAAY;QAC/B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAAA,CACpD;CACD","sourcesContent":["import type { Component } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Dynamic border component that adjusts to viewport width.\n *\n * Note: When used from hooks loaded via jiti, the global `theme` may be undefined\n * because jiti creates a separate module cache. Always pass an explicit color\n * function when using DynamicBorder in components exported for hook use.\n */\nexport class DynamicBorder implements Component {\n\tprivate color: (str: string) => string;\n\n\tconstructor(color: (str: string) => string = (str) => theme.fg(\"border\", str)) {\n\t\tthis.color = color;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\treturn [this.color(\"─\".repeat(Math.max(1, width)))];\n\t}\n}\n"]}
@@ -1,18 +1,25 @@
1
- import type { AgentState } from "@mariozechner/pi-agent-core";
2
1
  import { type Component } from "@mariozechner/pi-tui";
3
- import type { ModelRegistry } from "../../../core/model-registry.js";
2
+ import type { AgentSession } from "../../../core/agent-session.js";
4
3
  /**
5
4
  * Footer component that shows pwd, token stats, and context usage
6
5
  */
7
6
  export declare class FooterComponent implements Component {
8
- private state;
9
- private modelRegistry;
7
+ private session;
10
8
  private cachedBranch;
11
9
  private gitWatcher;
12
10
  private onBranchChange;
13
11
  private autoCompactEnabled;
14
- constructor(state: AgentState, modelRegistry: ModelRegistry);
12
+ private hookStatuses;
13
+ constructor(session: AgentSession);
15
14
  setAutoCompactEnabled(enabled: boolean): void;
15
+ /**
16
+ * Set hook status text to display in the footer.
17
+ * Text is sanitized (newlines/tabs replaced with spaces) and truncated to terminal width.
18
+ * ANSI escape codes for styling are preserved.
19
+ * @param key - Unique key to identify this status
20
+ * @param text - Status text, or undefined to clear
21
+ */
22
+ setHookStatus(key: string, text: string | undefined): void;
16
23
  /**
17
24
  * Set up a file watcher on .git/HEAD to detect branch changes.
18
25
  * Call the provided callback when branch changes.
@@ -23,7 +30,6 @@ export declare class FooterComponent implements Component {
23
30
  * Clean up the file watcher
24
31
  */
25
32
  dispose(): void;
26
- updateState(state: AgentState): void;
27
33
  invalidate(): void;
28
34
  /**
29
35
  * Get current git branch by reading .git/HEAD directly.
@@ -1 +1 @@
1
- {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,sBAAsB,CAAC;AAGpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAuBrE;;GAEG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,YAAY,CAAwC;IAC5D,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,kBAAkB,CAAiB;IAE3C,YAAY,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAG1D;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,WAAW,CAAC,cAAc,EAAE,MAAM,IAAI,GAAG,IAAI,CAG5C;IAED,OAAO,CAAC,eAAe;IAwBvB;;OAEG;IACH,OAAO,IAAI,IAAI,CAKd;IAED,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAEnC;IAED,UAAU,IAAI,IAAI,CAGjB;IAED;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA6BxB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA2J9B;CACD","sourcesContent":["import type { AgentState } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage } from \"@mariozechner/pi-ai\";\nimport { type Component, visibleWidth } from \"@mariozechner/pi-tui\";\nimport { existsSync, type FSWatcher, readFileSync, watch } from \"fs\";\nimport { dirname, join } from \"path\";\nimport type { ModelRegistry } from \"../../../core/model-registry.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Find the git root directory by walking up from cwd.\n * Returns the path to .git/HEAD if found, null otherwise.\n */\nfunction findGitHeadPath(): string | null {\n\tlet dir = process.cwd();\n\twhile (true) {\n\t\tconst gitHeadPath = join(dir, \".git\", \"HEAD\");\n\t\tif (existsSync(gitHeadPath)) {\n\t\t\treturn gitHeadPath;\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) {\n\t\t\t// Reached filesystem root\n\t\t\treturn null;\n\t\t}\n\t\tdir = parent;\n\t}\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage\n */\nexport class FooterComponent implements Component {\n\tprivate state: AgentState;\n\tprivate modelRegistry: ModelRegistry;\n\tprivate cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name\n\tprivate gitWatcher: FSWatcher | null = null;\n\tprivate onBranchChange: (() => void) | null = null;\n\tprivate autoCompactEnabled: boolean = true;\n\n\tconstructor(state: AgentState, modelRegistry: ModelRegistry) {\n\t\tthis.state = state;\n\t\tthis.modelRegistry = modelRegistry;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * Set up a file watcher on .git/HEAD to detect branch changes.\n\t * Call the provided callback when branch changes.\n\t */\n\twatchBranch(onBranchChange: () => void): void {\n\t\tthis.onBranchChange = onBranchChange;\n\t\tthis.setupGitWatcher();\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\t// Clean up existing watcher\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\n\t\tconst gitHeadPath = findGitHeadPath();\n\t\tif (!gitHeadPath) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.gitWatcher = watch(gitHeadPath, () => {\n\t\t\t\tthis.cachedBranch = undefined; // Invalidate cache\n\t\t\t\tif (this.onBranchChange) {\n\t\t\t\t\tthis.onBranchChange();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\t}\n\n\t/**\n\t * Clean up the file watcher\n\t */\n\tdispose(): void {\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\t}\n\n\tupdateState(state: AgentState): void {\n\t\tthis.state = state;\n\t}\n\n\tinvalidate(): void {\n\t\t// Invalidate cached branch so it gets re-read on next render\n\t\tthis.cachedBranch = undefined;\n\t}\n\n\t/**\n\t * Get current git branch by reading .git/HEAD directly.\n\t * Returns null if not in a git repo, branch name otherwise.\n\t */\n\tprivate getCurrentBranch(): string | null {\n\t\t// Return cached value if available\n\t\tif (this.cachedBranch !== undefined) {\n\t\t\treturn this.cachedBranch;\n\t\t}\n\n\t\ttry {\n\t\t\tconst gitHeadPath = findGitHeadPath();\n\t\t\tif (!gitHeadPath) {\n\t\t\t\tthis.cachedBranch = null;\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst content = readFileSync(gitHeadPath, \"utf8\").trim();\n\n\t\t\tif (content.startsWith(\"ref: refs/heads/\")) {\n\t\t\t\t// Normal branch: extract branch name\n\t\t\t\tthis.cachedBranch = content.slice(16);\n\t\t\t} else {\n\t\t\t\t// Detached HEAD state\n\t\t\t\tthis.cachedBranch = \"detached\";\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not in a git repo or error reading file\n\t\t\tthis.cachedBranch = null;\n\t\t}\n\n\t\treturn this.cachedBranch;\n\t}\n\n\trender(width: number): string[] {\n\t\t// Calculate cumulative usage from all assistant messages\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of this.state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Get last assistant message for context percentage calculation (skip aborted messages)\n\t\tconst lastAssistantMessage = this.state.messages\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as AssistantMessage | undefined;\n\n\t\t// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)\n\t\tconst contextTokens = lastAssistantMessage\n\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t: 0;\n\t\tconst contextWindow = this.state.model?.contextWindow || 0;\n\t\tconst contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;\n\t\tconst contextPercent = contextPercentValue.toFixed(1);\n\n\t\t// Format token counts (similar to web-ui)\n\t\tconst formatTokens = (count: number): string => {\n\t\t\tif (count < 1000) return count.toString();\n\t\t\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\t\t\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\t\t\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\t\t\treturn `${Math.round(count / 1000000)}M`;\n\t\t};\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.getCurrentBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Truncate path if too long to fit width\n\t\tif (pwd.length > width) {\n\t\t\tconst half = Math.floor(width / 2) - 2;\n\t\t\tif (half > 0) {\n\t\t\t\tconst start = pwd.slice(0, half);\n\t\t\t\tconst end = pwd.slice(-(half - 1));\n\t\t\t\tpwd = `${start}...${end}`;\n\t\t\t} else {\n\t\t\t\tpwd = pwd.slice(0, Math.max(1, width));\n\t\t\t}\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = this.state.model ? this.modelRegistry.isUsingOAuth(this.state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay = `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = this.state.model?.id || \"no-model\";\n\n\t\t// Add thinking level hint if model supports reasoning and thinking is enabled\n\t\tlet rightSide = modelName;\n\t\tif (this.state.model?.reasoning) {\n\t\t\tconst thinkingLevel = this.state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel !== \"off\") {\n\t\t\t\trightSide = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\t// Truncate statsLeft to fit width (no room for right side)\n\t\t\tconst plainStatsLeft = statsLeft.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\tstatsLeft = `${plainStatsLeft.substring(0, width - 3)}...`;\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 3) {\n\t\t\t\t// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)\n\t\t\t\tconst plainRightSide = rightSide.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\t\tconst truncatedPlain = plainRightSide.substring(0, availableForRight);\n\t\t\t\t// For simplicity, just use plain truncated version (loses color, but fits)\n\t\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - truncatedPlain.length);\n\t\t\t\tstatsLine = statsLeft + padding + truncatedPlain;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\treturn [theme.fg(\"dim\", pwd), dimStatsLeft + dimRemainder];\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,sBAAsB,CAAC;AAGrF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAmCnE;;GAEG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,YAAY,CAAwC;IAC5D,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,kBAAkB,CAAiB;IAC3C,OAAO,CAAC,YAAY,CAAkC;IAEtD,YAAY,OAAO,EAAE,YAAY,EAEhC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;;;;OAMG;IACH,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAMzD;IAED;;;OAGG;IACH,WAAW,CAAC,cAAc,EAAE,MAAM,IAAI,GAAG,IAAI,CAG5C;IAED,OAAO,CAAC,eAAe;IAwBvB;;OAEG;IACH,OAAO,IAAI,IAAI,CAKd;IAED,UAAU,IAAI,IAAI,CAGjB;IAED;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA6BxB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAwK9B;CACD","sourcesContent":["import type { AssistantMessage } from \"@mariozechner/pi-ai\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@mariozechner/pi-tui\";\nimport { existsSync, type FSWatcher, readFileSync, watch } from \"fs\";\nimport { dirname, join } from \"path\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Find the git root directory by walking up from cwd.\n * Returns the path to .git/HEAD if found, null otherwise.\n */\nfunction findGitHeadPath(): string | null {\n\tlet dir = process.cwd();\n\twhile (true) {\n\t\tconst gitHeadPath = join(dir, \".git\", \"HEAD\");\n\t\tif (existsSync(gitHeadPath)) {\n\t\t\treturn gitHeadPath;\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) {\n\t\t\t// Reached filesystem root\n\t\t\treturn null;\n\t\t}\n\t\tdir = parent;\n\t}\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage\n */\nexport class FooterComponent implements Component {\n\tprivate session: AgentSession;\n\tprivate cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name\n\tprivate gitWatcher: FSWatcher | null = null;\n\tprivate onBranchChange: (() => void) | null = null;\n\tprivate autoCompactEnabled: boolean = true;\n\tprivate hookStatuses: Map<string, string> = new Map();\n\n\tconstructor(session: AgentSession) {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * Set hook status text to display in the footer.\n\t * Text is sanitized (newlines/tabs replaced with spaces) and truncated to terminal width.\n\t * ANSI escape codes for styling are preserved.\n\t * @param key - Unique key to identify this status\n\t * @param text - Status text, or undefined to clear\n\t */\n\tsetHookStatus(key: string, text: string | undefined): void {\n\t\tif (text === undefined) {\n\t\t\tthis.hookStatuses.delete(key);\n\t\t} else {\n\t\t\tthis.hookStatuses.set(key, text);\n\t\t}\n\t}\n\n\t/**\n\t * Set up a file watcher on .git/HEAD to detect branch changes.\n\t * Call the provided callback when branch changes.\n\t */\n\twatchBranch(onBranchChange: () => void): void {\n\t\tthis.onBranchChange = onBranchChange;\n\t\tthis.setupGitWatcher();\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\t// Clean up existing watcher\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\n\t\tconst gitHeadPath = findGitHeadPath();\n\t\tif (!gitHeadPath) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.gitWatcher = watch(gitHeadPath, () => {\n\t\t\t\tthis.cachedBranch = undefined; // Invalidate cache\n\t\t\t\tif (this.onBranchChange) {\n\t\t\t\t\tthis.onBranchChange();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\t}\n\n\t/**\n\t * Clean up the file watcher\n\t */\n\tdispose(): void {\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\t}\n\n\tinvalidate(): void {\n\t\t// Invalidate cached branch so it gets re-read on next render\n\t\tthis.cachedBranch = undefined;\n\t}\n\n\t/**\n\t * Get current git branch by reading .git/HEAD directly.\n\t * Returns null if not in a git repo, branch name otherwise.\n\t */\n\tprivate getCurrentBranch(): string | null {\n\t\t// Return cached value if available\n\t\tif (this.cachedBranch !== undefined) {\n\t\t\treturn this.cachedBranch;\n\t\t}\n\n\t\ttry {\n\t\t\tconst gitHeadPath = findGitHeadPath();\n\t\t\tif (!gitHeadPath) {\n\t\t\t\tthis.cachedBranch = null;\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst content = readFileSync(gitHeadPath, \"utf8\").trim();\n\n\t\t\tif (content.startsWith(\"ref: refs/heads/\")) {\n\t\t\t\t// Normal branch: extract branch name\n\t\t\t\tthis.cachedBranch = content.slice(16);\n\t\t\t} else {\n\t\t\t\t// Detached HEAD state\n\t\t\t\tthis.cachedBranch = \"detached\";\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not in a git repo or error reading file\n\t\t\tthis.cachedBranch = null;\n\t\t}\n\n\t\treturn this.cachedBranch;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Get last assistant message for context percentage calculation (skip aborted messages)\n\t\tconst lastAssistantMessage = state.messages\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as AssistantMessage | undefined;\n\n\t\t// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)\n\t\tconst contextTokens = lastAssistantMessage\n\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t: 0;\n\t\tconst contextWindow = state.model?.contextWindow || 0;\n\t\tconst contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;\n\t\tconst contextPercent = contextPercentValue.toFixed(1);\n\n\t\t// Format token counts (similar to web-ui)\n\t\tconst formatTokens = (count: number): string => {\n\t\t\tif (count < 1000) return count.toString();\n\t\t\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\t\t\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\t\t\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\t\t\treturn `${Math.round(count / 1000000)}M`;\n\t\t};\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.getCurrentBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Truncate path if too long to fit width\n\t\tif (pwd.length > width) {\n\t\t\tconst half = Math.floor(width / 2) - 2;\n\t\t\tif (half > 0) {\n\t\t\t\tconst start = pwd.slice(0, half);\n\t\t\t\tconst end = pwd.slice(-(half - 1));\n\t\t\t\tpwd = `${start}...${end}`;\n\t\t\t} else {\n\t\t\t\tpwd = pwd.slice(0, Math.max(1, width));\n\t\t\t}\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay = `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\t// Add thinking level hint if model supports reasoning and thinking is enabled\n\t\tlet rightSide = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel !== \"off\") {\n\t\t\t\trightSide = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\t// Truncate statsLeft to fit width (no room for right side)\n\t\t\tconst plainStatsLeft = statsLeft.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\tstatsLeft = `${plainStatsLeft.substring(0, width - 3)}...`;\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 3) {\n\t\t\t\t// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)\n\t\t\t\tconst plainRightSide = rightSide.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\t\tconst truncatedPlain = plainRightSide.substring(0, availableForRight);\n\t\t\t\t// For simplicity, just use plain truncated version (loses color, but fits)\n\t\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - truncatedPlain.length);\n\t\t\t\tstatsLine = statsLeft + padding + truncatedPlain;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst lines = [theme.fg(\"dim\", pwd), dimStatsLeft + dimRemainder];\n\n\t\t// Add hook statuses on a single line, sorted by key alphabetically\n\t\tif (this.hookStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(this.hookStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
@@ -1,7 +1,18 @@
1
- import { visibleWidth } from "@mariozechner/pi-tui";
1
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
2
2
  import { existsSync, readFileSync, watch } from "fs";
3
3
  import { dirname, join } from "path";
4
4
  import { theme } from "../theme/theme.js";
5
+ /**
6
+ * Sanitize text for display in a single-line status.
7
+ * Removes newlines, tabs, carriage returns, and other control characters.
8
+ */
9
+ function sanitizeStatusText(text) {
10
+ // Replace newlines, tabs, carriage returns with space, then collapse multiple spaces
11
+ return text
12
+ .replace(/[\r\n\t]/g, " ")
13
+ .replace(/ +/g, " ")
14
+ .trim();
15
+ }
5
16
  /**
6
17
  * Find the git root directory by walking up from cwd.
7
18
  * Returns the path to .git/HEAD if found, null otherwise.
@@ -25,19 +36,33 @@ function findGitHeadPath() {
25
36
  * Footer component that shows pwd, token stats, and context usage
26
37
  */
27
38
  export class FooterComponent {
28
- state;
29
- modelRegistry;
39
+ session;
30
40
  cachedBranch = undefined; // undefined = not checked yet, null = not in git repo, string = branch name
31
41
  gitWatcher = null;
32
42
  onBranchChange = null;
33
43
  autoCompactEnabled = true;
34
- constructor(state, modelRegistry) {
35
- this.state = state;
36
- this.modelRegistry = modelRegistry;
44
+ hookStatuses = new Map();
45
+ constructor(session) {
46
+ this.session = session;
37
47
  }
38
48
  setAutoCompactEnabled(enabled) {
39
49
  this.autoCompactEnabled = enabled;
40
50
  }
51
+ /**
52
+ * Set hook status text to display in the footer.
53
+ * Text is sanitized (newlines/tabs replaced with spaces) and truncated to terminal width.
54
+ * ANSI escape codes for styling are preserved.
55
+ * @param key - Unique key to identify this status
56
+ * @param text - Status text, or undefined to clear
57
+ */
58
+ setHookStatus(key, text) {
59
+ if (text === undefined) {
60
+ this.hookStatuses.delete(key);
61
+ }
62
+ else {
63
+ this.hookStatuses.set(key, text);
64
+ }
65
+ }
41
66
  /**
42
67
  * Set up a file watcher on .git/HEAD to detect branch changes.
43
68
  * Call the provided callback when branch changes.
@@ -77,9 +102,6 @@ export class FooterComponent {
77
102
  this.gitWatcher = null;
78
103
  }
79
104
  }
80
- updateState(state) {
81
- this.state = state;
82
- }
83
105
  invalidate() {
84
106
  // Invalidate cached branch so it gets re-read on next render
85
107
  this.cachedBranch = undefined;
@@ -116,24 +138,24 @@ export class FooterComponent {
116
138
  return this.cachedBranch;
117
139
  }
118
140
  render(width) {
119
- // Calculate cumulative usage from all assistant messages
141
+ const state = this.session.state;
142
+ // Calculate cumulative usage from ALL session entries (not just post-compaction messages)
120
143
  let totalInput = 0;
121
144
  let totalOutput = 0;
122
145
  let totalCacheRead = 0;
123
146
  let totalCacheWrite = 0;
124
147
  let totalCost = 0;
125
- for (const message of this.state.messages) {
126
- if (message.role === "assistant") {
127
- const assistantMsg = message;
128
- totalInput += assistantMsg.usage.input;
129
- totalOutput += assistantMsg.usage.output;
130
- totalCacheRead += assistantMsg.usage.cacheRead;
131
- totalCacheWrite += assistantMsg.usage.cacheWrite;
132
- totalCost += assistantMsg.usage.cost.total;
148
+ for (const entry of this.session.sessionManager.getEntries()) {
149
+ if (entry.type === "message" && entry.message.role === "assistant") {
150
+ totalInput += entry.message.usage.input;
151
+ totalOutput += entry.message.usage.output;
152
+ totalCacheRead += entry.message.usage.cacheRead;
153
+ totalCacheWrite += entry.message.usage.cacheWrite;
154
+ totalCost += entry.message.usage.cost.total;
133
155
  }
134
156
  }
135
157
  // Get last assistant message for context percentage calculation (skip aborted messages)
136
- const lastAssistantMessage = this.state.messages
158
+ const lastAssistantMessage = state.messages
137
159
  .slice()
138
160
  .reverse()
139
161
  .find((m) => m.role === "assistant" && m.stopReason !== "aborted");
@@ -144,7 +166,7 @@ export class FooterComponent {
144
166
  lastAssistantMessage.usage.cacheRead +
145
167
  lastAssistantMessage.usage.cacheWrite
146
168
  : 0;
147
- const contextWindow = this.state.model?.contextWindow || 0;
169
+ const contextWindow = state.model?.contextWindow || 0;
148
170
  const contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
149
171
  const contextPercent = contextPercentValue.toFixed(1);
150
172
  // Format token counts (similar to web-ui)
@@ -193,7 +215,7 @@ export class FooterComponent {
193
215
  if (totalCacheWrite)
194
216
  statsParts.push(`W${formatTokens(totalCacheWrite)}`);
195
217
  // Show cost with "(sub)" indicator if using OAuth subscription
196
- const usingSubscription = this.state.model ? this.modelRegistry.isUsingOAuth(this.state.model) : false;
218
+ const usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;
197
219
  if (totalCost || usingSubscription) {
198
220
  const costStr = `$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
199
221
  statsParts.push(costStr);
@@ -214,11 +236,11 @@ export class FooterComponent {
214
236
  statsParts.push(contextPercentStr);
215
237
  let statsLeft = statsParts.join(" ");
216
238
  // Add model name on the right side, plus thinking level if model supports it
217
- const modelName = this.state.model?.id || "no-model";
239
+ const modelName = state.model?.id || "no-model";
218
240
  // Add thinking level hint if model supports reasoning and thinking is enabled
219
241
  let rightSide = modelName;
220
- if (this.state.model?.reasoning) {
221
- const thinkingLevel = this.state.thinkingLevel || "off";
242
+ if (state.model?.reasoning) {
243
+ const thinkingLevel = state.thinkingLevel || "off";
222
244
  if (thinkingLevel !== "off") {
223
245
  rightSide = `${modelName} • ${thinkingLevel}`;
224
246
  }
@@ -263,7 +285,17 @@ export class FooterComponent {
263
285
  const dimStatsLeft = theme.fg("dim", statsLeft);
264
286
  const remainder = statsLine.slice(statsLeft.length); // padding + rightSide
265
287
  const dimRemainder = theme.fg("dim", remainder);
266
- return [theme.fg("dim", pwd), dimStatsLeft + dimRemainder];
288
+ const lines = [theme.fg("dim", pwd), dimStatsLeft + dimRemainder];
289
+ // Add hook statuses on a single line, sorted by key alphabetically
290
+ if (this.hookStatuses.size > 0) {
291
+ const sortedStatuses = Array.from(this.hookStatuses.entries())
292
+ .sort(([a], [b]) => a.localeCompare(b))
293
+ .map(([, text]) => sanitizeStatusText(text));
294
+ const statusLine = sortedStatuses.join(" ");
295
+ // Truncate to terminal width with dim ellipsis for consistency with footer style
296
+ lines.push(truncateToWidth(statusLine, width, theme.fg("dim", "...")));
297
+ }
298
+ return lines;
267
299
  }
268
300
  }
269
301
  //# sourceMappingURL=footer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentState } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage } from \"@mariozechner/pi-ai\";\nimport { type Component, visibleWidth } from \"@mariozechner/pi-tui\";\nimport { existsSync, type FSWatcher, readFileSync, watch } from \"fs\";\nimport { dirname, join } from \"path\";\nimport type { ModelRegistry } from \"../../../core/model-registry.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Find the git root directory by walking up from cwd.\n * Returns the path to .git/HEAD if found, null otherwise.\n */\nfunction findGitHeadPath(): string | null {\n\tlet dir = process.cwd();\n\twhile (true) {\n\t\tconst gitHeadPath = join(dir, \".git\", \"HEAD\");\n\t\tif (existsSync(gitHeadPath)) {\n\t\t\treturn gitHeadPath;\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) {\n\t\t\t// Reached filesystem root\n\t\t\treturn null;\n\t\t}\n\t\tdir = parent;\n\t}\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage\n */\nexport class FooterComponent implements Component {\n\tprivate state: AgentState;\n\tprivate modelRegistry: ModelRegistry;\n\tprivate cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name\n\tprivate gitWatcher: FSWatcher | null = null;\n\tprivate onBranchChange: (() => void) | null = null;\n\tprivate autoCompactEnabled: boolean = true;\n\n\tconstructor(state: AgentState, modelRegistry: ModelRegistry) {\n\t\tthis.state = state;\n\t\tthis.modelRegistry = modelRegistry;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * Set up a file watcher on .git/HEAD to detect branch changes.\n\t * Call the provided callback when branch changes.\n\t */\n\twatchBranch(onBranchChange: () => void): void {\n\t\tthis.onBranchChange = onBranchChange;\n\t\tthis.setupGitWatcher();\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\t// Clean up existing watcher\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\n\t\tconst gitHeadPath = findGitHeadPath();\n\t\tif (!gitHeadPath) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.gitWatcher = watch(gitHeadPath, () => {\n\t\t\t\tthis.cachedBranch = undefined; // Invalidate cache\n\t\t\t\tif (this.onBranchChange) {\n\t\t\t\t\tthis.onBranchChange();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\t}\n\n\t/**\n\t * Clean up the file watcher\n\t */\n\tdispose(): void {\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\t}\n\n\tupdateState(state: AgentState): void {\n\t\tthis.state = state;\n\t}\n\n\tinvalidate(): void {\n\t\t// Invalidate cached branch so it gets re-read on next render\n\t\tthis.cachedBranch = undefined;\n\t}\n\n\t/**\n\t * Get current git branch by reading .git/HEAD directly.\n\t * Returns null if not in a git repo, branch name otherwise.\n\t */\n\tprivate getCurrentBranch(): string | null {\n\t\t// Return cached value if available\n\t\tif (this.cachedBranch !== undefined) {\n\t\t\treturn this.cachedBranch;\n\t\t}\n\n\t\ttry {\n\t\t\tconst gitHeadPath = findGitHeadPath();\n\t\t\tif (!gitHeadPath) {\n\t\t\t\tthis.cachedBranch = null;\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst content = readFileSync(gitHeadPath, \"utf8\").trim();\n\n\t\t\tif (content.startsWith(\"ref: refs/heads/\")) {\n\t\t\t\t// Normal branch: extract branch name\n\t\t\t\tthis.cachedBranch = content.slice(16);\n\t\t\t} else {\n\t\t\t\t// Detached HEAD state\n\t\t\t\tthis.cachedBranch = \"detached\";\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not in a git repo or error reading file\n\t\t\tthis.cachedBranch = null;\n\t\t}\n\n\t\treturn this.cachedBranch;\n\t}\n\n\trender(width: number): string[] {\n\t\t// Calculate cumulative usage from all assistant messages\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of this.state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Get last assistant message for context percentage calculation (skip aborted messages)\n\t\tconst lastAssistantMessage = this.state.messages\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as AssistantMessage | undefined;\n\n\t\t// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)\n\t\tconst contextTokens = lastAssistantMessage\n\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t: 0;\n\t\tconst contextWindow = this.state.model?.contextWindow || 0;\n\t\tconst contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;\n\t\tconst contextPercent = contextPercentValue.toFixed(1);\n\n\t\t// Format token counts (similar to web-ui)\n\t\tconst formatTokens = (count: number): string => {\n\t\t\tif (count < 1000) return count.toString();\n\t\t\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\t\t\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\t\t\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\t\t\treturn `${Math.round(count / 1000000)}M`;\n\t\t};\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.getCurrentBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Truncate path if too long to fit width\n\t\tif (pwd.length > width) {\n\t\t\tconst half = Math.floor(width / 2) - 2;\n\t\t\tif (half > 0) {\n\t\t\t\tconst start = pwd.slice(0, half);\n\t\t\t\tconst end = pwd.slice(-(half - 1));\n\t\t\t\tpwd = `${start}...${end}`;\n\t\t\t} else {\n\t\t\t\tpwd = pwd.slice(0, Math.max(1, width));\n\t\t\t}\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = this.state.model ? this.modelRegistry.isUsingOAuth(this.state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay = `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = this.state.model?.id || \"no-model\";\n\n\t\t// Add thinking level hint if model supports reasoning and thinking is enabled\n\t\tlet rightSide = modelName;\n\t\tif (this.state.model?.reasoning) {\n\t\t\tconst thinkingLevel = this.state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel !== \"off\") {\n\t\t\t\trightSide = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\t// Truncate statsLeft to fit width (no room for right side)\n\t\t\tconst plainStatsLeft = statsLeft.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\tstatsLeft = `${plainStatsLeft.substring(0, width - 3)}...`;\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 3) {\n\t\t\t\t// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)\n\t\t\t\tconst plainRightSide = rightSide.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\t\tconst truncatedPlain = plainRightSide.substring(0, availableForRight);\n\t\t\t\t// For simplicity, just use plain truncated version (loses color, but fits)\n\t\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - truncatedPlain.length);\n\t\t\t\tstatsLine = statsLeft + padding + truncatedPlain;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\treturn [theme.fg(\"dim\", pwd), dimStatsLeft + dimRemainder];\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage } from \"@mariozechner/pi-ai\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@mariozechner/pi-tui\";\nimport { existsSync, type FSWatcher, readFileSync, watch } from \"fs\";\nimport { dirname, join } from \"path\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Find the git root directory by walking up from cwd.\n * Returns the path to .git/HEAD if found, null otherwise.\n */\nfunction findGitHeadPath(): string | null {\n\tlet dir = process.cwd();\n\twhile (true) {\n\t\tconst gitHeadPath = join(dir, \".git\", \"HEAD\");\n\t\tif (existsSync(gitHeadPath)) {\n\t\t\treturn gitHeadPath;\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) {\n\t\t\t// Reached filesystem root\n\t\t\treturn null;\n\t\t}\n\t\tdir = parent;\n\t}\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage\n */\nexport class FooterComponent implements Component {\n\tprivate session: AgentSession;\n\tprivate cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name\n\tprivate gitWatcher: FSWatcher | null = null;\n\tprivate onBranchChange: (() => void) | null = null;\n\tprivate autoCompactEnabled: boolean = true;\n\tprivate hookStatuses: Map<string, string> = new Map();\n\n\tconstructor(session: AgentSession) {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * Set hook status text to display in the footer.\n\t * Text is sanitized (newlines/tabs replaced with spaces) and truncated to terminal width.\n\t * ANSI escape codes for styling are preserved.\n\t * @param key - Unique key to identify this status\n\t * @param text - Status text, or undefined to clear\n\t */\n\tsetHookStatus(key: string, text: string | undefined): void {\n\t\tif (text === undefined) {\n\t\t\tthis.hookStatuses.delete(key);\n\t\t} else {\n\t\t\tthis.hookStatuses.set(key, text);\n\t\t}\n\t}\n\n\t/**\n\t * Set up a file watcher on .git/HEAD to detect branch changes.\n\t * Call the provided callback when branch changes.\n\t */\n\twatchBranch(onBranchChange: () => void): void {\n\t\tthis.onBranchChange = onBranchChange;\n\t\tthis.setupGitWatcher();\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\t// Clean up existing watcher\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\n\t\tconst gitHeadPath = findGitHeadPath();\n\t\tif (!gitHeadPath) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.gitWatcher = watch(gitHeadPath, () => {\n\t\t\t\tthis.cachedBranch = undefined; // Invalidate cache\n\t\t\t\tif (this.onBranchChange) {\n\t\t\t\t\tthis.onBranchChange();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\t}\n\n\t/**\n\t * Clean up the file watcher\n\t */\n\tdispose(): void {\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\t}\n\n\tinvalidate(): void {\n\t\t// Invalidate cached branch so it gets re-read on next render\n\t\tthis.cachedBranch = undefined;\n\t}\n\n\t/**\n\t * Get current git branch by reading .git/HEAD directly.\n\t * Returns null if not in a git repo, branch name otherwise.\n\t */\n\tprivate getCurrentBranch(): string | null {\n\t\t// Return cached value if available\n\t\tif (this.cachedBranch !== undefined) {\n\t\t\treturn this.cachedBranch;\n\t\t}\n\n\t\ttry {\n\t\t\tconst gitHeadPath = findGitHeadPath();\n\t\t\tif (!gitHeadPath) {\n\t\t\t\tthis.cachedBranch = null;\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst content = readFileSync(gitHeadPath, \"utf8\").trim();\n\n\t\t\tif (content.startsWith(\"ref: refs/heads/\")) {\n\t\t\t\t// Normal branch: extract branch name\n\t\t\t\tthis.cachedBranch = content.slice(16);\n\t\t\t} else {\n\t\t\t\t// Detached HEAD state\n\t\t\t\tthis.cachedBranch = \"detached\";\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not in a git repo or error reading file\n\t\t\tthis.cachedBranch = null;\n\t\t}\n\n\t\treturn this.cachedBranch;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Get last assistant message for context percentage calculation (skip aborted messages)\n\t\tconst lastAssistantMessage = state.messages\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as AssistantMessage | undefined;\n\n\t\t// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)\n\t\tconst contextTokens = lastAssistantMessage\n\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t: 0;\n\t\tconst contextWindow = state.model?.contextWindow || 0;\n\t\tconst contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;\n\t\tconst contextPercent = contextPercentValue.toFixed(1);\n\n\t\t// Format token counts (similar to web-ui)\n\t\tconst formatTokens = (count: number): string => {\n\t\t\tif (count < 1000) return count.toString();\n\t\t\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\t\t\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\t\t\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\t\t\treturn `${Math.round(count / 1000000)}M`;\n\t\t};\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.getCurrentBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Truncate path if too long to fit width\n\t\tif (pwd.length > width) {\n\t\t\tconst half = Math.floor(width / 2) - 2;\n\t\t\tif (half > 0) {\n\t\t\t\tconst start = pwd.slice(0, half);\n\t\t\t\tconst end = pwd.slice(-(half - 1));\n\t\t\t\tpwd = `${start}...${end}`;\n\t\t\t} else {\n\t\t\t\tpwd = pwd.slice(0, Math.max(1, width));\n\t\t\t}\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay = `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\t// Add thinking level hint if model supports reasoning and thinking is enabled\n\t\tlet rightSide = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel !== \"off\") {\n\t\t\t\trightSide = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\t// Truncate statsLeft to fit width (no room for right side)\n\t\t\tconst plainStatsLeft = statsLeft.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\tstatsLeft = `${plainStatsLeft.substring(0, width - 3)}...`;\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 3) {\n\t\t\t\t// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)\n\t\t\t\tconst plainRightSide = rightSide.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\t\tconst truncatedPlain = plainRightSide.substring(0, availableForRight);\n\t\t\t\t// For simplicity, just use plain truncated version (loses color, but fits)\n\t\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - truncatedPlain.length);\n\t\t\t\tstatsLine = statsLeft + padding + truncatedPlain;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst lines = [theme.fg(\"dim\", pwd), dimStatsLeft + dimRemainder];\n\n\t\t// Add hook statuses on a single line, sorted by key alphabetically\n\t\tif (this.hookStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(this.hookStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Multi-line editor component for hooks.
3
+ * Supports Ctrl+G for external editor.
4
+ */
5
+ import { Container, type TUI } from "@mariozechner/pi-tui";
6
+ export declare class HookEditorComponent extends Container {
7
+ private editor;
8
+ private onSubmitCallback;
9
+ private onCancelCallback;
10
+ private tui;
11
+ constructor(tui: TUI, title: string, prefill: string | undefined, onSubmit: (value: string) => void, onCancel: () => void);
12
+ handleInput(keyData: string): void;
13
+ private openExternalEditor;
14
+ }
15
+ //# sourceMappingURL=hook-editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-editor.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/hook-editor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,SAAS,EAA2C,KAAK,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAIpG,qBAAa,mBAAoB,SAAQ,SAAS;IACjD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAA0B;IAClD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,GAAG,CAAM;IAEjB,YACC,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EACjC,QAAQ,EAAE,MAAM,IAAI,EAoCpB;IAED,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAqBjC;IAED,OAAO,CAAC,kBAAkB;CAgC1B","sourcesContent":["/**\n * Multi-line editor component for hooks.\n * Supports Ctrl+G for external editor.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { Container, Editor, isCtrlG, isEscape, Spacer, Text, type TUI } from \"@mariozechner/pi-tui\";\nimport { getEditorTheme, theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\nexport class HookEditorComponent extends Container {\n\tprivate editor: Editor;\n\tprivate onSubmitCallback: (value: string) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate tui: TUI;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\ttitle: string,\n\t\tprefill: string | undefined,\n\t\tonSubmit: (value: string) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\n\t\tthis.tui = tui;\n\t\tthis.onSubmitCallback = onSubmit;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tthis.addChild(new Text(theme.fg(\"accent\", title), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create editor\n\t\tthis.editor = new Editor(getEditorTheme());\n\t\tif (prefill) {\n\t\t\tthis.editor.setText(prefill);\n\t\t}\n\t\tthis.addChild(this.editor);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add hint\n\t\tconst hasExternalEditor = !!(process.env.VISUAL || process.env.EDITOR);\n\t\tconst hint = hasExternalEditor\n\t\t\t? \"ctrl+enter submit esc cancel ctrl+g external editor\"\n\t\t\t: \"ctrl+enter submit esc cancel\";\n\t\tthis.addChild(new Text(theme.fg(\"dim\", hint), 1, 0));\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Ctrl+Enter to submit\n\t\tif (keyData === \"\\x1b[13;5u\" || keyData === \"\\x1b[27;5;13~\") {\n\t\t\tthis.onSubmitCallback(this.editor.getText());\n\t\t\treturn;\n\t\t}\n\n\t\t// Escape to cancel\n\t\tif (isEscape(keyData)) {\n\t\t\tthis.onCancelCallback();\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+G for external editor\n\t\tif (isCtrlG(keyData)) {\n\t\t\tthis.openExternalEditor();\n\t\t\treturn;\n\t\t}\n\n\t\t// Forward to editor\n\t\tthis.editor.handleInput(keyData);\n\t}\n\n\tprivate openExternalEditor(): void {\n\t\tconst editorCmd = process.env.VISUAL || process.env.EDITOR;\n\t\tif (!editorCmd) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst currentText = this.editor.getText();\n\t\tconst tmpFile = path.join(os.tmpdir(), `pi-hook-editor-${Date.now()}.md`);\n\n\t\ttry {\n\t\t\tfs.writeFileSync(tmpFile, currentText, \"utf-8\");\n\t\t\tthis.tui.stop();\n\n\t\t\tconst [editor, ...editorArgs] = editorCmd.split(\" \");\n\t\t\tconst result = spawnSync(editor, [...editorArgs, tmpFile], {\n\t\t\t\tstdio: \"inherit\",\n\t\t\t});\n\n\t\t\tif (result.status === 0) {\n\t\t\t\tconst newContent = fs.readFileSync(tmpFile, \"utf-8\").replace(/\\n$/, \"\");\n\t\t\t\tthis.editor.setText(newContent);\n\t\t\t}\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tfs.unlinkSync(tmpFile);\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t\tthis.tui.start();\n\t\t\tthis.tui.requestRender();\n\t\t}\n\t}\n}\n"]}