@mariozechner/pi-coding-agent 0.14.2 → 0.16.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 (316) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +415 -1098
  3. package/dist/cli/args.d.ts +30 -0
  4. package/dist/cli/args.d.ts.map +1 -0
  5. package/dist/cli/args.js +179 -0
  6. package/dist/cli/args.js.map +1 -0
  7. package/dist/cli/file-processor.d.ts +11 -0
  8. package/dist/cli/file-processor.d.ts.map +1 -0
  9. package/dist/cli/file-processor.js +82 -0
  10. package/dist/cli/file-processor.js.map +1 -0
  11. package/dist/cli/session-picker.d.ts +7 -0
  12. package/dist/cli/session-picker.d.ts.map +1 -0
  13. package/dist/cli/session-picker.js +29 -0
  14. package/dist/cli/session-picker.js.map +1 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +7 -18
  17. package/dist/cli.js.map +1 -1
  18. package/dist/config.d.ts +2 -2
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +15 -9
  21. package/dist/config.js.map +1 -1
  22. package/dist/core/agent-session.d.ts +287 -0
  23. package/dist/core/agent-session.d.ts.map +1 -0
  24. package/dist/core/agent-session.js +735 -0
  25. package/dist/core/agent-session.js.map +1 -0
  26. package/dist/core/bash-executor.d.ts +41 -0
  27. package/dist/core/bash-executor.d.ts.map +1 -0
  28. package/dist/core/bash-executor.js +132 -0
  29. package/dist/core/bash-executor.js.map +1 -0
  30. package/dist/{compaction.d.ts → core/compaction.d.ts} +5 -1
  31. package/dist/core/compaction.d.ts.map +1 -0
  32. package/dist/{compaction.js → core/compaction.js} +23 -1
  33. package/dist/core/compaction.js.map +1 -0
  34. package/dist/core/export-html.d.ts.map +1 -0
  35. package/dist/{export-html.js → core/export-html.js} +1 -1
  36. package/dist/{export-html.d.ts.map → core/export-html.js.map} +1 -1
  37. package/dist/core/index.d.ts +6 -0
  38. package/dist/core/index.d.ts.map +1 -0
  39. package/dist/core/index.js +6 -0
  40. package/dist/core/index.js.map +1 -0
  41. package/dist/core/messages.d.ts.map +1 -0
  42. package/dist/core/messages.js.map +1 -0
  43. package/dist/core/model-config.d.ts.map +1 -0
  44. package/dist/{model-config.js → core/model-config.js} +1 -1
  45. package/dist/core/model-config.js.map +1 -0
  46. package/dist/core/model-resolver.d.ts +48 -0
  47. package/dist/core/model-resolver.d.ts.map +1 -0
  48. package/dist/core/model-resolver.js +244 -0
  49. package/dist/core/model-resolver.js.map +1 -0
  50. package/dist/core/oauth/anthropic.d.ts.map +1 -0
  51. package/dist/core/oauth/anthropic.js.map +1 -0
  52. package/dist/core/oauth/index.d.ts.map +1 -0
  53. package/dist/{oauth/index.d.ts.map → core/oauth/index.js.map} +1 -1
  54. package/dist/core/oauth/storage.d.ts.map +1 -0
  55. package/dist/{oauth → core/oauth}/storage.js +1 -1
  56. package/dist/core/oauth/storage.js.map +1 -0
  57. package/dist/core/session-manager.d.ts.map +1 -0
  58. package/dist/{session-manager.js → core/session-manager.js} +1 -1
  59. package/dist/core/session-manager.js.map +1 -0
  60. package/dist/core/settings-manager.d.ts.map +1 -0
  61. package/dist/{settings-manager.js → core/settings-manager.js} +1 -1
  62. package/dist/core/settings-manager.js.map +1 -0
  63. package/dist/core/slash-commands.d.ts.map +1 -0
  64. package/dist/{slash-commands.js → core/slash-commands.js} +1 -1
  65. package/dist/core/slash-commands.js.map +1 -0
  66. package/dist/core/system-prompt.d.ts +17 -0
  67. package/dist/core/system-prompt.d.ts.map +1 -0
  68. package/dist/core/system-prompt.js +203 -0
  69. package/dist/core/system-prompt.js.map +1 -0
  70. package/dist/core/tools/bash.d.ts.map +1 -0
  71. package/dist/{tools → core/tools}/bash.js +1 -1
  72. package/dist/core/tools/bash.js.map +1 -0
  73. package/dist/core/tools/edit.d.ts.map +1 -0
  74. package/dist/core/tools/edit.js.map +1 -0
  75. package/dist/core/tools/find.d.ts.map +1 -0
  76. package/dist/{tools → core/tools}/find.js +1 -1
  77. package/dist/core/tools/find.js.map +1 -0
  78. package/dist/core/tools/grep.d.ts.map +1 -0
  79. package/dist/{tools → core/tools}/grep.js +1 -1
  80. package/dist/core/tools/grep.js.map +1 -0
  81. package/dist/core/tools/index.d.ts.map +1 -0
  82. package/dist/core/tools/index.js.map +1 -0
  83. package/dist/core/tools/ls.d.ts.map +1 -0
  84. package/dist/core/tools/ls.js.map +1 -0
  85. package/dist/core/tools/read.d.ts.map +1 -0
  86. package/dist/core/tools/read.js.map +1 -0
  87. package/dist/core/tools/truncate.d.ts.map +1 -0
  88. package/dist/core/tools/truncate.js.map +1 -0
  89. package/dist/core/tools/write.d.ts.map +1 -0
  90. package/dist/core/tools/write.js.map +1 -0
  91. package/dist/index.d.ts +2 -2
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +2 -2
  94. package/dist/index.js.map +1 -1
  95. package/dist/main.d.ts +3 -0
  96. package/dist/main.d.ts.map +1 -1
  97. package/dist/main.js +176 -1082
  98. package/dist/main.js.map +1 -1
  99. package/dist/modes/index.d.ts +9 -0
  100. package/dist/modes/index.d.ts.map +1 -0
  101. package/dist/modes/index.js +8 -0
  102. package/dist/modes/index.js.map +1 -0
  103. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -0
  104. package/dist/modes/interactive/components/assistant-message.js.map +1 -0
  105. package/dist/{tui → modes/interactive/components}/bash-execution.d.ts +1 -1
  106. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -0
  107. package/dist/{tui → modes/interactive/components}/bash-execution.js +1 -1
  108. package/dist/modes/interactive/components/bash-execution.js.map +1 -0
  109. package/dist/modes/interactive/components/compaction.d.ts.map +1 -0
  110. package/dist/modes/interactive/components/compaction.js.map +1 -0
  111. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -0
  112. package/dist/modes/interactive/components/custom-editor.js.map +1 -0
  113. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -0
  114. package/dist/modes/interactive/components/dynamic-border.js.map +1 -0
  115. package/dist/modes/interactive/components/footer.d.ts.map +1 -0
  116. package/dist/{tui → modes/interactive/components}/footer.js +1 -1
  117. package/dist/modes/interactive/components/footer.js.map +1 -0
  118. package/dist/{tui → modes/interactive/components}/model-selector.d.ts +1 -1
  119. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -0
  120. package/dist/{tui → modes/interactive/components}/model-selector.js +3 -3
  121. package/dist/modes/interactive/components/model-selector.js.map +1 -0
  122. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -0
  123. package/dist/{tui → modes/interactive/components}/oauth-selector.js +2 -2
  124. package/dist/modes/interactive/components/oauth-selector.js.map +1 -0
  125. package/dist/modes/interactive/components/queue-mode-selector.d.ts.map +1 -0
  126. package/dist/modes/interactive/components/queue-mode-selector.js.map +1 -0
  127. package/dist/{tui → modes/interactive/components}/session-selector.d.ts +1 -1
  128. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -0
  129. package/dist/{tui → modes/interactive/components}/session-selector.js +1 -1
  130. package/dist/modes/interactive/components/session-selector.js.map +1 -0
  131. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -0
  132. package/dist/{tui/theme-selector.d.ts.map → modes/interactive/components/theme-selector.js.map} +1 -1
  133. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -0
  134. package/dist/modes/interactive/components/thinking-selector.js.map +1 -0
  135. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -0
  136. package/dist/modes/interactive/components/tool-execution.js.map +1 -0
  137. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -0
  138. package/dist/modes/interactive/components/user-message-selector.js.map +1 -0
  139. package/dist/modes/interactive/components/user-message.d.ts.map +1 -0
  140. package/dist/modes/interactive/components/user-message.js.map +1 -0
  141. package/dist/{tui/tui-renderer.d.ts → modes/interactive/interactive-mode.d.ts} +36 -38
  142. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -0
  143. package/dist/modes/interactive/interactive-mode.js +1217 -0
  144. package/dist/modes/interactive/interactive-mode.js.map +1 -0
  145. package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  146. package/dist/{theme → modes/interactive/theme}/theme.js +1 -1
  147. package/dist/modes/interactive/theme/theme.js.map +1 -0
  148. package/dist/modes/print-mode.d.ts +21 -0
  149. package/dist/modes/print-mode.d.ts.map +1 -0
  150. package/dist/modes/print-mode.js +53 -0
  151. package/dist/modes/print-mode.js.map +1 -0
  152. package/dist/modes/rpc/rpc-client.d.ts +182 -0
  153. package/dist/modes/rpc/rpc-client.d.ts.map +1 -0
  154. package/dist/modes/rpc/rpc-client.js +362 -0
  155. package/dist/modes/rpc/rpc-client.js.map +1 -0
  156. package/dist/modes/rpc/rpc-mode.d.ts +19 -0
  157. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -0
  158. package/dist/modes/rpc/rpc-mode.js +204 -0
  159. package/dist/modes/rpc/rpc-mode.js.map +1 -0
  160. package/dist/modes/rpc/rpc-types.d.ts +254 -0
  161. package/dist/modes/rpc/rpc-types.d.ts.map +1 -0
  162. package/dist/modes/rpc/rpc-types.js +8 -0
  163. package/dist/modes/rpc/rpc-types.js.map +1 -0
  164. package/dist/{changelog.d.ts → utils/changelog.d.ts} +1 -1
  165. package/dist/{changelog.js.map → utils/changelog.d.ts.map} +1 -1
  166. package/dist/{changelog.js → utils/changelog.js} +1 -1
  167. package/dist/utils/changelog.js.map +1 -0
  168. package/dist/utils/clipboard.d.ts.map +1 -0
  169. package/dist/utils/clipboard.js.map +1 -0
  170. package/dist/utils/fuzzy.d.ts.map +1 -0
  171. package/dist/utils/fuzzy.js.map +1 -0
  172. package/dist/utils/shell.d.ts.map +1 -0
  173. package/dist/{shell.js → utils/shell.js} +1 -1
  174. package/dist/utils/shell.js.map +1 -0
  175. package/dist/utils/tools-manager.d.ts.map +1 -0
  176. package/dist/{tools-manager.js → utils/tools-manager.js} +1 -1
  177. package/dist/utils/tools-manager.js.map +1 -0
  178. package/package.json +6 -6
  179. package/dist/changelog.d.ts.map +0 -1
  180. package/dist/clipboard.d.ts.map +0 -1
  181. package/dist/clipboard.js.map +0 -1
  182. package/dist/compaction.d.ts.map +0 -1
  183. package/dist/compaction.js.map +0 -1
  184. package/dist/export-html.js.map +0 -1
  185. package/dist/fuzzy.d.ts.map +0 -1
  186. package/dist/fuzzy.js.map +0 -1
  187. package/dist/messages.d.ts.map +0 -1
  188. package/dist/messages.js.map +0 -1
  189. package/dist/model-config.d.ts.map +0 -1
  190. package/dist/model-config.js.map +0 -1
  191. package/dist/oauth/anthropic.d.ts.map +0 -1
  192. package/dist/oauth/anthropic.js.map +0 -1
  193. package/dist/oauth/index.js.map +0 -1
  194. package/dist/oauth/storage.d.ts.map +0 -1
  195. package/dist/oauth/storage.js.map +0 -1
  196. package/dist/session-manager.d.ts.map +0 -1
  197. package/dist/session-manager.js.map +0 -1
  198. package/dist/settings-manager.d.ts.map +0 -1
  199. package/dist/settings-manager.js.map +0 -1
  200. package/dist/shell.d.ts.map +0 -1
  201. package/dist/shell.js.map +0 -1
  202. package/dist/slash-commands.d.ts.map +0 -1
  203. package/dist/slash-commands.js.map +0 -1
  204. package/dist/theme/theme.d.ts.map +0 -1
  205. package/dist/theme/theme.js.map +0 -1
  206. package/dist/tools/bash.d.ts.map +0 -1
  207. package/dist/tools/bash.js.map +0 -1
  208. package/dist/tools/edit.d.ts.map +0 -1
  209. package/dist/tools/edit.js.map +0 -1
  210. package/dist/tools/find.d.ts.map +0 -1
  211. package/dist/tools/find.js.map +0 -1
  212. package/dist/tools/grep.d.ts.map +0 -1
  213. package/dist/tools/grep.js.map +0 -1
  214. package/dist/tools/index.d.ts.map +0 -1
  215. package/dist/tools/index.js.map +0 -1
  216. package/dist/tools/ls.d.ts.map +0 -1
  217. package/dist/tools/ls.js.map +0 -1
  218. package/dist/tools/read.d.ts.map +0 -1
  219. package/dist/tools/read.js.map +0 -1
  220. package/dist/tools/truncate.d.ts.map +0 -1
  221. package/dist/tools/truncate.js.map +0 -1
  222. package/dist/tools/write.d.ts.map +0 -1
  223. package/dist/tools/write.js.map +0 -1
  224. package/dist/tools-manager.d.ts.map +0 -1
  225. package/dist/tools-manager.js.map +0 -1
  226. package/dist/tui/assistant-message.d.ts.map +0 -1
  227. package/dist/tui/assistant-message.js.map +0 -1
  228. package/dist/tui/bash-execution.d.ts.map +0 -1
  229. package/dist/tui/bash-execution.js.map +0 -1
  230. package/dist/tui/compaction.d.ts.map +0 -1
  231. package/dist/tui/compaction.js.map +0 -1
  232. package/dist/tui/custom-editor.d.ts.map +0 -1
  233. package/dist/tui/custom-editor.js.map +0 -1
  234. package/dist/tui/dynamic-border.d.ts.map +0 -1
  235. package/dist/tui/dynamic-border.js.map +0 -1
  236. package/dist/tui/footer.d.ts.map +0 -1
  237. package/dist/tui/footer.js.map +0 -1
  238. package/dist/tui/model-selector.d.ts.map +0 -1
  239. package/dist/tui/model-selector.js.map +0 -1
  240. package/dist/tui/oauth-selector.d.ts.map +0 -1
  241. package/dist/tui/oauth-selector.js.map +0 -1
  242. package/dist/tui/queue-mode-selector.d.ts.map +0 -1
  243. package/dist/tui/queue-mode-selector.js.map +0 -1
  244. package/dist/tui/session-selector.d.ts.map +0 -1
  245. package/dist/tui/session-selector.js.map +0 -1
  246. package/dist/tui/theme-selector.js.map +0 -1
  247. package/dist/tui/thinking-selector.d.ts.map +0 -1
  248. package/dist/tui/thinking-selector.js.map +0 -1
  249. package/dist/tui/tool-execution.d.ts.map +0 -1
  250. package/dist/tui/tool-execution.js.map +0 -1
  251. package/dist/tui/tui-renderer.d.ts.map +0 -1
  252. package/dist/tui/tui-renderer.js +0 -1937
  253. package/dist/tui/tui-renderer.js.map +0 -1
  254. package/dist/tui/user-message-selector.d.ts.map +0 -1
  255. package/dist/tui/user-message-selector.js.map +0 -1
  256. package/dist/tui/user-message.d.ts.map +0 -1
  257. package/dist/tui/user-message.js.map +0 -1
  258. /package/dist/{export-html.d.ts → core/export-html.d.ts} +0 -0
  259. /package/dist/{messages.d.ts → core/messages.d.ts} +0 -0
  260. /package/dist/{messages.js → core/messages.js} +0 -0
  261. /package/dist/{model-config.d.ts → core/model-config.d.ts} +0 -0
  262. /package/dist/{oauth → core/oauth}/anthropic.d.ts +0 -0
  263. /package/dist/{oauth → core/oauth}/anthropic.js +0 -0
  264. /package/dist/{oauth → core/oauth}/index.d.ts +0 -0
  265. /package/dist/{oauth → core/oauth}/index.js +0 -0
  266. /package/dist/{oauth → core/oauth}/storage.d.ts +0 -0
  267. /package/dist/{session-manager.d.ts → core/session-manager.d.ts} +0 -0
  268. /package/dist/{settings-manager.d.ts → core/settings-manager.d.ts} +0 -0
  269. /package/dist/{slash-commands.d.ts → core/slash-commands.d.ts} +0 -0
  270. /package/dist/{tools → core/tools}/bash.d.ts +0 -0
  271. /package/dist/{tools → core/tools}/edit.d.ts +0 -0
  272. /package/dist/{tools → core/tools}/edit.js +0 -0
  273. /package/dist/{tools → core/tools}/find.d.ts +0 -0
  274. /package/dist/{tools → core/tools}/grep.d.ts +0 -0
  275. /package/dist/{tools → core/tools}/index.d.ts +0 -0
  276. /package/dist/{tools → core/tools}/index.js +0 -0
  277. /package/dist/{tools → core/tools}/ls.d.ts +0 -0
  278. /package/dist/{tools → core/tools}/ls.js +0 -0
  279. /package/dist/{tools → core/tools}/read.d.ts +0 -0
  280. /package/dist/{tools → core/tools}/read.js +0 -0
  281. /package/dist/{tools → core/tools}/truncate.d.ts +0 -0
  282. /package/dist/{tools → core/tools}/truncate.js +0 -0
  283. /package/dist/{tools → core/tools}/write.d.ts +0 -0
  284. /package/dist/{tools → core/tools}/write.js +0 -0
  285. /package/dist/{tui → modes/interactive/components}/assistant-message.d.ts +0 -0
  286. /package/dist/{tui → modes/interactive/components}/assistant-message.js +0 -0
  287. /package/dist/{tui → modes/interactive/components}/compaction.d.ts +0 -0
  288. /package/dist/{tui → modes/interactive/components}/compaction.js +0 -0
  289. /package/dist/{tui → modes/interactive/components}/custom-editor.d.ts +0 -0
  290. /package/dist/{tui → modes/interactive/components}/custom-editor.js +0 -0
  291. /package/dist/{tui → modes/interactive/components}/dynamic-border.d.ts +0 -0
  292. /package/dist/{tui → modes/interactive/components}/dynamic-border.js +0 -0
  293. /package/dist/{tui → modes/interactive/components}/footer.d.ts +0 -0
  294. /package/dist/{tui → modes/interactive/components}/oauth-selector.d.ts +0 -0
  295. /package/dist/{tui → modes/interactive/components}/queue-mode-selector.d.ts +0 -0
  296. /package/dist/{tui → modes/interactive/components}/queue-mode-selector.js +0 -0
  297. /package/dist/{tui → modes/interactive/components}/theme-selector.d.ts +0 -0
  298. /package/dist/{tui → modes/interactive/components}/theme-selector.js +0 -0
  299. /package/dist/{tui → modes/interactive/components}/thinking-selector.d.ts +0 -0
  300. /package/dist/{tui → modes/interactive/components}/thinking-selector.js +0 -0
  301. /package/dist/{tui → modes/interactive/components}/tool-execution.d.ts +0 -0
  302. /package/dist/{tui → modes/interactive/components}/tool-execution.js +0 -0
  303. /package/dist/{tui → modes/interactive/components}/user-message-selector.d.ts +0 -0
  304. /package/dist/{tui → modes/interactive/components}/user-message-selector.js +0 -0
  305. /package/dist/{tui → modes/interactive/components}/user-message.d.ts +0 -0
  306. /package/dist/{tui → modes/interactive/components}/user-message.js +0 -0
  307. /package/dist/{theme → modes/interactive/theme}/dark.json +0 -0
  308. /package/dist/{theme → modes/interactive/theme}/light.json +0 -0
  309. /package/dist/{theme → modes/interactive/theme}/theme-schema.json +0 -0
  310. /package/dist/{theme → modes/interactive/theme}/theme.d.ts +0 -0
  311. /package/dist/{clipboard.d.ts → utils/clipboard.d.ts} +0 -0
  312. /package/dist/{clipboard.js → utils/clipboard.js} +0 -0
  313. /package/dist/{fuzzy.d.ts → utils/fuzzy.d.ts} +0 -0
  314. /package/dist/{fuzzy.js → utils/fuzzy.js} +0 -0
  315. /package/dist/{shell.d.ts → utils/shell.d.ts} +0 -0
  316. /package/dist/{tools-manager.d.ts → utils/tools-manager.d.ts} +0 -0
@@ -1,1937 +0,0 @@
1
- import { randomBytes } from "node:crypto";
2
- import * as fs from "node:fs";
3
- import { createWriteStream } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import * as path from "node:path";
6
- import { join } from "node:path";
7
- import { CombinedAutocompleteProvider, Container, Input, Loader, Markdown, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
8
- import { exec, spawn } from "child_process";
9
- import stripAnsi from "strip-ansi";
10
- import { getChangelogPath, parseChangelog } from "../changelog.js";
11
- import { copyToClipboard } from "../clipboard.js";
12
- import { calculateContextTokens, compact, shouldCompact } from "../compaction.js";
13
- import { APP_NAME, getDebugLogPath, getModelsPath, getOAuthPath } from "../config.js";
14
- import { exportSessionToHtml } from "../export-html.js";
15
- import { isBashExecutionMessage } from "../messages.js";
16
- import { getApiKeyForModel, getAvailableModels, invalidateOAuthCache } from "../model-config.js";
17
- import { listOAuthProviders, login, logout } from "../oauth/index.js";
18
- import { getLatestCompactionEntry, loadSessionFromEntries, SUMMARY_PREFIX, SUMMARY_SUFFIX, } from "../session-manager.js";
19
- import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "../shell.js";
20
- import { expandSlashCommand, loadSlashCommands } from "../slash-commands.js";
21
- import { getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "../theme/theme.js";
22
- import { DEFAULT_MAX_BYTES, truncateTail } from "../tools/truncate.js";
23
- import { AssistantMessageComponent } from "./assistant-message.js";
24
- import { BashExecutionComponent } from "./bash-execution.js";
25
- import { CompactionComponent } from "./compaction.js";
26
- import { CustomEditor } from "./custom-editor.js";
27
- import { DynamicBorder } from "./dynamic-border.js";
28
- import { FooterComponent } from "./footer.js";
29
- import { ModelSelectorComponent } from "./model-selector.js";
30
- import { OAuthSelectorComponent } from "./oauth-selector.js";
31
- import { QueueModeSelectorComponent } from "./queue-mode-selector.js";
32
- import { SessionSelectorComponent } from "./session-selector.js";
33
- import { ThemeSelectorComponent } from "./theme-selector.js";
34
- import { ThinkingSelectorComponent } from "./thinking-selector.js";
35
- import { ToolExecutionComponent } from "./tool-execution.js";
36
- import { UserMessageComponent } from "./user-message.js";
37
- import { UserMessageSelectorComponent } from "./user-message-selector.js";
38
- /**
39
- * TUI renderer for the coding agent
40
- */
41
- export class TuiRenderer {
42
- ui;
43
- chatContainer;
44
- pendingMessagesContainer;
45
- statusContainer;
46
- editor;
47
- editorContainer; // Container to swap between editor and selector
48
- footer;
49
- agent;
50
- sessionManager;
51
- settingsManager;
52
- version;
53
- isInitialized = false;
54
- onInputCallback;
55
- loadingAnimation = null;
56
- lastSigintTime = 0;
57
- lastEscapeTime = 0;
58
- changelogMarkdown = null;
59
- collapseChangelog = false;
60
- // Message queueing
61
- queuedMessages = [];
62
- // Streaming message tracking
63
- streamingComponent = null;
64
- // Tool execution tracking: toolCallId -> component
65
- pendingTools = new Map();
66
- // Thinking level selector
67
- thinkingSelector = null;
68
- // Queue mode selector
69
- queueModeSelector = null;
70
- // Theme selector
71
- themeSelector = null;
72
- // Model selector
73
- modelSelector = null;
74
- // User message selector (for branching)
75
- userMessageSelector = null;
76
- // Session selector (for resume)
77
- sessionSelector = null;
78
- // OAuth selector
79
- oauthSelector = null;
80
- // Track if this is the first user message (to skip spacer)
81
- isFirstUserMessage = true;
82
- // Model scope for quick cycling
83
- scopedModels = [];
84
- // Tool output expansion state
85
- toolOutputExpanded = false;
86
- // Thinking block visibility state
87
- hideThinkingBlock = false;
88
- // Agent subscription unsubscribe function
89
- unsubscribe;
90
- // File-based slash commands
91
- fileCommands = [];
92
- // Track if editor is in bash mode (text starts with !)
93
- isBashMode = false;
94
- // Track running bash command process for cancellation
95
- bashProcess = null;
96
- // Track current bash execution component
97
- bashComponent = null;
98
- constructor(agent, sessionManager, settingsManager, version, changelogMarkdown = null, collapseChangelog = false, scopedModels = [], fdPath = null) {
99
- this.agent = agent;
100
- this.sessionManager = sessionManager;
101
- this.settingsManager = settingsManager;
102
- this.version = version;
103
- this.changelogMarkdown = changelogMarkdown;
104
- this.collapseChangelog = collapseChangelog;
105
- this.scopedModels = scopedModels;
106
- this.ui = new TUI(new ProcessTerminal());
107
- this.chatContainer = new Container();
108
- this.pendingMessagesContainer = new Container();
109
- this.statusContainer = new Container();
110
- this.editor = new CustomEditor(getEditorTheme());
111
- this.editorContainer = new Container(); // Container to hold editor or selector
112
- this.editorContainer.addChild(this.editor); // Start with editor
113
- this.footer = new FooterComponent(agent.state);
114
- this.footer.setAutoCompactEnabled(this.settingsManager.getCompactionEnabled());
115
- // Define slash commands
116
- const thinkingCommand = {
117
- name: "thinking",
118
- description: "Select reasoning level (opens selector UI)",
119
- };
120
- const modelCommand = {
121
- name: "model",
122
- description: "Select model (opens selector UI)",
123
- };
124
- const exportCommand = {
125
- name: "export",
126
- description: "Export session to HTML file",
127
- };
128
- const copyCommand = {
129
- name: "copy",
130
- description: "Copy last agent message to clipboard",
131
- };
132
- const sessionCommand = {
133
- name: "session",
134
- description: "Show session info and stats",
135
- };
136
- const changelogCommand = {
137
- name: "changelog",
138
- description: "Show changelog entries",
139
- };
140
- const branchCommand = {
141
- name: "branch",
142
- description: "Create a new branch from a previous message",
143
- };
144
- const loginCommand = {
145
- name: "login",
146
- description: "Login with OAuth provider",
147
- };
148
- const logoutCommand = {
149
- name: "logout",
150
- description: "Logout from OAuth provider",
151
- };
152
- const queueCommand = {
153
- name: "queue",
154
- description: "Select message queue mode (opens selector UI)",
155
- };
156
- const themeCommand = {
157
- name: "theme",
158
- description: "Select color theme (opens selector UI)",
159
- };
160
- const clearCommand = {
161
- name: "clear",
162
- description: "Clear context and start a fresh session",
163
- };
164
- const compactCommand = {
165
- name: "compact",
166
- description: "Manually compact the session context",
167
- };
168
- const autocompactCommand = {
169
- name: "autocompact",
170
- description: "Toggle automatic context compaction",
171
- };
172
- const resumeCommand = {
173
- name: "resume",
174
- description: "Resume a different session",
175
- };
176
- // Load hide thinking block setting
177
- this.hideThinkingBlock = settingsManager.getHideThinkingBlock();
178
- // Load file-based slash commands
179
- this.fileCommands = loadSlashCommands();
180
- // Convert file commands to SlashCommand format
181
- const fileSlashCommands = this.fileCommands.map((cmd) => ({
182
- name: cmd.name,
183
- description: cmd.description,
184
- }));
185
- // Setup autocomplete for file paths and slash commands
186
- const autocompleteProvider = new CombinedAutocompleteProvider([
187
- thinkingCommand,
188
- modelCommand,
189
- themeCommand,
190
- exportCommand,
191
- copyCommand,
192
- sessionCommand,
193
- changelogCommand,
194
- branchCommand,
195
- loginCommand,
196
- logoutCommand,
197
- queueCommand,
198
- clearCommand,
199
- compactCommand,
200
- autocompactCommand,
201
- resumeCommand,
202
- ...fileSlashCommands,
203
- ], process.cwd(), fdPath);
204
- this.editor.setAutocompleteProvider(autocompleteProvider);
205
- }
206
- async init() {
207
- if (this.isInitialized)
208
- return;
209
- // Add header with logo and instructions
210
- const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
211
- const instructions = theme.fg("dim", "esc") +
212
- theme.fg("muted", " to interrupt") +
213
- "\n" +
214
- theme.fg("dim", "ctrl+c") +
215
- theme.fg("muted", " to clear") +
216
- "\n" +
217
- theme.fg("dim", "ctrl+c twice") +
218
- theme.fg("muted", " to exit") +
219
- "\n" +
220
- theme.fg("dim", "ctrl+k") +
221
- theme.fg("muted", " to delete line") +
222
- "\n" +
223
- theme.fg("dim", "shift+tab") +
224
- theme.fg("muted", " to cycle thinking") +
225
- "\n" +
226
- theme.fg("dim", "ctrl+p") +
227
- theme.fg("muted", " to cycle models") +
228
- "\n" +
229
- theme.fg("dim", "ctrl+o") +
230
- theme.fg("muted", " to expand tools") +
231
- "\n" +
232
- theme.fg("dim", "ctrl+t") +
233
- theme.fg("muted", " to toggle thinking") +
234
- "\n" +
235
- theme.fg("dim", "/") +
236
- theme.fg("muted", " for commands") +
237
- "\n" +
238
- theme.fg("dim", "!") +
239
- theme.fg("muted", " to run bash") +
240
- "\n" +
241
- theme.fg("dim", "drop files") +
242
- theme.fg("muted", " to attach");
243
- const header = new Text(logo + "\n" + instructions, 1, 0);
244
- // Setup UI layout
245
- this.ui.addChild(new Spacer(1));
246
- this.ui.addChild(header);
247
- this.ui.addChild(new Spacer(1));
248
- // Add changelog if provided
249
- if (this.changelogMarkdown) {
250
- this.ui.addChild(new DynamicBorder());
251
- if (this.collapseChangelog) {
252
- // Show condensed version with hint to use /changelog
253
- const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
254
- const latestVersion = versionMatch ? versionMatch[1] : this.version;
255
- const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
256
- this.ui.addChild(new Text(condensedText, 1, 0));
257
- }
258
- else {
259
- this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
260
- this.ui.addChild(new Spacer(1));
261
- this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
262
- this.ui.addChild(new Spacer(1));
263
- }
264
- this.ui.addChild(new DynamicBorder());
265
- }
266
- this.ui.addChild(this.chatContainer);
267
- this.ui.addChild(this.pendingMessagesContainer);
268
- this.ui.addChild(this.statusContainer);
269
- this.ui.addChild(new Spacer(1));
270
- this.ui.addChild(this.editorContainer); // Use container that can hold editor or selector
271
- this.ui.addChild(this.footer);
272
- this.ui.setFocus(this.editor);
273
- // Set up custom key handlers on the editor
274
- this.editor.onEscape = () => {
275
- // Intercept Escape key when processing
276
- if (this.loadingAnimation) {
277
- // Get all queued messages
278
- const queuedText = this.queuedMessages.join("\n\n");
279
- // Get current editor text
280
- const currentText = this.editor.getText();
281
- // Combine: queued messages + current editor text
282
- const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
283
- // Put back in editor
284
- this.editor.setText(combinedText);
285
- // Clear queued messages
286
- this.queuedMessages = [];
287
- this.updatePendingMessagesDisplay();
288
- // Clear agent's queue too
289
- this.agent.clearMessageQueue();
290
- // Abort
291
- this.agent.abort();
292
- }
293
- else if (this.bashProcess) {
294
- // Kill running bash command
295
- if (this.bashProcess.pid) {
296
- killProcessTree(this.bashProcess.pid);
297
- }
298
- this.bashProcess = null;
299
- }
300
- else if (this.isBashMode) {
301
- // Cancel bash mode and clear editor
302
- this.editor.setText("");
303
- this.isBashMode = false;
304
- this.updateEditorBorderColor();
305
- }
306
- else if (!this.editor.getText().trim()) {
307
- // Double-escape with empty editor triggers /branch
308
- const now = Date.now();
309
- if (now - this.lastEscapeTime < 500) {
310
- this.showUserMessageSelector();
311
- this.lastEscapeTime = 0; // Reset to prevent triple-escape
312
- }
313
- else {
314
- this.lastEscapeTime = now;
315
- }
316
- }
317
- };
318
- this.editor.onCtrlC = () => {
319
- this.handleCtrlC();
320
- };
321
- this.editor.onShiftTab = () => {
322
- this.cycleThinkingLevel();
323
- };
324
- this.editor.onCtrlP = () => {
325
- this.cycleModel();
326
- };
327
- this.editor.onCtrlO = () => {
328
- this.toggleToolOutputExpansion();
329
- };
330
- this.editor.onCtrlT = () => {
331
- this.toggleThinkingBlockVisibility();
332
- };
333
- // Handle editor text changes for bash mode detection
334
- this.editor.onChange = (text) => {
335
- const wasBashMode = this.isBashMode;
336
- this.isBashMode = text.trimStart().startsWith("!");
337
- if (wasBashMode !== this.isBashMode) {
338
- this.updateEditorBorderColor();
339
- }
340
- };
341
- // Handle editor submission
342
- this.editor.onSubmit = async (text) => {
343
- text = text.trim();
344
- if (!text)
345
- return;
346
- // Check for /thinking command
347
- if (text === "/thinking") {
348
- // Show thinking level selector
349
- this.showThinkingSelector();
350
- this.editor.setText("");
351
- return;
352
- }
353
- // Check for /model command
354
- if (text === "/model") {
355
- // Show model selector
356
- this.showModelSelector();
357
- this.editor.setText("");
358
- return;
359
- }
360
- // Check for /export command
361
- if (text.startsWith("/export")) {
362
- this.handleExportCommand(text);
363
- this.editor.setText("");
364
- return;
365
- }
366
- // Check for /copy command
367
- if (text === "/copy") {
368
- this.handleCopyCommand();
369
- this.editor.setText("");
370
- return;
371
- }
372
- // Check for /session command
373
- if (text === "/session") {
374
- this.handleSessionCommand();
375
- this.editor.setText("");
376
- return;
377
- }
378
- // Check for /changelog command
379
- if (text === "/changelog") {
380
- this.handleChangelogCommand();
381
- this.editor.setText("");
382
- return;
383
- }
384
- // Check for /branch command
385
- if (text === "/branch") {
386
- this.showUserMessageSelector();
387
- this.editor.setText("");
388
- return;
389
- }
390
- // Check for /login command
391
- if (text === "/login") {
392
- this.showOAuthSelector("login");
393
- this.editor.setText("");
394
- return;
395
- }
396
- // Check for /logout command
397
- if (text === "/logout") {
398
- this.showOAuthSelector("logout");
399
- this.editor.setText("");
400
- return;
401
- }
402
- // Check for /queue command
403
- if (text === "/queue") {
404
- this.showQueueModeSelector();
405
- this.editor.setText("");
406
- return;
407
- }
408
- // Check for /theme command
409
- if (text === "/theme") {
410
- this.showThemeSelector();
411
- this.editor.setText("");
412
- return;
413
- }
414
- // Check for /clear command
415
- if (text === "/clear") {
416
- this.handleClearCommand();
417
- this.editor.setText("");
418
- return;
419
- }
420
- // Check for /compact command
421
- if (text === "/compact" || text.startsWith("/compact ")) {
422
- const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
423
- this.handleCompactCommand(customInstructions);
424
- this.editor.setText("");
425
- return;
426
- }
427
- // Check for /autocompact command
428
- if (text === "/autocompact") {
429
- this.handleAutocompactCommand();
430
- this.editor.setText("");
431
- return;
432
- }
433
- // Check for /debug command
434
- if (text === "/debug") {
435
- this.handleDebugCommand();
436
- this.editor.setText("");
437
- return;
438
- }
439
- // Check for /resume command
440
- if (text === "/resume") {
441
- this.showSessionSelector();
442
- this.editor.setText("");
443
- return;
444
- }
445
- // Check for bash command (!<command>)
446
- if (text.startsWith("!")) {
447
- const command = text.slice(1).trim();
448
- if (command) {
449
- // Block if bash already running
450
- if (this.bashProcess) {
451
- this.showWarning("A bash command is already running. Press Esc to cancel it first.");
452
- // Restore text since editor clears on submit
453
- this.editor.setText(text);
454
- return;
455
- }
456
- // Add to history for up/down arrow navigation
457
- this.editor.addToHistory(text);
458
- this.handleBashCommand(command);
459
- // Reset bash mode since editor is now empty
460
- this.isBashMode = false;
461
- this.updateEditorBorderColor();
462
- return;
463
- }
464
- }
465
- // Check for file-based slash commands
466
- text = expandSlashCommand(text, this.fileCommands);
467
- // Normal message submission - validate model and API key first
468
- const currentModel = this.agent.state.model;
469
- if (!currentModel) {
470
- this.showError("No model selected.\n\n" +
471
- "Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)\n" +
472
- `or create ${getModelsPath()}\n\n` +
473
- "Then use /model to select a model.");
474
- return;
475
- }
476
- // Validate API key (async)
477
- const apiKey = await getApiKeyForModel(currentModel);
478
- if (!apiKey) {
479
- this.showError(`No API key found for ${currentModel.provider}.\n\n` +
480
- `Set the appropriate environment variable or update ${getModelsPath()}`);
481
- this.editor.setText(text);
482
- return;
483
- }
484
- // Check if agent is currently streaming
485
- if (this.agent.state.isStreaming) {
486
- // Queue the message instead of submitting
487
- this.queuedMessages.push(text);
488
- // Queue in agent
489
- await this.agent.queueMessage({
490
- role: "user",
491
- content: [{ type: "text", text }],
492
- timestamp: Date.now(),
493
- });
494
- // Update pending messages display
495
- this.updatePendingMessagesDisplay();
496
- // Add to history for up/down arrow navigation
497
- this.editor.addToHistory(text);
498
- // Clear editor
499
- this.editor.setText("");
500
- this.ui.requestRender();
501
- return;
502
- }
503
- // All good, proceed with submission
504
- if (this.onInputCallback) {
505
- this.onInputCallback(text);
506
- }
507
- // Add to history for up/down arrow navigation
508
- this.editor.addToHistory(text);
509
- };
510
- // Start the UI
511
- this.ui.start();
512
- this.isInitialized = true;
513
- // Subscribe to agent events for UI updates and session saving
514
- this.subscribeToAgent();
515
- // Set up theme file watcher for live reload
516
- onThemeChange(() => {
517
- this.ui.invalidate();
518
- this.updateEditorBorderColor();
519
- this.ui.requestRender();
520
- });
521
- // Set up git branch watcher
522
- this.footer.watchBranch(() => {
523
- this.ui.requestRender();
524
- });
525
- }
526
- subscribeToAgent() {
527
- this.unsubscribe = this.agent.subscribe(async (event) => {
528
- // Handle UI updates
529
- await this.handleEvent(event, this.agent.state);
530
- // Save messages to session
531
- if (event.type === "message_end") {
532
- this.sessionManager.saveMessage(event.message);
533
- // Check if we should initialize session now (after first user+assistant exchange)
534
- if (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {
535
- this.sessionManager.startSession(this.agent.state);
536
- }
537
- // Check for auto-compaction after assistant messages
538
- if (event.message.role === "assistant") {
539
- await this.checkAutoCompaction();
540
- }
541
- }
542
- });
543
- }
544
- async checkAutoCompaction() {
545
- const settings = this.settingsManager.getCompactionSettings();
546
- if (!settings.enabled)
547
- return;
548
- // Get last non-aborted assistant message from agent state
549
- const messages = this.agent.state.messages;
550
- let lastAssistant = null;
551
- for (let i = messages.length - 1; i >= 0; i--) {
552
- const msg = messages[i];
553
- if (msg.role === "assistant") {
554
- const assistantMsg = msg;
555
- if (assistantMsg.stopReason !== "aborted") {
556
- lastAssistant = assistantMsg;
557
- break;
558
- }
559
- }
560
- }
561
- if (!lastAssistant)
562
- return;
563
- const contextTokens = calculateContextTokens(lastAssistant.usage);
564
- const contextWindow = this.agent.state.model.contextWindow;
565
- if (!shouldCompact(contextTokens, contextWindow, settings))
566
- return;
567
- // Trigger auto-compaction
568
- await this.executeCompaction(undefined, true);
569
- }
570
- async handleEvent(event, state) {
571
- if (!this.isInitialized) {
572
- await this.init();
573
- }
574
- // Update footer with current stats
575
- this.footer.updateState(state);
576
- switch (event.type) {
577
- case "agent_start":
578
- // Show loading animation
579
- // Note: Don't disable submit - we handle queuing in onSubmit callback
580
- // Stop old loader before clearing
581
- if (this.loadingAnimation) {
582
- this.loadingAnimation.stop();
583
- }
584
- this.statusContainer.clear();
585
- this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), "Working... (esc to interrupt)");
586
- this.statusContainer.addChild(this.loadingAnimation);
587
- this.ui.requestRender();
588
- break;
589
- case "message_start":
590
- if (event.message.role === "user") {
591
- // Check if this is a queued message
592
- const userMsg = event.message;
593
- const textBlocks = typeof userMsg.content === "string"
594
- ? [{ type: "text", text: userMsg.content }]
595
- : userMsg.content.filter((c) => c.type === "text");
596
- const messageText = textBlocks.map((c) => c.text).join("");
597
- const queuedIndex = this.queuedMessages.indexOf(messageText);
598
- if (queuedIndex !== -1) {
599
- // Remove from queued messages
600
- this.queuedMessages.splice(queuedIndex, 1);
601
- this.updatePendingMessagesDisplay();
602
- }
603
- // Show user message immediately and clear editor
604
- this.addMessageToChat(event.message);
605
- this.editor.setText("");
606
- this.ui.requestRender();
607
- }
608
- else if (event.message.role === "assistant") {
609
- // Create assistant component for streaming
610
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock);
611
- this.chatContainer.addChild(this.streamingComponent);
612
- this.streamingComponent.updateContent(event.message);
613
- this.ui.requestRender();
614
- }
615
- break;
616
- case "message_update":
617
- // Update streaming component
618
- if (this.streamingComponent && event.message.role === "assistant") {
619
- const assistantMsg = event.message;
620
- this.streamingComponent.updateContent(assistantMsg);
621
- // Create tool execution components as soon as we see tool calls
622
- for (const content of assistantMsg.content) {
623
- if (content.type === "toolCall") {
624
- // Only create if we haven't created it yet
625
- if (!this.pendingTools.has(content.id)) {
626
- this.chatContainer.addChild(new Text("", 0, 0));
627
- const component = new ToolExecutionComponent(content.name, content.arguments);
628
- this.chatContainer.addChild(component);
629
- this.pendingTools.set(content.id, component);
630
- }
631
- else {
632
- // Update existing component with latest arguments as they stream
633
- const component = this.pendingTools.get(content.id);
634
- if (component) {
635
- component.updateArgs(content.arguments);
636
- }
637
- }
638
- }
639
- }
640
- this.ui.requestRender();
641
- }
642
- break;
643
- case "message_end":
644
- // Skip user messages (already shown in message_start)
645
- if (event.message.role === "user") {
646
- break;
647
- }
648
- if (this.streamingComponent && event.message.role === "assistant") {
649
- const assistantMsg = event.message;
650
- // Update streaming component with final message (includes stopReason)
651
- this.streamingComponent.updateContent(assistantMsg);
652
- // If message was aborted or errored, mark all pending tool components as failed
653
- if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
654
- const errorMessage = assistantMsg.stopReason === "aborted" ? "Operation aborted" : assistantMsg.errorMessage || "Error";
655
- for (const [toolCallId, component] of this.pendingTools.entries()) {
656
- component.updateResult({
657
- content: [{ type: "text", text: errorMessage }],
658
- isError: true,
659
- });
660
- }
661
- this.pendingTools.clear();
662
- }
663
- // Keep the streaming component - it's now the final assistant message
664
- this.streamingComponent = null;
665
- // Invalidate footer cache to refresh git branch (in case agent executed git commands)
666
- this.footer.invalidate();
667
- }
668
- this.ui.requestRender();
669
- break;
670
- case "tool_execution_start": {
671
- // Component should already exist from message_update, but create if missing
672
- if (!this.pendingTools.has(event.toolCallId)) {
673
- const component = new ToolExecutionComponent(event.toolName, event.args);
674
- this.chatContainer.addChild(component);
675
- this.pendingTools.set(event.toolCallId, component);
676
- this.ui.requestRender();
677
- }
678
- break;
679
- }
680
- case "tool_execution_end": {
681
- // Update the existing tool component with the result
682
- const component = this.pendingTools.get(event.toolCallId);
683
- if (component) {
684
- // Convert result to the format expected by updateResult
685
- const resultData = typeof event.result === "string"
686
- ? {
687
- content: [{ type: "text", text: event.result }],
688
- details: undefined,
689
- isError: event.isError,
690
- }
691
- : {
692
- content: event.result.content,
693
- details: event.result.details,
694
- isError: event.isError,
695
- };
696
- component.updateResult(resultData);
697
- this.pendingTools.delete(event.toolCallId);
698
- this.ui.requestRender();
699
- }
700
- break;
701
- }
702
- case "agent_end":
703
- // Stop loading animation
704
- if (this.loadingAnimation) {
705
- this.loadingAnimation.stop();
706
- this.loadingAnimation = null;
707
- this.statusContainer.clear();
708
- }
709
- if (this.streamingComponent) {
710
- this.chatContainer.removeChild(this.streamingComponent);
711
- this.streamingComponent = null;
712
- }
713
- this.pendingTools.clear();
714
- // Note: Don't need to re-enable submit - we never disable it
715
- this.ui.requestRender();
716
- break;
717
- }
718
- }
719
- addMessageToChat(message) {
720
- // Handle bash execution messages
721
- if (isBashExecutionMessage(message)) {
722
- const bashMsg = message;
723
- const component = new BashExecutionComponent(bashMsg.command, this.ui);
724
- if (bashMsg.output) {
725
- component.appendOutput(bashMsg.output);
726
- }
727
- component.setComplete(bashMsg.exitCode, bashMsg.cancelled, bashMsg.truncated ? { truncated: true } : undefined, bashMsg.fullOutputPath);
728
- this.chatContainer.addChild(component);
729
- return;
730
- }
731
- if (message.role === "user") {
732
- const userMsg = message;
733
- // Extract text content from content blocks
734
- const textBlocks = typeof userMsg.content === "string"
735
- ? [{ type: "text", text: userMsg.content }]
736
- : userMsg.content.filter((c) => c.type === "text");
737
- const textContent = textBlocks.map((c) => c.text).join("");
738
- if (textContent) {
739
- const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
740
- this.chatContainer.addChild(userComponent);
741
- this.isFirstUserMessage = false;
742
- }
743
- }
744
- else if (message.role === "assistant") {
745
- const assistantMsg = message;
746
- // Add assistant message component
747
- const assistantComponent = new AssistantMessageComponent(assistantMsg, this.hideThinkingBlock);
748
- this.chatContainer.addChild(assistantComponent);
749
- }
750
- // Note: tool calls and results are now handled via tool_execution_start/end events
751
- }
752
- renderInitialMessages(state) {
753
- // Render all existing messages (for --continue mode)
754
- // Reset first user message flag for initial render
755
- this.isFirstUserMessage = true;
756
- // Update footer with loaded state
757
- this.footer.updateState(state);
758
- // Update editor border color based on current thinking level
759
- this.updateEditorBorderColor();
760
- // Get compaction info if any
761
- const compactionEntry = getLatestCompactionEntry(this.sessionManager.loadEntries());
762
- // Render messages
763
- for (let i = 0; i < state.messages.length; i++) {
764
- const message = state.messages[i];
765
- // Handle bash execution messages
766
- if (isBashExecutionMessage(message)) {
767
- this.addMessageToChat(message);
768
- continue;
769
- }
770
- if (message.role === "user") {
771
- const userMsg = message;
772
- const textBlocks = typeof userMsg.content === "string"
773
- ? [{ type: "text", text: userMsg.content }]
774
- : userMsg.content.filter((c) => c.type === "text");
775
- const textContent = textBlocks.map((c) => c.text).join("");
776
- if (textContent) {
777
- // Check if this is a compaction summary message
778
- if (textContent.startsWith(SUMMARY_PREFIX) && compactionEntry) {
779
- const summary = textContent.slice(SUMMARY_PREFIX.length, -SUMMARY_SUFFIX.length);
780
- const component = new CompactionComponent(compactionEntry.tokensBefore, summary);
781
- component.setExpanded(this.toolOutputExpanded);
782
- this.chatContainer.addChild(component);
783
- }
784
- else {
785
- const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
786
- this.chatContainer.addChild(userComponent);
787
- this.isFirstUserMessage = false;
788
- }
789
- }
790
- }
791
- else if (message.role === "assistant") {
792
- const assistantMsg = message;
793
- const assistantComponent = new AssistantMessageComponent(assistantMsg, this.hideThinkingBlock);
794
- this.chatContainer.addChild(assistantComponent);
795
- // Create tool execution components for any tool calls
796
- for (const content of assistantMsg.content) {
797
- if (content.type === "toolCall") {
798
- const component = new ToolExecutionComponent(content.name, content.arguments);
799
- this.chatContainer.addChild(component);
800
- // If message was aborted/errored, immediately mark tool as failed
801
- if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
802
- const errorMessage = assistantMsg.stopReason === "aborted"
803
- ? "Operation aborted"
804
- : assistantMsg.errorMessage || "Error";
805
- component.updateResult({
806
- content: [{ type: "text", text: errorMessage }],
807
- isError: true,
808
- });
809
- }
810
- else {
811
- // Store in map so we can update with results later
812
- this.pendingTools.set(content.id, component);
813
- }
814
- }
815
- }
816
- }
817
- else if (message.role === "toolResult") {
818
- // Update existing tool execution component with results ;
819
- const component = this.pendingTools.get(message.toolCallId);
820
- if (component) {
821
- component.updateResult({
822
- content: message.content,
823
- details: message.details,
824
- isError: message.isError,
825
- });
826
- // Remove from pending map since it's complete
827
- this.pendingTools.delete(message.toolCallId);
828
- }
829
- }
830
- }
831
- // Clear pending tools after rendering initial messages
832
- this.pendingTools.clear();
833
- // Populate editor history with user messages from the session (oldest first so newest is at index 0)
834
- for (const message of state.messages) {
835
- if (message.role === "user") {
836
- const textBlocks = typeof message.content === "string"
837
- ? [{ type: "text", text: message.content }]
838
- : message.content.filter((c) => c.type === "text");
839
- const textContent = textBlocks.map((c) => c.text).join("");
840
- // Skip compaction summary messages
841
- if (textContent && !textContent.startsWith(SUMMARY_PREFIX)) {
842
- this.editor.addToHistory(textContent);
843
- }
844
- }
845
- }
846
- this.ui.requestRender();
847
- }
848
- async getUserInput() {
849
- return new Promise((resolve) => {
850
- this.onInputCallback = (text) => {
851
- this.onInputCallback = undefined;
852
- resolve(text);
853
- };
854
- });
855
- }
856
- rebuildChatFromMessages() {
857
- // Reset state and re-render messages from agent state
858
- this.isFirstUserMessage = true;
859
- this.pendingTools.clear();
860
- // Get compaction info if any
861
- const compactionEntry = getLatestCompactionEntry(this.sessionManager.loadEntries());
862
- for (const message of this.agent.state.messages) {
863
- // Handle bash execution messages
864
- if (isBashExecutionMessage(message)) {
865
- this.addMessageToChat(message);
866
- continue;
867
- }
868
- if (message.role === "user") {
869
- const userMsg = message;
870
- const textBlocks = typeof userMsg.content === "string"
871
- ? [{ type: "text", text: userMsg.content }]
872
- : userMsg.content.filter((c) => c.type === "text");
873
- const textContent = textBlocks.map((c) => c.text).join("");
874
- if (textContent) {
875
- // Check if this is a compaction summary message
876
- if (textContent.startsWith(SUMMARY_PREFIX) && compactionEntry) {
877
- const summary = textContent.slice(SUMMARY_PREFIX.length, -SUMMARY_SUFFIX.length);
878
- const component = new CompactionComponent(compactionEntry.tokensBefore, summary);
879
- component.setExpanded(this.toolOutputExpanded);
880
- this.chatContainer.addChild(component);
881
- }
882
- else {
883
- const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
884
- this.chatContainer.addChild(userComponent);
885
- this.isFirstUserMessage = false;
886
- }
887
- }
888
- }
889
- else if (message.role === "assistant") {
890
- const assistantMsg = message;
891
- const assistantComponent = new AssistantMessageComponent(assistantMsg, this.hideThinkingBlock);
892
- this.chatContainer.addChild(assistantComponent);
893
- for (const content of assistantMsg.content) {
894
- if (content.type === "toolCall") {
895
- const component = new ToolExecutionComponent(content.name, content.arguments);
896
- this.chatContainer.addChild(component);
897
- this.pendingTools.set(content.id, component);
898
- }
899
- }
900
- }
901
- else if (message.role === "toolResult") {
902
- const component = this.pendingTools.get(message.toolCallId);
903
- if (component) {
904
- component.updateResult({
905
- content: message.content,
906
- details: message.details,
907
- isError: message.isError,
908
- });
909
- this.pendingTools.delete(message.toolCallId);
910
- }
911
- }
912
- }
913
- this.pendingTools.clear();
914
- this.ui.requestRender();
915
- }
916
- handleCtrlC() {
917
- // Handle Ctrl+C double-press logic
918
- const now = Date.now();
919
- const timeSinceLastCtrlC = now - this.lastSigintTime;
920
- if (timeSinceLastCtrlC < 500) {
921
- // Second Ctrl+C within 500ms - exit
922
- this.stop();
923
- process.exit(0);
924
- }
925
- else {
926
- // First Ctrl+C - clear the editor
927
- this.clearEditor();
928
- this.lastSigintTime = now;
929
- }
930
- }
931
- updateEditorBorderColor() {
932
- if (this.isBashMode) {
933
- this.editor.borderColor = theme.getBashModeBorderColor();
934
- }
935
- else {
936
- const level = this.agent.state.thinkingLevel || "off";
937
- this.editor.borderColor = theme.getThinkingBorderColor(level);
938
- }
939
- this.ui.requestRender();
940
- }
941
- cycleThinkingLevel() {
942
- // Only cycle if model supports thinking
943
- if (!this.agent.state.model?.reasoning) {
944
- this.chatContainer.addChild(new Spacer(1));
945
- this.chatContainer.addChild(new Text(theme.fg("dim", "Current model does not support thinking"), 1, 0));
946
- this.ui.requestRender();
947
- return;
948
- }
949
- // xhigh is only available for codex-max models
950
- const modelId = this.agent.state.model?.id || "";
951
- const supportsXhigh = modelId.includes("codex-max");
952
- const levels = supportsXhigh
953
- ? ["off", "minimal", "low", "medium", "high", "xhigh"]
954
- : ["off", "minimal", "low", "medium", "high"];
955
- const currentLevel = this.agent.state.thinkingLevel || "off";
956
- const currentIndex = levels.indexOf(currentLevel);
957
- const nextIndex = (currentIndex + 1) % levels.length;
958
- const nextLevel = levels[nextIndex];
959
- // Apply the new thinking level
960
- this.agent.setThinkingLevel(nextLevel);
961
- // Save thinking level change to session and settings
962
- this.sessionManager.saveThinkingLevelChange(nextLevel);
963
- this.settingsManager.setDefaultThinkingLevel(nextLevel);
964
- // Update border color
965
- this.updateEditorBorderColor();
966
- // Show brief notification
967
- this.chatContainer.addChild(new Spacer(1));
968
- this.chatContainer.addChild(new Text(theme.fg("dim", `Thinking level: ${nextLevel}`), 1, 0));
969
- this.ui.requestRender();
970
- }
971
- async cycleModel() {
972
- // Use scoped models if available, otherwise all available models
973
- if (this.scopedModels.length > 0) {
974
- // Use scoped models with thinking levels
975
- if (this.scopedModels.length === 1) {
976
- this.chatContainer.addChild(new Spacer(1));
977
- this.chatContainer.addChild(new Text(theme.fg("dim", "Only one model in scope"), 1, 0));
978
- this.ui.requestRender();
979
- return;
980
- }
981
- const currentModel = this.agent.state.model;
982
- let currentIndex = this.scopedModels.findIndex((sm) => sm.model.id === currentModel?.id && sm.model.provider === currentModel?.provider);
983
- // If current model not in scope, start from first
984
- if (currentIndex === -1) {
985
- currentIndex = 0;
986
- }
987
- const nextIndex = (currentIndex + 1) % this.scopedModels.length;
988
- const nextEntry = this.scopedModels[nextIndex];
989
- const nextModel = nextEntry.model;
990
- const nextThinking = nextEntry.thinkingLevel;
991
- // Validate API key
992
- const apiKey = await getApiKeyForModel(nextModel);
993
- if (!apiKey) {
994
- this.showError(`No API key for ${nextModel.provider}/${nextModel.id}`);
995
- return;
996
- }
997
- // Switch model
998
- this.agent.setModel(nextModel);
999
- // Save model change to session and settings
1000
- this.sessionManager.saveModelChange(nextModel.provider, nextModel.id);
1001
- this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
1002
- // Apply thinking level (silently use "off" if model doesn't support thinking)
1003
- const effectiveThinking = nextModel.reasoning ? nextThinking : "off";
1004
- this.agent.setThinkingLevel(effectiveThinking);
1005
- this.sessionManager.saveThinkingLevelChange(effectiveThinking);
1006
- this.settingsManager.setDefaultThinkingLevel(effectiveThinking);
1007
- this.updateEditorBorderColor();
1008
- // Show notification
1009
- this.chatContainer.addChild(new Spacer(1));
1010
- const thinkingStr = nextModel.reasoning && nextThinking !== "off" ? ` (thinking: ${nextThinking})` : "";
1011
- this.chatContainer.addChild(new Text(theme.fg("dim", `Switched to ${nextModel.name || nextModel.id}${thinkingStr}`), 1, 0));
1012
- this.ui.requestRender();
1013
- }
1014
- else {
1015
- // Fallback to all available models (no thinking level changes)
1016
- const { models: availableModels, error } = await getAvailableModels();
1017
- if (error) {
1018
- this.showError(`Failed to load models: ${error}`);
1019
- return;
1020
- }
1021
- if (availableModels.length === 0) {
1022
- this.showError("No models available to cycle");
1023
- return;
1024
- }
1025
- if (availableModels.length === 1) {
1026
- this.chatContainer.addChild(new Spacer(1));
1027
- this.chatContainer.addChild(new Text(theme.fg("dim", "Only one model available"), 1, 0));
1028
- this.ui.requestRender();
1029
- return;
1030
- }
1031
- const currentModel = this.agent.state.model;
1032
- let currentIndex = availableModels.findIndex((m) => m.id === currentModel?.id && m.provider === currentModel?.provider);
1033
- // If current model not in scope, start from first
1034
- if (currentIndex === -1) {
1035
- currentIndex = 0;
1036
- }
1037
- const nextIndex = (currentIndex + 1) % availableModels.length;
1038
- const nextModel = availableModels[nextIndex];
1039
- // Validate API key
1040
- const apiKey = await getApiKeyForModel(nextModel);
1041
- if (!apiKey) {
1042
- this.showError(`No API key for ${nextModel.provider}/${nextModel.id}`);
1043
- return;
1044
- }
1045
- // Switch model
1046
- this.agent.setModel(nextModel);
1047
- // Save model change to session and settings
1048
- this.sessionManager.saveModelChange(nextModel.provider, nextModel.id);
1049
- this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
1050
- // Show notification
1051
- this.chatContainer.addChild(new Spacer(1));
1052
- this.chatContainer.addChild(new Text(theme.fg("dim", `Switched to ${nextModel.name || nextModel.id}`), 1, 0));
1053
- this.ui.requestRender();
1054
- }
1055
- }
1056
- toggleToolOutputExpansion() {
1057
- this.toolOutputExpanded = !this.toolOutputExpanded;
1058
- // Update all tool execution, compaction, and bash execution components
1059
- for (const child of this.chatContainer.children) {
1060
- if (child instanceof ToolExecutionComponent) {
1061
- child.setExpanded(this.toolOutputExpanded);
1062
- }
1063
- else if (child instanceof CompactionComponent) {
1064
- child.setExpanded(this.toolOutputExpanded);
1065
- }
1066
- else if (child instanceof BashExecutionComponent) {
1067
- child.setExpanded(this.toolOutputExpanded);
1068
- }
1069
- }
1070
- this.ui.requestRender();
1071
- }
1072
- toggleThinkingBlockVisibility() {
1073
- this.hideThinkingBlock = !this.hideThinkingBlock;
1074
- this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
1075
- // Update all assistant message components and rebuild their content
1076
- for (const child of this.chatContainer.children) {
1077
- if (child instanceof AssistantMessageComponent) {
1078
- child.setHideThinkingBlock(this.hideThinkingBlock);
1079
- }
1080
- }
1081
- // Rebuild chat to apply visibility change
1082
- this.chatContainer.clear();
1083
- this.rebuildChatFromMessages();
1084
- // Show brief notification
1085
- const status = this.hideThinkingBlock ? "hidden" : "visible";
1086
- this.chatContainer.addChild(new Spacer(1));
1087
- this.chatContainer.addChild(new Text(theme.fg("dim", `Thinking blocks: ${status}`), 1, 0));
1088
- this.ui.requestRender();
1089
- }
1090
- clearEditor() {
1091
- this.editor.setText("");
1092
- this.ui.requestRender();
1093
- }
1094
- showError(errorMessage) {
1095
- // Show error message in the chat
1096
- this.chatContainer.addChild(new Spacer(1));
1097
- this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
1098
- this.ui.requestRender();
1099
- }
1100
- showWarning(warningMessage) {
1101
- // Show warning message in the chat
1102
- this.chatContainer.addChild(new Spacer(1));
1103
- this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
1104
- this.ui.requestRender();
1105
- }
1106
- showNewVersionNotification(newVersion) {
1107
- // Show new version notification in the chat
1108
- this.chatContainer.addChild(new Spacer(1));
1109
- this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
1110
- this.chatContainer.addChild(new Text(theme.bold(theme.fg("warning", "Update Available")) +
1111
- "\n" +
1112
- theme.fg("muted", `New version ${newVersion} is available. Run: `) +
1113
- theme.fg("accent", "npm install -g @mariozechner/pi-coding-agent"), 1, 0));
1114
- this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
1115
- this.ui.requestRender();
1116
- }
1117
- showThinkingSelector() {
1118
- // Create thinking selector with current level
1119
- this.thinkingSelector = new ThinkingSelectorComponent(this.agent.state.thinkingLevel, (level) => {
1120
- // Apply the selected thinking level
1121
- this.agent.setThinkingLevel(level);
1122
- // Save thinking level change to session and settings
1123
- this.sessionManager.saveThinkingLevelChange(level);
1124
- this.settingsManager.setDefaultThinkingLevel(level);
1125
- // Update border color
1126
- this.updateEditorBorderColor();
1127
- // Show confirmation message with proper spacing
1128
- this.chatContainer.addChild(new Spacer(1));
1129
- const confirmText = new Text(theme.fg("dim", `Thinking level: ${level}`), 1, 0);
1130
- this.chatContainer.addChild(confirmText);
1131
- // Hide selector and show editor again
1132
- this.hideThinkingSelector();
1133
- this.ui.requestRender();
1134
- }, () => {
1135
- // Just hide the selector
1136
- this.hideThinkingSelector();
1137
- this.ui.requestRender();
1138
- });
1139
- // Replace editor with selector
1140
- this.editorContainer.clear();
1141
- this.editorContainer.addChild(this.thinkingSelector);
1142
- this.ui.setFocus(this.thinkingSelector.getSelectList());
1143
- this.ui.requestRender();
1144
- }
1145
- hideThinkingSelector() {
1146
- // Replace selector with editor in the container
1147
- this.editorContainer.clear();
1148
- this.editorContainer.addChild(this.editor);
1149
- this.thinkingSelector = null;
1150
- this.ui.setFocus(this.editor);
1151
- }
1152
- showQueueModeSelector() {
1153
- // Create queue mode selector with current mode
1154
- this.queueModeSelector = new QueueModeSelectorComponent(this.agent.getQueueMode(), (mode) => {
1155
- // Apply the selected queue mode
1156
- this.agent.setQueueMode(mode);
1157
- // Save queue mode to settings
1158
- this.settingsManager.setQueueMode(mode);
1159
- // Show confirmation message with proper spacing
1160
- this.chatContainer.addChild(new Spacer(1));
1161
- const confirmText = new Text(theme.fg("dim", `Queue mode: ${mode}`), 1, 0);
1162
- this.chatContainer.addChild(confirmText);
1163
- // Hide selector and show editor again
1164
- this.hideQueueModeSelector();
1165
- this.ui.requestRender();
1166
- }, () => {
1167
- // Just hide the selector
1168
- this.hideQueueModeSelector();
1169
- this.ui.requestRender();
1170
- });
1171
- // Replace editor with selector
1172
- this.editorContainer.clear();
1173
- this.editorContainer.addChild(this.queueModeSelector);
1174
- this.ui.setFocus(this.queueModeSelector.getSelectList());
1175
- this.ui.requestRender();
1176
- }
1177
- hideQueueModeSelector() {
1178
- // Replace selector with editor in the container
1179
- this.editorContainer.clear();
1180
- this.editorContainer.addChild(this.editor);
1181
- this.queueModeSelector = null;
1182
- this.ui.setFocus(this.editor);
1183
- }
1184
- showThemeSelector() {
1185
- // Get current theme from settings
1186
- const currentTheme = this.settingsManager.getTheme() || "dark";
1187
- // Create theme selector
1188
- this.themeSelector = new ThemeSelectorComponent(currentTheme, (themeName) => {
1189
- // Apply the selected theme
1190
- const result = setTheme(themeName);
1191
- // Save theme to settings
1192
- this.settingsManager.setTheme(themeName);
1193
- // Invalidate all components to clear cached rendering
1194
- this.ui.invalidate();
1195
- // Show confirmation or error message
1196
- this.chatContainer.addChild(new Spacer(1));
1197
- if (result.success) {
1198
- const confirmText = new Text(theme.fg("dim", `Theme: ${themeName}`), 1, 0);
1199
- this.chatContainer.addChild(confirmText);
1200
- }
1201
- else {
1202
- const errorText = new Text(theme.fg("error", `Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`), 1, 0);
1203
- this.chatContainer.addChild(errorText);
1204
- }
1205
- // Hide selector and show editor again
1206
- this.hideThemeSelector();
1207
- this.ui.requestRender();
1208
- }, () => {
1209
- // Just hide the selector
1210
- this.hideThemeSelector();
1211
- this.ui.requestRender();
1212
- }, (themeName) => {
1213
- // Preview theme on selection change
1214
- const result = setTheme(themeName);
1215
- if (result.success) {
1216
- this.ui.invalidate();
1217
- this.ui.requestRender();
1218
- }
1219
- // If failed, theme already fell back to dark, just don't re-render
1220
- });
1221
- // Replace editor with selector
1222
- this.editorContainer.clear();
1223
- this.editorContainer.addChild(this.themeSelector);
1224
- this.ui.setFocus(this.themeSelector.getSelectList());
1225
- this.ui.requestRender();
1226
- }
1227
- hideThemeSelector() {
1228
- // Replace selector with editor in the container
1229
- this.editorContainer.clear();
1230
- this.editorContainer.addChild(this.editor);
1231
- this.themeSelector = null;
1232
- this.ui.setFocus(this.editor);
1233
- }
1234
- showModelSelector() {
1235
- // Create model selector with current model
1236
- this.modelSelector = new ModelSelectorComponent(this.ui, this.agent.state.model, this.settingsManager, (model) => {
1237
- // Apply the selected model
1238
- this.agent.setModel(model);
1239
- // Save model change to session
1240
- this.sessionManager.saveModelChange(model.provider, model.id);
1241
- // Show confirmation message with proper spacing
1242
- this.chatContainer.addChild(new Spacer(1));
1243
- const confirmText = new Text(theme.fg("dim", `Model: ${model.id}`), 1, 0);
1244
- this.chatContainer.addChild(confirmText);
1245
- // Hide selector and show editor again
1246
- this.hideModelSelector();
1247
- this.ui.requestRender();
1248
- }, () => {
1249
- // Just hide the selector
1250
- this.hideModelSelector();
1251
- this.ui.requestRender();
1252
- });
1253
- // Replace editor with selector
1254
- this.editorContainer.clear();
1255
- this.editorContainer.addChild(this.modelSelector);
1256
- this.ui.setFocus(this.modelSelector);
1257
- this.ui.requestRender();
1258
- }
1259
- hideModelSelector() {
1260
- // Replace selector with editor in the container
1261
- this.editorContainer.clear();
1262
- this.editorContainer.addChild(this.editor);
1263
- this.modelSelector = null;
1264
- this.ui.setFocus(this.editor);
1265
- }
1266
- showUserMessageSelector() {
1267
- // Read from session file directly to see ALL historical user messages
1268
- // (including those before compaction events)
1269
- const entries = this.sessionManager.loadEntries();
1270
- const userMessages = [];
1271
- const getUserMessageText = (content) => {
1272
- if (typeof content === "string")
1273
- return content;
1274
- if (Array.isArray(content)) {
1275
- return content
1276
- .filter((c) => c.type === "text")
1277
- .map((c) => c.text)
1278
- .join("");
1279
- }
1280
- return "";
1281
- };
1282
- for (let i = 0; i < entries.length; i++) {
1283
- const entry = entries[i];
1284
- if (entry.type !== "message")
1285
- continue;
1286
- if (entry.message.role !== "user")
1287
- continue;
1288
- const textContent = getUserMessageText(entry.message.content);
1289
- if (textContent) {
1290
- userMessages.push({ index: i, text: textContent });
1291
- }
1292
- }
1293
- // Don't show selector if there are no messages or only one message
1294
- if (userMessages.length <= 1) {
1295
- this.chatContainer.addChild(new Spacer(1));
1296
- this.chatContainer.addChild(new Text(theme.fg("dim", "No messages to branch from"), 1, 0));
1297
- this.ui.requestRender();
1298
- return;
1299
- }
1300
- // Create user message selector
1301
- this.userMessageSelector = new UserMessageSelectorComponent(userMessages, (entryIndex) => {
1302
- // Get the selected user message text to put in the editor
1303
- const selectedEntry = entries[entryIndex];
1304
- if (selectedEntry.type !== "message")
1305
- return;
1306
- if (selectedEntry.message.role !== "user")
1307
- return;
1308
- const selectedText = getUserMessageText(selectedEntry.message.content);
1309
- // Create a branched session by copying entries up to (but not including) the selected entry
1310
- const newSessionFile = this.sessionManager.createBranchedSessionFromEntries(entries, entryIndex);
1311
- // Set the new session file as active
1312
- this.sessionManager.setSessionFile(newSessionFile);
1313
- // Reload the session
1314
- const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
1315
- this.agent.replaceMessages(loaded.messages);
1316
- // Clear and re-render the chat
1317
- this.chatContainer.clear();
1318
- this.isFirstUserMessage = true;
1319
- this.renderInitialMessages(this.agent.state);
1320
- // Show confirmation message
1321
- this.chatContainer.addChild(new Spacer(1));
1322
- this.chatContainer.addChild(new Text(theme.fg("dim", "Branched to new session"), 1, 0));
1323
- // Put the selected message in the editor
1324
- this.editor.setText(selectedText);
1325
- // Hide selector and show editor again
1326
- this.hideUserMessageSelector();
1327
- this.ui.requestRender();
1328
- }, () => {
1329
- // Just hide the selector
1330
- this.hideUserMessageSelector();
1331
- this.ui.requestRender();
1332
- });
1333
- // Replace editor with selector
1334
- this.editorContainer.clear();
1335
- this.editorContainer.addChild(this.userMessageSelector);
1336
- this.ui.setFocus(this.userMessageSelector.getMessageList());
1337
- this.ui.requestRender();
1338
- }
1339
- hideUserMessageSelector() {
1340
- // Replace selector with editor in the container
1341
- this.editorContainer.clear();
1342
- this.editorContainer.addChild(this.editor);
1343
- this.userMessageSelector = null;
1344
- this.ui.setFocus(this.editor);
1345
- }
1346
- showSessionSelector() {
1347
- // Create session selector
1348
- this.sessionSelector = new SessionSelectorComponent(this.sessionManager, async (sessionPath) => {
1349
- this.hideSessionSelector();
1350
- await this.handleResumeSession(sessionPath);
1351
- }, () => {
1352
- // Just hide the selector
1353
- this.hideSessionSelector();
1354
- this.ui.requestRender();
1355
- });
1356
- // Replace editor with selector
1357
- this.editorContainer.clear();
1358
- this.editorContainer.addChild(this.sessionSelector);
1359
- this.ui.setFocus(this.sessionSelector.getSessionList());
1360
- this.ui.requestRender();
1361
- }
1362
- async handleResumeSession(sessionPath) {
1363
- // Unsubscribe first to prevent processing events during transition
1364
- this.unsubscribe?.();
1365
- // Abort and wait for completion
1366
- this.agent.abort();
1367
- await this.agent.waitForIdle();
1368
- // Stop loading animation
1369
- if (this.loadingAnimation) {
1370
- this.loadingAnimation.stop();
1371
- this.loadingAnimation = null;
1372
- }
1373
- this.statusContainer.clear();
1374
- // Clear UI state
1375
- this.queuedMessages = [];
1376
- this.pendingMessagesContainer.clear();
1377
- this.streamingComponent = null;
1378
- this.pendingTools.clear();
1379
- // Set the selected session as active
1380
- this.sessionManager.setSessionFile(sessionPath);
1381
- // Reload the session
1382
- const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
1383
- this.agent.replaceMessages(loaded.messages);
1384
- // Restore model if saved in session
1385
- const savedModel = this.sessionManager.loadModel();
1386
- if (savedModel) {
1387
- const availableModels = (await getAvailableModels()).models;
1388
- const match = availableModels.find((m) => m.provider === savedModel.provider && m.id === savedModel.modelId);
1389
- if (match) {
1390
- this.agent.setModel(match);
1391
- }
1392
- }
1393
- // Restore thinking level if saved in session
1394
- const savedThinking = this.sessionManager.loadThinkingLevel();
1395
- if (savedThinking) {
1396
- this.agent.setThinkingLevel(savedThinking);
1397
- }
1398
- // Resubscribe to agent
1399
- this.subscribeToAgent();
1400
- // Clear and re-render the chat
1401
- this.chatContainer.clear();
1402
- this.isFirstUserMessage = true;
1403
- this.renderInitialMessages(this.agent.state);
1404
- // Show confirmation message
1405
- this.chatContainer.addChild(new Spacer(1));
1406
- this.chatContainer.addChild(new Text(theme.fg("dim", "Resumed session"), 1, 0));
1407
- this.ui.requestRender();
1408
- }
1409
- hideSessionSelector() {
1410
- // Replace selector with editor in the container
1411
- this.editorContainer.clear();
1412
- this.editorContainer.addChild(this.editor);
1413
- this.sessionSelector = null;
1414
- this.ui.setFocus(this.editor);
1415
- }
1416
- async showOAuthSelector(mode) {
1417
- // For logout mode, filter to only show logged-in providers
1418
- let providersToShow = [];
1419
- if (mode === "logout") {
1420
- const loggedInProviders = listOAuthProviders();
1421
- if (loggedInProviders.length === 0) {
1422
- this.chatContainer.addChild(new Spacer(1));
1423
- this.chatContainer.addChild(new Text(theme.fg("dim", "No OAuth providers logged in. Use /login first."), 1, 0));
1424
- this.ui.requestRender();
1425
- return;
1426
- }
1427
- providersToShow = loggedInProviders;
1428
- }
1429
- // Create OAuth selector
1430
- this.oauthSelector = new OAuthSelectorComponent(mode, async (providerId) => {
1431
- // Hide selector first
1432
- this.hideOAuthSelector();
1433
- if (mode === "login") {
1434
- // Handle login
1435
- this.chatContainer.addChild(new Spacer(1));
1436
- this.chatContainer.addChild(new Text(theme.fg("dim", `Logging in to ${providerId}...`), 1, 0));
1437
- this.ui.requestRender();
1438
- try {
1439
- await login(providerId, (url) => {
1440
- // Show auth URL to user
1441
- this.chatContainer.addChild(new Spacer(1));
1442
- this.chatContainer.addChild(new Text(theme.fg("accent", "Opening browser to:"), 1, 0));
1443
- this.chatContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
1444
- this.chatContainer.addChild(new Spacer(1));
1445
- this.chatContainer.addChild(new Text(theme.fg("warning", "Paste the authorization code below:"), 1, 0));
1446
- this.ui.requestRender();
1447
- // Open URL in browser
1448
- const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1449
- exec(`${openCmd} "${url}"`);
1450
- }, async () => {
1451
- // Prompt for code with a simple Input
1452
- return new Promise((resolve) => {
1453
- const codeInput = new Input();
1454
- codeInput.onSubmit = () => {
1455
- const code = codeInput.getValue();
1456
- // Restore editor
1457
- this.editorContainer.clear();
1458
- this.editorContainer.addChild(this.editor);
1459
- this.ui.setFocus(this.editor);
1460
- resolve(code);
1461
- };
1462
- this.editorContainer.clear();
1463
- this.editorContainer.addChild(codeInput);
1464
- this.ui.setFocus(codeInput);
1465
- this.ui.requestRender();
1466
- });
1467
- });
1468
- // Success - invalidate OAuth cache so footer updates
1469
- invalidateOAuthCache();
1470
- this.chatContainer.addChild(new Spacer(1));
1471
- this.chatContainer.addChild(new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0));
1472
- this.chatContainer.addChild(new Text(theme.fg("dim", `Tokens saved to ${getOAuthPath()}`), 1, 0));
1473
- this.ui.requestRender();
1474
- }
1475
- catch (error) {
1476
- this.showError(`Login failed: ${error.message}`);
1477
- }
1478
- }
1479
- else {
1480
- // Handle logout
1481
- try {
1482
- await logout(providerId);
1483
- // Invalidate OAuth cache so footer updates
1484
- invalidateOAuthCache();
1485
- this.chatContainer.addChild(new Spacer(1));
1486
- this.chatContainer.addChild(new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0));
1487
- this.chatContainer.addChild(new Text(theme.fg("dim", `Credentials removed from ${getOAuthPath()}`), 1, 0));
1488
- this.ui.requestRender();
1489
- }
1490
- catch (error) {
1491
- this.showError(`Logout failed: ${error.message}`);
1492
- }
1493
- }
1494
- }, () => {
1495
- // Cancel - just hide the selector
1496
- this.hideOAuthSelector();
1497
- this.ui.requestRender();
1498
- });
1499
- // Replace editor with selector
1500
- this.editorContainer.clear();
1501
- this.editorContainer.addChild(this.oauthSelector);
1502
- this.ui.setFocus(this.oauthSelector);
1503
- this.ui.requestRender();
1504
- }
1505
- hideOAuthSelector() {
1506
- // Replace selector with editor in the container
1507
- this.editorContainer.clear();
1508
- this.editorContainer.addChild(this.editor);
1509
- this.oauthSelector = null;
1510
- this.ui.setFocus(this.editor);
1511
- }
1512
- handleExportCommand(text) {
1513
- // Parse optional filename from command: /export [filename]
1514
- const parts = text.split(/\s+/);
1515
- const outputPath = parts.length > 1 ? parts[1] : undefined;
1516
- try {
1517
- // Export session to HTML
1518
- const filePath = exportSessionToHtml(this.sessionManager, this.agent.state, outputPath);
1519
- // Show success message in chat - matching thinking level style
1520
- this.chatContainer.addChild(new Spacer(1));
1521
- this.chatContainer.addChild(new Text(theme.fg("dim", `Session exported to: ${filePath}`), 1, 0));
1522
- this.ui.requestRender();
1523
- }
1524
- catch (error) {
1525
- // Show error message in chat
1526
- this.chatContainer.addChild(new Spacer(1));
1527
- this.chatContainer.addChild(new Text(theme.fg("error", `Failed to export session: ${error.message || "Unknown error"}`), 1, 0));
1528
- this.ui.requestRender();
1529
- }
1530
- }
1531
- handleCopyCommand() {
1532
- // Find the last assistant message
1533
- const lastAssistantMessage = this.agent.state.messages
1534
- .slice()
1535
- .reverse()
1536
- .find((m) => m.role === "assistant");
1537
- if (!lastAssistantMessage) {
1538
- this.showError("No agent messages to copy yet.");
1539
- return;
1540
- }
1541
- // Extract raw text content from all text blocks
1542
- let textContent = "";
1543
- for (const content of lastAssistantMessage.content) {
1544
- if (content.type === "text") {
1545
- textContent += content.text;
1546
- }
1547
- }
1548
- if (!textContent.trim()) {
1549
- this.showError("Last agent message contains no text content.");
1550
- return;
1551
- }
1552
- // Copy to clipboard using cross-platform compatible method
1553
- try {
1554
- copyToClipboard(textContent);
1555
- }
1556
- catch (error) {
1557
- this.showError(error instanceof Error ? error.message : String(error));
1558
- return;
1559
- }
1560
- // Show confirmation message
1561
- this.chatContainer.addChild(new Spacer(1));
1562
- this.chatContainer.addChild(new Text(theme.fg("dim", "Copied last agent message to clipboard"), 1, 0));
1563
- this.ui.requestRender();
1564
- }
1565
- handleSessionCommand() {
1566
- // Get session info
1567
- const sessionFile = this.sessionManager.getSessionFile();
1568
- const state = this.agent.state;
1569
- // Count messages
1570
- const userMessages = state.messages.filter((m) => m.role === "user").length;
1571
- const assistantMessages = state.messages.filter((m) => m.role === "assistant").length;
1572
- const toolResults = state.messages.filter((m) => m.role === "toolResult").length;
1573
- const totalMessages = state.messages.length;
1574
- // Count tool calls from assistant messages
1575
- let toolCalls = 0;
1576
- for (const message of state.messages) {
1577
- if (message.role === "assistant") {
1578
- const assistantMsg = message;
1579
- toolCalls += assistantMsg.content.filter((c) => c.type === "toolCall").length;
1580
- }
1581
- }
1582
- // Calculate cumulative usage from all assistant messages (same as footer)
1583
- let totalInput = 0;
1584
- let totalOutput = 0;
1585
- let totalCacheRead = 0;
1586
- let totalCacheWrite = 0;
1587
- let totalCost = 0;
1588
- for (const message of state.messages) {
1589
- if (message.role === "assistant") {
1590
- const assistantMsg = message;
1591
- totalInput += assistantMsg.usage.input;
1592
- totalOutput += assistantMsg.usage.output;
1593
- totalCacheRead += assistantMsg.usage.cacheRead;
1594
- totalCacheWrite += assistantMsg.usage.cacheWrite;
1595
- totalCost += assistantMsg.usage.cost.total;
1596
- }
1597
- }
1598
- const totalTokens = totalInput + totalOutput + totalCacheRead + totalCacheWrite;
1599
- // Build info text
1600
- let info = `${theme.bold("Session Info")}\n\n`;
1601
- info += `${theme.fg("dim", "File:")} ${sessionFile}\n`;
1602
- info += `${theme.fg("dim", "ID:")} ${this.sessionManager.getSessionId()}\n\n`;
1603
- info += `${theme.bold("Messages")}\n`;
1604
- info += `${theme.fg("dim", "User:")} ${userMessages}\n`;
1605
- info += `${theme.fg("dim", "Assistant:")} ${assistantMessages}\n`;
1606
- info += `${theme.fg("dim", "Tool Calls:")} ${toolCalls}\n`;
1607
- info += `${theme.fg("dim", "Tool Results:")} ${toolResults}\n`;
1608
- info += `${theme.fg("dim", "Total:")} ${totalMessages}\n\n`;
1609
- info += `${theme.bold("Tokens")}\n`;
1610
- info += `${theme.fg("dim", "Input:")} ${totalInput.toLocaleString()}\n`;
1611
- info += `${theme.fg("dim", "Output:")} ${totalOutput.toLocaleString()}\n`;
1612
- if (totalCacheRead > 0) {
1613
- info += `${theme.fg("dim", "Cache Read:")} ${totalCacheRead.toLocaleString()}\n`;
1614
- }
1615
- if (totalCacheWrite > 0) {
1616
- info += `${theme.fg("dim", "Cache Write:")} ${totalCacheWrite.toLocaleString()}\n`;
1617
- }
1618
- info += `${theme.fg("dim", "Total:")} ${totalTokens.toLocaleString()}\n`;
1619
- if (totalCost > 0) {
1620
- info += `\n${theme.bold("Cost")}\n`;
1621
- info += `${theme.fg("dim", "Total:")} ${totalCost.toFixed(4)}`;
1622
- }
1623
- // Show info in chat
1624
- this.chatContainer.addChild(new Spacer(1));
1625
- this.chatContainer.addChild(new Text(info, 1, 0));
1626
- this.ui.requestRender();
1627
- }
1628
- handleChangelogCommand() {
1629
- const changelogPath = getChangelogPath();
1630
- const allEntries = parseChangelog(changelogPath);
1631
- // Show all entries in reverse order (oldest first, newest last)
1632
- const changelogMarkdown = allEntries.length > 0
1633
- ? allEntries
1634
- .reverse()
1635
- .map((e) => e.content)
1636
- .join("\n\n")
1637
- : "No changelog entries found.";
1638
- // Display in chat
1639
- this.chatContainer.addChild(new Spacer(1));
1640
- this.chatContainer.addChild(new DynamicBorder());
1641
- this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
1642
- this.ui.addChild(new Spacer(1));
1643
- this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));
1644
- this.chatContainer.addChild(new DynamicBorder());
1645
- this.ui.requestRender();
1646
- }
1647
- async handleClearCommand() {
1648
- // Unsubscribe first to prevent processing abort events
1649
- this.unsubscribe?.();
1650
- // Abort and wait for completion
1651
- this.agent.abort();
1652
- await this.agent.waitForIdle();
1653
- // Stop loading animation
1654
- if (this.loadingAnimation) {
1655
- this.loadingAnimation.stop();
1656
- this.loadingAnimation = null;
1657
- }
1658
- this.statusContainer.clear();
1659
- // Reset agent and session
1660
- this.agent.reset();
1661
- this.sessionManager.reset();
1662
- // Resubscribe to agent
1663
- this.subscribeToAgent();
1664
- // Clear UI state
1665
- this.chatContainer.clear();
1666
- this.pendingMessagesContainer.clear();
1667
- this.queuedMessages = [];
1668
- this.streamingComponent = null;
1669
- this.pendingTools.clear();
1670
- this.isFirstUserMessage = true;
1671
- // Show confirmation
1672
- this.chatContainer.addChild(new Spacer(1));
1673
- this.chatContainer.addChild(new Text(theme.fg("accent", "✓ Context cleared") + "\n" + theme.fg("muted", "Started fresh session"), 1, 1));
1674
- this.ui.requestRender();
1675
- }
1676
- handleDebugCommand() {
1677
- // Force a render and capture all lines with their widths
1678
- const width = this.ui.terminal.columns;
1679
- const allLines = this.ui.render(width);
1680
- const debugLogPath = getDebugLogPath();
1681
- const debugData = [
1682
- `Debug output at ${new Date().toISOString()}`,
1683
- `Terminal width: ${width}`,
1684
- `Total lines: ${allLines.length}`,
1685
- "",
1686
- "=== All rendered lines with visible widths ===",
1687
- ...allLines.map((line, idx) => {
1688
- const vw = visibleWidth(line);
1689
- const escaped = JSON.stringify(line);
1690
- return `[${idx}] (w=${vw}) ${escaped}`;
1691
- }),
1692
- "",
1693
- "=== Agent messages (JSONL) ===",
1694
- ...this.agent.state.messages.map((msg) => JSON.stringify(msg)),
1695
- "",
1696
- ].join("\n");
1697
- fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
1698
- fs.writeFileSync(debugLogPath, debugData);
1699
- // Show confirmation
1700
- this.chatContainer.addChild(new Spacer(1));
1701
- this.chatContainer.addChild(new Text(theme.fg("accent", "✓ Debug log written") + "\n" + theme.fg("muted", debugLogPath), 1, 1));
1702
- this.ui.requestRender();
1703
- }
1704
- async handleBashCommand(command) {
1705
- // Create component and add to chat
1706
- this.bashComponent = new BashExecutionComponent(command, this.ui);
1707
- this.chatContainer.addChild(this.bashComponent);
1708
- this.ui.requestRender();
1709
- try {
1710
- const result = await this.executeBashCommand(command, (chunk) => {
1711
- if (this.bashComponent) {
1712
- this.bashComponent.appendOutput(chunk);
1713
- this.ui.requestRender();
1714
- }
1715
- });
1716
- if (this.bashComponent) {
1717
- this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncationResult, result.fullOutputPath);
1718
- // Create and save message (even if cancelled, for consistency with LLM aborts)
1719
- const bashMessage = {
1720
- role: "bashExecution",
1721
- command,
1722
- output: result.truncationResult?.content || this.bashComponent.getOutput(),
1723
- exitCode: result.exitCode,
1724
- cancelled: result.cancelled,
1725
- truncated: result.truncationResult?.truncated || false,
1726
- fullOutputPath: result.fullOutputPath,
1727
- timestamp: Date.now(),
1728
- };
1729
- // Add to agent state
1730
- this.agent.appendMessage(bashMessage);
1731
- // Save to session
1732
- this.sessionManager.saveMessage(bashMessage);
1733
- }
1734
- }
1735
- catch (error) {
1736
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1737
- if (this.bashComponent) {
1738
- this.bashComponent.setComplete(null, false);
1739
- }
1740
- this.showError(`Bash command failed: ${errorMessage}`);
1741
- }
1742
- this.bashComponent = null;
1743
- this.ui.requestRender();
1744
- }
1745
- executeBashCommand(command, onChunk) {
1746
- return new Promise((resolve, reject) => {
1747
- const { shell, args } = getShellConfig();
1748
- const child = spawn(shell, [...args, command], {
1749
- detached: true,
1750
- stdio: ["ignore", "pipe", "pipe"],
1751
- });
1752
- this.bashProcess = child;
1753
- // Track sanitized output for truncation
1754
- const outputChunks = [];
1755
- let outputBytes = 0;
1756
- const maxOutputBytes = DEFAULT_MAX_BYTES * 2;
1757
- // Temp file for large output
1758
- let tempFilePath;
1759
- let tempFileStream;
1760
- let totalBytes = 0;
1761
- const handleData = (data) => {
1762
- totalBytes += data.length;
1763
- // Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines
1764
- const text = sanitizeBinaryOutput(stripAnsi(data.toString())).replace(/\r/g, "");
1765
- // Start writing to temp file if exceeds threshold
1766
- if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
1767
- const id = randomBytes(8).toString("hex");
1768
- tempFilePath = join(tmpdir(), `pi-bash-${id}.log`);
1769
- tempFileStream = createWriteStream(tempFilePath);
1770
- for (const chunk of outputChunks) {
1771
- tempFileStream.write(chunk);
1772
- }
1773
- }
1774
- if (tempFileStream) {
1775
- tempFileStream.write(text);
1776
- }
1777
- // Keep rolling buffer of sanitized text
1778
- outputChunks.push(text);
1779
- outputBytes += text.length;
1780
- while (outputBytes > maxOutputBytes && outputChunks.length > 1) {
1781
- const removed = outputChunks.shift();
1782
- outputBytes -= removed.length;
1783
- }
1784
- // Stream to component
1785
- onChunk(text);
1786
- };
1787
- child.stdout?.on("data", handleData);
1788
- child.stderr?.on("data", handleData);
1789
- child.on("close", (code) => {
1790
- if (tempFileStream) {
1791
- tempFileStream.end();
1792
- }
1793
- this.bashProcess = null;
1794
- // Combine buffered chunks for truncation (already sanitized)
1795
- const fullOutput = outputChunks.join("");
1796
- const truncationResult = truncateTail(fullOutput);
1797
- // code === null means killed (cancelled)
1798
- const cancelled = code === null;
1799
- resolve({
1800
- exitCode: code,
1801
- cancelled,
1802
- truncationResult: truncationResult.truncated ? truncationResult : undefined,
1803
- fullOutputPath: tempFilePath,
1804
- });
1805
- });
1806
- child.on("error", (err) => {
1807
- if (tempFileStream) {
1808
- tempFileStream.end();
1809
- }
1810
- this.bashProcess = null;
1811
- reject(err);
1812
- });
1813
- });
1814
- }
1815
- compactionAbortController = null;
1816
- /**
1817
- * Shared logic to execute context compaction.
1818
- * Handles aborting agent, showing loader, performing compaction, updating session/UI.
1819
- */
1820
- async executeCompaction(customInstructions, isAuto = false) {
1821
- // Unsubscribe first to prevent processing events during compaction
1822
- this.unsubscribe?.();
1823
- // Abort and wait for completion
1824
- this.agent.abort();
1825
- await this.agent.waitForIdle();
1826
- // Stop loading animation
1827
- if (this.loadingAnimation) {
1828
- this.loadingAnimation.stop();
1829
- this.loadingAnimation = null;
1830
- }
1831
- this.statusContainer.clear();
1832
- // Create abort controller for compaction
1833
- this.compactionAbortController = new AbortController();
1834
- // Set up escape handler during compaction
1835
- const originalOnEscape = this.editor.onEscape;
1836
- this.editor.onEscape = () => {
1837
- if (this.compactionAbortController) {
1838
- this.compactionAbortController.abort();
1839
- }
1840
- };
1841
- // Show compacting status with loader
1842
- this.chatContainer.addChild(new Spacer(1));
1843
- const label = isAuto ? "Auto-compacting context... (esc to cancel)" : "Compacting context... (esc to cancel)";
1844
- const compactingLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
1845
- this.statusContainer.addChild(compactingLoader);
1846
- this.ui.requestRender();
1847
- try {
1848
- // Get API key for current model
1849
- const apiKey = await getApiKeyForModel(this.agent.state.model);
1850
- if (!apiKey) {
1851
- throw new Error(`No API key for ${this.agent.state.model.provider}`);
1852
- }
1853
- // Perform compaction with abort signal
1854
- const entries = this.sessionManager.loadEntries();
1855
- const settings = this.settingsManager.getCompactionSettings();
1856
- const compactionEntry = await compact(entries, this.agent.state.model, settings, apiKey, this.compactionAbortController.signal, customInstructions);
1857
- // Check if aborted after compact returned
1858
- if (this.compactionAbortController.signal.aborted) {
1859
- throw new Error("Compaction cancelled");
1860
- }
1861
- // Save compaction to session
1862
- this.sessionManager.saveCompaction(compactionEntry);
1863
- // Reload session
1864
- const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
1865
- this.agent.replaceMessages(loaded.messages);
1866
- // Rebuild UI
1867
- this.chatContainer.clear();
1868
- this.rebuildChatFromMessages();
1869
- // Add compaction component at current position so user can see/expand the summary
1870
- const compactionComponent = new CompactionComponent(compactionEntry.tokensBefore, compactionEntry.summary);
1871
- compactionComponent.setExpanded(this.toolOutputExpanded);
1872
- this.chatContainer.addChild(compactionComponent);
1873
- // Update footer with new state (fixes context % display)
1874
- this.footer.updateState(this.agent.state);
1875
- }
1876
- catch (error) {
1877
- const message = error instanceof Error ? error.message : String(error);
1878
- if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
1879
- this.showError("Compaction cancelled");
1880
- }
1881
- else {
1882
- this.showError(`Compaction failed: ${message}`);
1883
- }
1884
- }
1885
- finally {
1886
- // Clean up
1887
- compactingLoader.stop();
1888
- this.statusContainer.clear();
1889
- this.compactionAbortController = null;
1890
- this.editor.onEscape = originalOnEscape;
1891
- }
1892
- // Resubscribe to agent
1893
- this.subscribeToAgent();
1894
- }
1895
- async handleCompactCommand(customInstructions) {
1896
- // Check if there are any messages to compact
1897
- const entries = this.sessionManager.loadEntries();
1898
- const messageCount = entries.filter((e) => e.type === "message").length;
1899
- if (messageCount < 2) {
1900
- this.showWarning("Nothing to compact (no messages yet)");
1901
- return;
1902
- }
1903
- await this.executeCompaction(customInstructions, false);
1904
- }
1905
- handleAutocompactCommand() {
1906
- const currentEnabled = this.settingsManager.getCompactionEnabled();
1907
- const newState = !currentEnabled;
1908
- this.settingsManager.setCompactionEnabled(newState);
1909
- this.footer.setAutoCompactEnabled(newState);
1910
- // Show brief notification (same style as thinking level toggle)
1911
- this.chatContainer.addChild(new Spacer(1));
1912
- this.chatContainer.addChild(new Text(theme.fg("dim", `Auto-compaction: ${newState ? "on" : "off"}`), 1, 0));
1913
- this.ui.requestRender();
1914
- }
1915
- updatePendingMessagesDisplay() {
1916
- this.pendingMessagesContainer.clear();
1917
- if (this.queuedMessages.length > 0) {
1918
- this.pendingMessagesContainer.addChild(new Spacer(1));
1919
- for (const message of this.queuedMessages) {
1920
- const queuedText = theme.fg("dim", "Queued: " + message);
1921
- this.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
1922
- }
1923
- }
1924
- }
1925
- stop() {
1926
- if (this.loadingAnimation) {
1927
- this.loadingAnimation.stop();
1928
- this.loadingAnimation = null;
1929
- }
1930
- this.footer.dispose();
1931
- if (this.isInitialized) {
1932
- this.ui.stop();
1933
- this.isInitialized = false;
1934
- }
1935
- }
1936
- }
1937
- //# sourceMappingURL=tui-renderer.js.map