@mariozechner/pi-coding-agent 0.14.1 → 0.15.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 (308) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +10 -1
  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 +7 -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 +2 -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-mode.d.ts +21 -0
  153. package/dist/modes/rpc-mode.d.ts.map +1 -0
  154. package/dist/modes/rpc-mode.js +77 -0
  155. package/dist/modes/rpc-mode.js.map +1 -0
  156. package/dist/{changelog.d.ts → utils/changelog.d.ts} +1 -1
  157. package/dist/{changelog.js.map → utils/changelog.d.ts.map} +1 -1
  158. package/dist/{changelog.js → utils/changelog.js} +1 -1
  159. package/dist/utils/changelog.js.map +1 -0
  160. package/dist/utils/clipboard.d.ts.map +1 -0
  161. package/dist/utils/clipboard.js.map +1 -0
  162. package/dist/utils/fuzzy.d.ts.map +1 -0
  163. package/dist/utils/fuzzy.js.map +1 -0
  164. package/dist/{shell.d.ts → utils/shell.d.ts} +8 -0
  165. package/dist/utils/shell.d.ts.map +1 -0
  166. package/dist/{shell.js → utils/shell.js} +15 -1
  167. package/dist/utils/shell.js.map +1 -0
  168. package/dist/utils/tools-manager.d.ts.map +1 -0
  169. package/dist/{tools-manager.js → utils/tools-manager.js} +1 -1
  170. package/dist/utils/tools-manager.js.map +1 -0
  171. package/package.json +6 -6
  172. package/dist/changelog.d.ts.map +0 -1
  173. package/dist/clipboard.d.ts.map +0 -1
  174. package/dist/clipboard.js.map +0 -1
  175. package/dist/compaction.d.ts.map +0 -1
  176. package/dist/compaction.js.map +0 -1
  177. package/dist/export-html.js.map +0 -1
  178. package/dist/fuzzy.d.ts.map +0 -1
  179. package/dist/fuzzy.js.map +0 -1
  180. package/dist/messages.d.ts.map +0 -1
  181. package/dist/messages.js.map +0 -1
  182. package/dist/model-config.d.ts.map +0 -1
  183. package/dist/model-config.js.map +0 -1
  184. package/dist/oauth/anthropic.d.ts.map +0 -1
  185. package/dist/oauth/anthropic.js.map +0 -1
  186. package/dist/oauth/index.js.map +0 -1
  187. package/dist/oauth/storage.d.ts.map +0 -1
  188. package/dist/oauth/storage.js.map +0 -1
  189. package/dist/session-manager.d.ts.map +0 -1
  190. package/dist/session-manager.js.map +0 -1
  191. package/dist/settings-manager.d.ts.map +0 -1
  192. package/dist/settings-manager.js.map +0 -1
  193. package/dist/shell.d.ts.map +0 -1
  194. package/dist/shell.js.map +0 -1
  195. package/dist/slash-commands.d.ts.map +0 -1
  196. package/dist/slash-commands.js.map +0 -1
  197. package/dist/theme/theme.d.ts.map +0 -1
  198. package/dist/theme/theme.js.map +0 -1
  199. package/dist/tools/bash.d.ts.map +0 -1
  200. package/dist/tools/bash.js.map +0 -1
  201. package/dist/tools/edit.d.ts.map +0 -1
  202. package/dist/tools/edit.js.map +0 -1
  203. package/dist/tools/find.d.ts.map +0 -1
  204. package/dist/tools/find.js.map +0 -1
  205. package/dist/tools/grep.d.ts.map +0 -1
  206. package/dist/tools/grep.js.map +0 -1
  207. package/dist/tools/index.d.ts.map +0 -1
  208. package/dist/tools/index.js.map +0 -1
  209. package/dist/tools/ls.d.ts.map +0 -1
  210. package/dist/tools/ls.js.map +0 -1
  211. package/dist/tools/read.d.ts.map +0 -1
  212. package/dist/tools/read.js.map +0 -1
  213. package/dist/tools/truncate.d.ts.map +0 -1
  214. package/dist/tools/truncate.js.map +0 -1
  215. package/dist/tools/write.d.ts.map +0 -1
  216. package/dist/tools/write.js.map +0 -1
  217. package/dist/tools-manager.d.ts.map +0 -1
  218. package/dist/tools-manager.js.map +0 -1
  219. package/dist/tui/assistant-message.d.ts.map +0 -1
  220. package/dist/tui/assistant-message.js.map +0 -1
  221. package/dist/tui/bash-execution.d.ts.map +0 -1
  222. package/dist/tui/bash-execution.js.map +0 -1
  223. package/dist/tui/compaction.d.ts.map +0 -1
  224. package/dist/tui/compaction.js.map +0 -1
  225. package/dist/tui/custom-editor.d.ts.map +0 -1
  226. package/dist/tui/custom-editor.js.map +0 -1
  227. package/dist/tui/dynamic-border.d.ts.map +0 -1
  228. package/dist/tui/dynamic-border.js.map +0 -1
  229. package/dist/tui/footer.d.ts.map +0 -1
  230. package/dist/tui/footer.js.map +0 -1
  231. package/dist/tui/model-selector.d.ts.map +0 -1
  232. package/dist/tui/model-selector.js.map +0 -1
  233. package/dist/tui/oauth-selector.d.ts.map +0 -1
  234. package/dist/tui/oauth-selector.js.map +0 -1
  235. package/dist/tui/queue-mode-selector.d.ts.map +0 -1
  236. package/dist/tui/queue-mode-selector.js.map +0 -1
  237. package/dist/tui/session-selector.d.ts.map +0 -1
  238. package/dist/tui/session-selector.js.map +0 -1
  239. package/dist/tui/theme-selector.js.map +0 -1
  240. package/dist/tui/thinking-selector.d.ts.map +0 -1
  241. package/dist/tui/thinking-selector.js.map +0 -1
  242. package/dist/tui/tool-execution.d.ts.map +0 -1
  243. package/dist/tui/tool-execution.js.map +0 -1
  244. package/dist/tui/tui-renderer.d.ts.map +0 -1
  245. package/dist/tui/tui-renderer.js +0 -1934
  246. package/dist/tui/tui-renderer.js.map +0 -1
  247. package/dist/tui/user-message-selector.d.ts.map +0 -1
  248. package/dist/tui/user-message-selector.js.map +0 -1
  249. package/dist/tui/user-message.d.ts.map +0 -1
  250. package/dist/tui/user-message.js.map +0 -1
  251. /package/dist/{export-html.d.ts → core/export-html.d.ts} +0 -0
  252. /package/dist/{messages.d.ts → core/messages.d.ts} +0 -0
  253. /package/dist/{messages.js → core/messages.js} +0 -0
  254. /package/dist/{model-config.d.ts → core/model-config.d.ts} +0 -0
  255. /package/dist/{oauth → core/oauth}/anthropic.d.ts +0 -0
  256. /package/dist/{oauth → core/oauth}/anthropic.js +0 -0
  257. /package/dist/{oauth → core/oauth}/index.d.ts +0 -0
  258. /package/dist/{oauth → core/oauth}/index.js +0 -0
  259. /package/dist/{oauth → core/oauth}/storage.d.ts +0 -0
  260. /package/dist/{session-manager.d.ts → core/session-manager.d.ts} +0 -0
  261. /package/dist/{settings-manager.d.ts → core/settings-manager.d.ts} +0 -0
  262. /package/dist/{slash-commands.d.ts → core/slash-commands.d.ts} +0 -0
  263. /package/dist/{tools → core/tools}/bash.d.ts +0 -0
  264. /package/dist/{tools → core/tools}/edit.d.ts +0 -0
  265. /package/dist/{tools → core/tools}/edit.js +0 -0
  266. /package/dist/{tools → core/tools}/find.d.ts +0 -0
  267. /package/dist/{tools → core/tools}/grep.d.ts +0 -0
  268. /package/dist/{tools → core/tools}/index.d.ts +0 -0
  269. /package/dist/{tools → core/tools}/index.js +0 -0
  270. /package/dist/{tools → core/tools}/ls.d.ts +0 -0
  271. /package/dist/{tools → core/tools}/ls.js +0 -0
  272. /package/dist/{tools → core/tools}/read.d.ts +0 -0
  273. /package/dist/{tools → core/tools}/read.js +0 -0
  274. /package/dist/{tools → core/tools}/truncate.d.ts +0 -0
  275. /package/dist/{tools → core/tools}/truncate.js +0 -0
  276. /package/dist/{tools → core/tools}/write.d.ts +0 -0
  277. /package/dist/{tools → core/tools}/write.js +0 -0
  278. /package/dist/{tui → modes/interactive/components}/assistant-message.d.ts +0 -0
  279. /package/dist/{tui → modes/interactive/components}/assistant-message.js +0 -0
  280. /package/dist/{tui → modes/interactive/components}/compaction.d.ts +0 -0
  281. /package/dist/{tui → modes/interactive/components}/compaction.js +0 -0
  282. /package/dist/{tui → modes/interactive/components}/custom-editor.d.ts +0 -0
  283. /package/dist/{tui → modes/interactive/components}/custom-editor.js +0 -0
  284. /package/dist/{tui → modes/interactive/components}/dynamic-border.d.ts +0 -0
  285. /package/dist/{tui → modes/interactive/components}/dynamic-border.js +0 -0
  286. /package/dist/{tui → modes/interactive/components}/footer.d.ts +0 -0
  287. /package/dist/{tui → modes/interactive/components}/oauth-selector.d.ts +0 -0
  288. /package/dist/{tui → modes/interactive/components}/queue-mode-selector.d.ts +0 -0
  289. /package/dist/{tui → modes/interactive/components}/queue-mode-selector.js +0 -0
  290. /package/dist/{tui → modes/interactive/components}/theme-selector.d.ts +0 -0
  291. /package/dist/{tui → modes/interactive/components}/theme-selector.js +0 -0
  292. /package/dist/{tui → modes/interactive/components}/thinking-selector.d.ts +0 -0
  293. /package/dist/{tui → modes/interactive/components}/thinking-selector.js +0 -0
  294. /package/dist/{tui → modes/interactive/components}/tool-execution.d.ts +0 -0
  295. /package/dist/{tui → modes/interactive/components}/tool-execution.js +0 -0
  296. /package/dist/{tui → modes/interactive/components}/user-message-selector.d.ts +0 -0
  297. /package/dist/{tui → modes/interactive/components}/user-message-selector.js +0 -0
  298. /package/dist/{tui → modes/interactive/components}/user-message.d.ts +0 -0
  299. /package/dist/{tui → modes/interactive/components}/user-message.js +0 -0
  300. /package/dist/{theme → modes/interactive/theme}/dark.json +0 -0
  301. /package/dist/{theme → modes/interactive/theme}/light.json +0 -0
  302. /package/dist/{theme → modes/interactive/theme}/theme-schema.json +0 -0
  303. /package/dist/{theme → modes/interactive/theme}/theme.d.ts +0 -0
  304. /package/dist/{clipboard.d.ts → utils/clipboard.d.ts} +0 -0
  305. /package/dist/{clipboard.js → utils/clipboard.js} +0 -0
  306. /package/dist/{fuzzy.d.ts → utils/fuzzy.d.ts} +0 -0
  307. /package/dist/{fuzzy.js → utils/fuzzy.js} +0 -0
  308. /package/dist/{tools-manager.d.ts → utils/tools-manager.d.ts} +0 -0
@@ -0,0 +1,1217 @@
1
+ /**
2
+ * Interactive mode for the coding agent.
3
+ * Handles TUI rendering and user interaction, delegating business logic to AgentSession.
4
+ */
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ import { CombinedAutocompleteProvider, Container, Input, Loader, Markdown, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
8
+ import { exec } from "child_process";
9
+ import { APP_NAME, getDebugLogPath, getOAuthPath } from "../../config.js";
10
+ import { isBashExecutionMessage } from "../../core/messages.js";
11
+ import { invalidateOAuthCache } from "../../core/model-config.js";
12
+ import { listOAuthProviders, login, logout } from "../../core/oauth/index.js";
13
+ import { getLatestCompactionEntry, SUMMARY_PREFIX, SUMMARY_SUFFIX } from "../../core/session-manager.js";
14
+ import { getChangelogPath, parseChangelog } from "../../utils/changelog.js";
15
+ import { copyToClipboard } from "../../utils/clipboard.js";
16
+ import { AssistantMessageComponent } from "./components/assistant-message.js";
17
+ import { BashExecutionComponent } from "./components/bash-execution.js";
18
+ import { CompactionComponent } from "./components/compaction.js";
19
+ import { CustomEditor } from "./components/custom-editor.js";
20
+ import { DynamicBorder } from "./components/dynamic-border.js";
21
+ import { FooterComponent } from "./components/footer.js";
22
+ import { ModelSelectorComponent } from "./components/model-selector.js";
23
+ import { OAuthSelectorComponent } from "./components/oauth-selector.js";
24
+ import { QueueModeSelectorComponent } from "./components/queue-mode-selector.js";
25
+ import { SessionSelectorComponent } from "./components/session-selector.js";
26
+ import { ThemeSelectorComponent } from "./components/theme-selector.js";
27
+ import { ThinkingSelectorComponent } from "./components/thinking-selector.js";
28
+ import { ToolExecutionComponent } from "./components/tool-execution.js";
29
+ import { UserMessageComponent } from "./components/user-message.js";
30
+ import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
31
+ import { getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "./theme/theme.js";
32
+ export class InteractiveMode {
33
+ session;
34
+ ui;
35
+ chatContainer;
36
+ pendingMessagesContainer;
37
+ statusContainer;
38
+ editor;
39
+ editorContainer;
40
+ footer;
41
+ version;
42
+ isInitialized = false;
43
+ onInputCallback;
44
+ loadingAnimation = null;
45
+ lastSigintTime = 0;
46
+ lastEscapeTime = 0;
47
+ changelogMarkdown = null;
48
+ // Streaming message tracking
49
+ streamingComponent = null;
50
+ // Tool execution tracking: toolCallId -> component
51
+ pendingTools = new Map();
52
+ // Track if this is the first user message (to skip spacer)
53
+ isFirstUserMessage = true;
54
+ // Tool output expansion state
55
+ toolOutputExpanded = false;
56
+ // Thinking block visibility state
57
+ hideThinkingBlock = false;
58
+ // Agent subscription unsubscribe function
59
+ unsubscribe;
60
+ // Track if editor is in bash mode (text starts with !)
61
+ isBashMode = false;
62
+ // Track current bash execution component
63
+ bashComponent = null;
64
+ // Track pending bash components (shown in pending area, moved to chat on submit)
65
+ pendingBashComponents = [];
66
+ // Auto-compaction state
67
+ autoCompactionLoader = null;
68
+ autoCompactionEscapeHandler;
69
+ // Convenience accessors
70
+ get agent() {
71
+ return this.session.agent;
72
+ }
73
+ get sessionManager() {
74
+ return this.session.sessionManager;
75
+ }
76
+ get settingsManager() {
77
+ return this.session.settingsManager;
78
+ }
79
+ constructor(session, version, changelogMarkdown = null, fdPath = null) {
80
+ this.session = session;
81
+ this.version = version;
82
+ this.changelogMarkdown = changelogMarkdown;
83
+ this.ui = new TUI(new ProcessTerminal());
84
+ this.chatContainer = new Container();
85
+ this.pendingMessagesContainer = new Container();
86
+ this.statusContainer = new Container();
87
+ this.editor = new CustomEditor(getEditorTheme());
88
+ this.editorContainer = new Container();
89
+ this.editorContainer.addChild(this.editor);
90
+ this.footer = new FooterComponent(session.state);
91
+ this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
92
+ // Define slash commands for autocomplete
93
+ const slashCommands = [
94
+ { name: "thinking", description: "Select reasoning level (opens selector UI)" },
95
+ { name: "model", description: "Select model (opens selector UI)" },
96
+ { name: "export", description: "Export session to HTML file" },
97
+ { name: "copy", description: "Copy last agent message to clipboard" },
98
+ { name: "session", description: "Show session info and stats" },
99
+ { name: "changelog", description: "Show changelog entries" },
100
+ { name: "branch", description: "Create a new branch from a previous message" },
101
+ { name: "login", description: "Login with OAuth provider" },
102
+ { name: "logout", description: "Logout from OAuth provider" },
103
+ { name: "queue", description: "Select message queue mode (opens selector UI)" },
104
+ { name: "theme", description: "Select color theme (opens selector UI)" },
105
+ { name: "clear", description: "Clear context and start a fresh session" },
106
+ { name: "compact", description: "Manually compact the session context" },
107
+ { name: "autocompact", description: "Toggle automatic context compaction" },
108
+ { name: "resume", description: "Resume a different session" },
109
+ ];
110
+ // Load hide thinking block setting
111
+ this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
112
+ // Convert file commands to SlashCommand format
113
+ const fileSlashCommands = this.session.fileCommands.map((cmd) => ({
114
+ name: cmd.name,
115
+ description: cmd.description,
116
+ }));
117
+ // Setup autocomplete
118
+ const autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...fileSlashCommands], process.cwd(), fdPath);
119
+ this.editor.setAutocompleteProvider(autocompleteProvider);
120
+ }
121
+ async init() {
122
+ if (this.isInitialized)
123
+ return;
124
+ // Add header
125
+ const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
126
+ const instructions = theme.fg("dim", "esc") +
127
+ theme.fg("muted", " to interrupt") +
128
+ "\n" +
129
+ theme.fg("dim", "ctrl+c") +
130
+ theme.fg("muted", " to clear") +
131
+ "\n" +
132
+ theme.fg("dim", "ctrl+c twice") +
133
+ theme.fg("muted", " to exit") +
134
+ "\n" +
135
+ theme.fg("dim", "ctrl+k") +
136
+ theme.fg("muted", " to delete line") +
137
+ "\n" +
138
+ theme.fg("dim", "shift+tab") +
139
+ theme.fg("muted", " to cycle thinking") +
140
+ "\n" +
141
+ theme.fg("dim", "ctrl+p") +
142
+ theme.fg("muted", " to cycle models") +
143
+ "\n" +
144
+ theme.fg("dim", "ctrl+o") +
145
+ theme.fg("muted", " to expand tools") +
146
+ "\n" +
147
+ theme.fg("dim", "ctrl+t") +
148
+ theme.fg("muted", " to toggle thinking") +
149
+ "\n" +
150
+ theme.fg("dim", "/") +
151
+ theme.fg("muted", " for commands") +
152
+ "\n" +
153
+ theme.fg("dim", "!") +
154
+ theme.fg("muted", " to run bash") +
155
+ "\n" +
156
+ theme.fg("dim", "drop files") +
157
+ theme.fg("muted", " to attach");
158
+ const header = new Text(logo + "\n" + instructions, 1, 0);
159
+ // Setup UI layout
160
+ this.ui.addChild(new Spacer(1));
161
+ this.ui.addChild(header);
162
+ this.ui.addChild(new Spacer(1));
163
+ // Add changelog if provided
164
+ if (this.changelogMarkdown) {
165
+ this.ui.addChild(new DynamicBorder());
166
+ if (this.settingsManager.getCollapseChangelog()) {
167
+ const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
168
+ const latestVersion = versionMatch ? versionMatch[1] : this.version;
169
+ const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
170
+ this.ui.addChild(new Text(condensedText, 1, 0));
171
+ }
172
+ else {
173
+ this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
174
+ this.ui.addChild(new Spacer(1));
175
+ this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
176
+ this.ui.addChild(new Spacer(1));
177
+ }
178
+ this.ui.addChild(new DynamicBorder());
179
+ }
180
+ this.ui.addChild(this.chatContainer);
181
+ this.ui.addChild(this.pendingMessagesContainer);
182
+ this.ui.addChild(this.statusContainer);
183
+ this.ui.addChild(new Spacer(1));
184
+ this.ui.addChild(this.editorContainer);
185
+ this.ui.addChild(this.footer);
186
+ this.ui.setFocus(this.editor);
187
+ this.setupKeyHandlers();
188
+ this.setupEditorSubmitHandler();
189
+ // Start the UI
190
+ this.ui.start();
191
+ this.isInitialized = true;
192
+ // Subscribe to agent events
193
+ this.subscribeToAgent();
194
+ // Set up theme file watcher
195
+ onThemeChange(() => {
196
+ this.ui.invalidate();
197
+ this.updateEditorBorderColor();
198
+ this.ui.requestRender();
199
+ });
200
+ // Set up git branch watcher
201
+ this.footer.watchBranch(() => {
202
+ this.ui.requestRender();
203
+ });
204
+ }
205
+ setupKeyHandlers() {
206
+ this.editor.onEscape = () => {
207
+ if (this.loadingAnimation) {
208
+ // Abort and restore queued messages to editor
209
+ const queuedMessages = this.session.clearQueue();
210
+ const queuedText = queuedMessages.join("\n\n");
211
+ const currentText = this.editor.getText();
212
+ const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
213
+ this.editor.setText(combinedText);
214
+ this.updatePendingMessagesDisplay();
215
+ this.agent.abort();
216
+ }
217
+ else if (this.session.isBashRunning) {
218
+ this.session.abortBash();
219
+ }
220
+ else if (this.isBashMode) {
221
+ this.editor.setText("");
222
+ this.isBashMode = false;
223
+ this.updateEditorBorderColor();
224
+ }
225
+ else if (!this.editor.getText().trim()) {
226
+ // Double-escape with empty editor triggers /branch
227
+ const now = Date.now();
228
+ if (now - this.lastEscapeTime < 500) {
229
+ this.showUserMessageSelector();
230
+ this.lastEscapeTime = 0;
231
+ }
232
+ else {
233
+ this.lastEscapeTime = now;
234
+ }
235
+ }
236
+ };
237
+ this.editor.onCtrlC = () => this.handleCtrlC();
238
+ this.editor.onShiftTab = () => this.cycleThinkingLevel();
239
+ this.editor.onCtrlP = () => this.cycleModel();
240
+ this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
241
+ this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
242
+ this.editor.onChange = (text) => {
243
+ const wasBashMode = this.isBashMode;
244
+ this.isBashMode = text.trimStart().startsWith("!");
245
+ if (wasBashMode !== this.isBashMode) {
246
+ this.updateEditorBorderColor();
247
+ }
248
+ };
249
+ }
250
+ setupEditorSubmitHandler() {
251
+ this.editor.onSubmit = async (text) => {
252
+ text = text.trim();
253
+ if (!text)
254
+ return;
255
+ // Handle slash commands
256
+ if (text === "/thinking") {
257
+ this.showThinkingSelector();
258
+ this.editor.setText("");
259
+ return;
260
+ }
261
+ if (text === "/model") {
262
+ this.showModelSelector();
263
+ this.editor.setText("");
264
+ return;
265
+ }
266
+ if (text.startsWith("/export")) {
267
+ this.handleExportCommand(text);
268
+ this.editor.setText("");
269
+ return;
270
+ }
271
+ if (text === "/copy") {
272
+ this.handleCopyCommand();
273
+ this.editor.setText("");
274
+ return;
275
+ }
276
+ if (text === "/session") {
277
+ this.handleSessionCommand();
278
+ this.editor.setText("");
279
+ return;
280
+ }
281
+ if (text === "/changelog") {
282
+ this.handleChangelogCommand();
283
+ this.editor.setText("");
284
+ return;
285
+ }
286
+ if (text === "/branch") {
287
+ this.showUserMessageSelector();
288
+ this.editor.setText("");
289
+ return;
290
+ }
291
+ if (text === "/login") {
292
+ this.showOAuthSelector("login");
293
+ this.editor.setText("");
294
+ return;
295
+ }
296
+ if (text === "/logout") {
297
+ this.showOAuthSelector("logout");
298
+ this.editor.setText("");
299
+ return;
300
+ }
301
+ if (text === "/queue") {
302
+ this.showQueueModeSelector();
303
+ this.editor.setText("");
304
+ return;
305
+ }
306
+ if (text === "/theme") {
307
+ this.showThemeSelector();
308
+ this.editor.setText("");
309
+ return;
310
+ }
311
+ if (text === "/clear") {
312
+ await this.handleClearCommand();
313
+ this.editor.setText("");
314
+ return;
315
+ }
316
+ if (text === "/compact" || text.startsWith("/compact ")) {
317
+ const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
318
+ await this.handleCompactCommand(customInstructions);
319
+ this.editor.setText("");
320
+ return;
321
+ }
322
+ if (text === "/autocompact") {
323
+ this.handleAutocompactCommand();
324
+ this.editor.setText("");
325
+ return;
326
+ }
327
+ if (text === "/debug") {
328
+ this.handleDebugCommand();
329
+ this.editor.setText("");
330
+ return;
331
+ }
332
+ if (text === "/resume") {
333
+ this.showSessionSelector();
334
+ this.editor.setText("");
335
+ return;
336
+ }
337
+ // Handle bash command
338
+ if (text.startsWith("!")) {
339
+ const command = text.slice(1).trim();
340
+ if (command) {
341
+ if (this.session.isBashRunning) {
342
+ this.showWarning("A bash command is already running. Press Esc to cancel it first.");
343
+ this.editor.setText(text);
344
+ return;
345
+ }
346
+ this.editor.addToHistory(text);
347
+ await this.handleBashCommand(command);
348
+ this.isBashMode = false;
349
+ this.updateEditorBorderColor();
350
+ return;
351
+ }
352
+ }
353
+ // Queue message if agent is streaming
354
+ if (this.session.isStreaming) {
355
+ await this.session.queueMessage(text);
356
+ this.updatePendingMessagesDisplay();
357
+ this.editor.addToHistory(text);
358
+ this.editor.setText("");
359
+ this.ui.requestRender();
360
+ return;
361
+ }
362
+ // Normal message submission
363
+ // First, move any pending bash components to chat
364
+ this.flushPendingBashComponents();
365
+ if (this.onInputCallback) {
366
+ this.onInputCallback(text);
367
+ }
368
+ this.editor.addToHistory(text);
369
+ };
370
+ }
371
+ subscribeToAgent() {
372
+ this.unsubscribe = this.session.subscribe(async (event) => {
373
+ await this.handleEvent(event, this.session.state);
374
+ });
375
+ }
376
+ async handleEvent(event, state) {
377
+ if (!this.isInitialized) {
378
+ await this.init();
379
+ }
380
+ this.footer.updateState(state);
381
+ switch (event.type) {
382
+ case "agent_start":
383
+ if (this.loadingAnimation) {
384
+ this.loadingAnimation.stop();
385
+ }
386
+ this.statusContainer.clear();
387
+ this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), "Working... (esc to interrupt)");
388
+ this.statusContainer.addChild(this.loadingAnimation);
389
+ this.ui.requestRender();
390
+ break;
391
+ case "message_start":
392
+ if (event.message.role === "user") {
393
+ this.addMessageToChat(event.message);
394
+ this.editor.setText("");
395
+ this.updatePendingMessagesDisplay();
396
+ this.ui.requestRender();
397
+ }
398
+ else if (event.message.role === "assistant") {
399
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock);
400
+ this.chatContainer.addChild(this.streamingComponent);
401
+ this.streamingComponent.updateContent(event.message);
402
+ this.ui.requestRender();
403
+ }
404
+ break;
405
+ case "message_update":
406
+ if (this.streamingComponent && event.message.role === "assistant") {
407
+ const assistantMsg = event.message;
408
+ this.streamingComponent.updateContent(assistantMsg);
409
+ for (const content of assistantMsg.content) {
410
+ if (content.type === "toolCall") {
411
+ if (!this.pendingTools.has(content.id)) {
412
+ this.chatContainer.addChild(new Text("", 0, 0));
413
+ const component = new ToolExecutionComponent(content.name, content.arguments);
414
+ this.chatContainer.addChild(component);
415
+ this.pendingTools.set(content.id, component);
416
+ }
417
+ else {
418
+ const component = this.pendingTools.get(content.id);
419
+ if (component) {
420
+ component.updateArgs(content.arguments);
421
+ }
422
+ }
423
+ }
424
+ }
425
+ this.ui.requestRender();
426
+ }
427
+ break;
428
+ case "message_end":
429
+ if (event.message.role === "user")
430
+ break;
431
+ if (this.streamingComponent && event.message.role === "assistant") {
432
+ const assistantMsg = event.message;
433
+ this.streamingComponent.updateContent(assistantMsg);
434
+ if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
435
+ const errorMessage = assistantMsg.stopReason === "aborted" ? "Operation aborted" : assistantMsg.errorMessage || "Error";
436
+ for (const [, component] of this.pendingTools.entries()) {
437
+ component.updateResult({
438
+ content: [{ type: "text", text: errorMessage }],
439
+ isError: true,
440
+ });
441
+ }
442
+ this.pendingTools.clear();
443
+ }
444
+ this.streamingComponent = null;
445
+ this.footer.invalidate();
446
+ }
447
+ this.ui.requestRender();
448
+ break;
449
+ case "tool_execution_start": {
450
+ if (!this.pendingTools.has(event.toolCallId)) {
451
+ const component = new ToolExecutionComponent(event.toolName, event.args);
452
+ this.chatContainer.addChild(component);
453
+ this.pendingTools.set(event.toolCallId, component);
454
+ this.ui.requestRender();
455
+ }
456
+ break;
457
+ }
458
+ case "tool_execution_end": {
459
+ const component = this.pendingTools.get(event.toolCallId);
460
+ if (component) {
461
+ const resultData = typeof event.result === "string"
462
+ ? {
463
+ content: [{ type: "text", text: event.result }],
464
+ details: undefined,
465
+ isError: event.isError,
466
+ }
467
+ : { content: event.result.content, details: event.result.details, isError: event.isError };
468
+ component.updateResult(resultData);
469
+ this.pendingTools.delete(event.toolCallId);
470
+ this.ui.requestRender();
471
+ }
472
+ break;
473
+ }
474
+ case "agent_end":
475
+ if (this.loadingAnimation) {
476
+ this.loadingAnimation.stop();
477
+ this.loadingAnimation = null;
478
+ this.statusContainer.clear();
479
+ }
480
+ if (this.streamingComponent) {
481
+ this.chatContainer.removeChild(this.streamingComponent);
482
+ this.streamingComponent = null;
483
+ }
484
+ this.pendingTools.clear();
485
+ this.ui.requestRender();
486
+ break;
487
+ case "auto_compaction_start":
488
+ // Set up escape to abort auto-compaction
489
+ this.autoCompactionEscapeHandler = this.editor.onEscape;
490
+ this.editor.onEscape = () => {
491
+ this.session.abortCompaction();
492
+ };
493
+ // Show compacting indicator
494
+ this.statusContainer.clear();
495
+ this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), "Auto-compacting... (esc to cancel)");
496
+ this.statusContainer.addChild(this.autoCompactionLoader);
497
+ this.ui.requestRender();
498
+ break;
499
+ case "auto_compaction_end":
500
+ // Restore escape handler
501
+ if (this.autoCompactionEscapeHandler) {
502
+ this.editor.onEscape = this.autoCompactionEscapeHandler;
503
+ this.autoCompactionEscapeHandler = undefined;
504
+ }
505
+ // Stop loader
506
+ if (this.autoCompactionLoader) {
507
+ this.autoCompactionLoader.stop();
508
+ this.autoCompactionLoader = null;
509
+ this.statusContainer.clear();
510
+ }
511
+ // Handle result
512
+ if (event.aborted) {
513
+ this.showStatus("Auto-compaction cancelled");
514
+ }
515
+ else if (event.result) {
516
+ // Rebuild chat to show compacted state
517
+ this.chatContainer.clear();
518
+ this.rebuildChatFromMessages();
519
+ // Add compaction component (same as manual /compact)
520
+ const compactionComponent = new CompactionComponent(event.result.tokensBefore, event.result.summary);
521
+ compactionComponent.setExpanded(this.toolOutputExpanded);
522
+ this.chatContainer.addChild(compactionComponent);
523
+ this.footer.updateState(this.session.state);
524
+ }
525
+ this.ui.requestRender();
526
+ break;
527
+ }
528
+ }
529
+ /** Extract text content from a user message */
530
+ getUserMessageText(message) {
531
+ if (message.role !== "user")
532
+ return "";
533
+ const textBlocks = typeof message.content === "string"
534
+ ? [{ type: "text", text: message.content }]
535
+ : message.content.filter((c) => c.type === "text");
536
+ return textBlocks.map((c) => c.text).join("");
537
+ }
538
+ /** Show a status message in the chat */
539
+ showStatus(message) {
540
+ this.chatContainer.addChild(new Spacer(1));
541
+ this.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
542
+ this.ui.requestRender();
543
+ }
544
+ addMessageToChat(message) {
545
+ if (isBashExecutionMessage(message)) {
546
+ const component = new BashExecutionComponent(message.command, this.ui);
547
+ if (message.output) {
548
+ component.appendOutput(message.output);
549
+ }
550
+ component.setComplete(message.exitCode, message.cancelled, message.truncated ? { truncated: true } : undefined, message.fullOutputPath);
551
+ this.chatContainer.addChild(component);
552
+ return;
553
+ }
554
+ if (message.role === "user") {
555
+ const textContent = this.getUserMessageText(message);
556
+ if (textContent) {
557
+ const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
558
+ this.chatContainer.addChild(userComponent);
559
+ this.isFirstUserMessage = false;
560
+ }
561
+ }
562
+ else if (message.role === "assistant") {
563
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock);
564
+ this.chatContainer.addChild(assistantComponent);
565
+ }
566
+ }
567
+ /**
568
+ * Render messages to chat. Used for initial load and rebuild after compaction.
569
+ * @param messages Messages to render
570
+ * @param options.updateFooter Update footer state
571
+ * @param options.populateHistory Add user messages to editor history
572
+ */
573
+ renderMessages(messages, options = {}) {
574
+ this.isFirstUserMessage = true;
575
+ this.pendingTools.clear();
576
+ if (options.updateFooter) {
577
+ this.footer.updateState(this.session.state);
578
+ this.updateEditorBorderColor();
579
+ }
580
+ const compactionEntry = getLatestCompactionEntry(this.sessionManager.loadEntries());
581
+ for (const message of messages) {
582
+ if (isBashExecutionMessage(message)) {
583
+ this.addMessageToChat(message);
584
+ continue;
585
+ }
586
+ if (message.role === "user") {
587
+ const textContent = this.getUserMessageText(message);
588
+ if (textContent) {
589
+ if (textContent.startsWith(SUMMARY_PREFIX) && compactionEntry) {
590
+ const summary = textContent.slice(SUMMARY_PREFIX.length, -SUMMARY_SUFFIX.length);
591
+ const component = new CompactionComponent(compactionEntry.tokensBefore, summary);
592
+ component.setExpanded(this.toolOutputExpanded);
593
+ this.chatContainer.addChild(component);
594
+ }
595
+ else {
596
+ const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
597
+ this.chatContainer.addChild(userComponent);
598
+ this.isFirstUserMessage = false;
599
+ if (options.populateHistory) {
600
+ this.editor.addToHistory(textContent);
601
+ }
602
+ }
603
+ }
604
+ }
605
+ else if (message.role === "assistant") {
606
+ const assistantMsg = message;
607
+ const assistantComponent = new AssistantMessageComponent(assistantMsg, this.hideThinkingBlock);
608
+ this.chatContainer.addChild(assistantComponent);
609
+ for (const content of assistantMsg.content) {
610
+ if (content.type === "toolCall") {
611
+ const component = new ToolExecutionComponent(content.name, content.arguments);
612
+ this.chatContainer.addChild(component);
613
+ if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
614
+ const errorMessage = assistantMsg.stopReason === "aborted"
615
+ ? "Operation aborted"
616
+ : assistantMsg.errorMessage || "Error";
617
+ component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
618
+ }
619
+ else {
620
+ this.pendingTools.set(content.id, component);
621
+ }
622
+ }
623
+ }
624
+ }
625
+ else if (message.role === "toolResult") {
626
+ const component = this.pendingTools.get(message.toolCallId);
627
+ if (component) {
628
+ component.updateResult({
629
+ content: message.content,
630
+ details: message.details,
631
+ isError: message.isError,
632
+ });
633
+ this.pendingTools.delete(message.toolCallId);
634
+ }
635
+ }
636
+ }
637
+ this.pendingTools.clear();
638
+ this.ui.requestRender();
639
+ }
640
+ renderInitialMessages(state) {
641
+ this.renderMessages(state.messages, { updateFooter: true, populateHistory: true });
642
+ }
643
+ async getUserInput() {
644
+ return new Promise((resolve) => {
645
+ this.onInputCallback = (text) => {
646
+ this.onInputCallback = undefined;
647
+ resolve(text);
648
+ };
649
+ });
650
+ }
651
+ rebuildChatFromMessages() {
652
+ this.renderMessages(this.session.messages);
653
+ }
654
+ // =========================================================================
655
+ // Key handlers
656
+ // =========================================================================
657
+ handleCtrlC() {
658
+ const now = Date.now();
659
+ if (now - this.lastSigintTime < 500) {
660
+ this.stop();
661
+ process.exit(0);
662
+ }
663
+ else {
664
+ this.clearEditor();
665
+ this.lastSigintTime = now;
666
+ }
667
+ }
668
+ updateEditorBorderColor() {
669
+ if (this.isBashMode) {
670
+ this.editor.borderColor = theme.getBashModeBorderColor();
671
+ }
672
+ else {
673
+ const level = this.session.thinkingLevel || "off";
674
+ this.editor.borderColor = theme.getThinkingBorderColor(level);
675
+ }
676
+ this.ui.requestRender();
677
+ }
678
+ cycleThinkingLevel() {
679
+ const newLevel = this.session.cycleThinkingLevel();
680
+ if (newLevel === null) {
681
+ this.showStatus("Current model does not support thinking");
682
+ }
683
+ else {
684
+ this.updateEditorBorderColor();
685
+ this.showStatus(`Thinking level: ${newLevel}`);
686
+ }
687
+ }
688
+ async cycleModel() {
689
+ try {
690
+ const result = await this.session.cycleModel();
691
+ if (result === null) {
692
+ const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
693
+ this.showStatus(msg);
694
+ }
695
+ else {
696
+ this.updateEditorBorderColor();
697
+ const thinkingStr = result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
698
+ this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
699
+ }
700
+ }
701
+ catch (error) {
702
+ this.showError(error instanceof Error ? error.message : String(error));
703
+ }
704
+ }
705
+ toggleToolOutputExpansion() {
706
+ this.toolOutputExpanded = !this.toolOutputExpanded;
707
+ for (const child of this.chatContainer.children) {
708
+ if (child instanceof ToolExecutionComponent) {
709
+ child.setExpanded(this.toolOutputExpanded);
710
+ }
711
+ else if (child instanceof CompactionComponent) {
712
+ child.setExpanded(this.toolOutputExpanded);
713
+ }
714
+ else if (child instanceof BashExecutionComponent) {
715
+ child.setExpanded(this.toolOutputExpanded);
716
+ }
717
+ }
718
+ this.ui.requestRender();
719
+ }
720
+ toggleThinkingBlockVisibility() {
721
+ this.hideThinkingBlock = !this.hideThinkingBlock;
722
+ this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
723
+ for (const child of this.chatContainer.children) {
724
+ if (child instanceof AssistantMessageComponent) {
725
+ child.setHideThinkingBlock(this.hideThinkingBlock);
726
+ }
727
+ }
728
+ this.chatContainer.clear();
729
+ this.rebuildChatFromMessages();
730
+ this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
731
+ }
732
+ // =========================================================================
733
+ // UI helpers
734
+ // =========================================================================
735
+ clearEditor() {
736
+ this.editor.setText("");
737
+ this.ui.requestRender();
738
+ }
739
+ showError(errorMessage) {
740
+ this.chatContainer.addChild(new Spacer(1));
741
+ this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
742
+ this.ui.requestRender();
743
+ }
744
+ showWarning(warningMessage) {
745
+ this.chatContainer.addChild(new Spacer(1));
746
+ this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
747
+ this.ui.requestRender();
748
+ }
749
+ showNewVersionNotification(newVersion) {
750
+ this.chatContainer.addChild(new Spacer(1));
751
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
752
+ this.chatContainer.addChild(new Text(theme.bold(theme.fg("warning", "Update Available")) +
753
+ "\n" +
754
+ theme.fg("muted", `New version ${newVersion} is available. Run: `) +
755
+ theme.fg("accent", "npm install -g @mariozechner/pi-coding-agent"), 1, 0));
756
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
757
+ this.ui.requestRender();
758
+ }
759
+ updatePendingMessagesDisplay() {
760
+ this.pendingMessagesContainer.clear();
761
+ const queuedMessages = this.session.getQueuedMessages();
762
+ if (queuedMessages.length > 0) {
763
+ this.pendingMessagesContainer.addChild(new Spacer(1));
764
+ for (const message of queuedMessages) {
765
+ const queuedText = theme.fg("dim", "Queued: " + message);
766
+ this.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
767
+ }
768
+ }
769
+ }
770
+ /** Move pending bash components from pending area to chat */
771
+ flushPendingBashComponents() {
772
+ for (const component of this.pendingBashComponents) {
773
+ this.pendingMessagesContainer.removeChild(component);
774
+ this.chatContainer.addChild(component);
775
+ }
776
+ this.pendingBashComponents = [];
777
+ }
778
+ // =========================================================================
779
+ // Selectors
780
+ // =========================================================================
781
+ /**
782
+ * Shows a selector component in place of the editor.
783
+ * @param create Factory that receives a `done` callback and returns the component and focus target
784
+ */
785
+ showSelector(create) {
786
+ const done = () => {
787
+ this.editorContainer.clear();
788
+ this.editorContainer.addChild(this.editor);
789
+ this.ui.setFocus(this.editor);
790
+ };
791
+ const { component, focus } = create(done);
792
+ this.editorContainer.clear();
793
+ this.editorContainer.addChild(component);
794
+ this.ui.setFocus(focus);
795
+ this.ui.requestRender();
796
+ }
797
+ showThinkingSelector() {
798
+ this.showSelector((done) => {
799
+ const selector = new ThinkingSelectorComponent(this.session.thinkingLevel, (level) => {
800
+ this.session.setThinkingLevel(level);
801
+ this.updateEditorBorderColor();
802
+ done();
803
+ this.showStatus(`Thinking level: ${level}`);
804
+ }, () => {
805
+ done();
806
+ this.ui.requestRender();
807
+ });
808
+ return { component: selector, focus: selector.getSelectList() };
809
+ });
810
+ }
811
+ showQueueModeSelector() {
812
+ this.showSelector((done) => {
813
+ const selector = new QueueModeSelectorComponent(this.session.queueMode, (mode) => {
814
+ this.session.setQueueMode(mode);
815
+ done();
816
+ this.showStatus(`Queue mode: ${mode}`);
817
+ }, () => {
818
+ done();
819
+ this.ui.requestRender();
820
+ });
821
+ return { component: selector, focus: selector.getSelectList() };
822
+ });
823
+ }
824
+ showThemeSelector() {
825
+ const currentTheme = this.settingsManager.getTheme() || "dark";
826
+ this.showSelector((done) => {
827
+ const selector = new ThemeSelectorComponent(currentTheme, (themeName) => {
828
+ const result = setTheme(themeName);
829
+ this.settingsManager.setTheme(themeName);
830
+ this.ui.invalidate();
831
+ done();
832
+ if (result.success) {
833
+ this.showStatus(`Theme: ${themeName}`);
834
+ }
835
+ else {
836
+ this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`);
837
+ }
838
+ }, () => {
839
+ done();
840
+ this.ui.requestRender();
841
+ }, (themeName) => {
842
+ const result = setTheme(themeName);
843
+ if (result.success) {
844
+ this.ui.invalidate();
845
+ this.ui.requestRender();
846
+ }
847
+ });
848
+ return { component: selector, focus: selector.getSelectList() };
849
+ });
850
+ }
851
+ showModelSelector() {
852
+ this.showSelector((done) => {
853
+ const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, (model) => {
854
+ this.agent.setModel(model);
855
+ this.sessionManager.saveModelChange(model.provider, model.id);
856
+ done();
857
+ this.showStatus(`Model: ${model.id}`);
858
+ }, () => {
859
+ done();
860
+ this.ui.requestRender();
861
+ });
862
+ return { component: selector, focus: selector };
863
+ });
864
+ }
865
+ showUserMessageSelector() {
866
+ const userMessages = this.session.getUserMessagesForBranching();
867
+ if (userMessages.length <= 1) {
868
+ this.showStatus("No messages to branch from");
869
+ return;
870
+ }
871
+ this.showSelector((done) => {
872
+ const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ index: m.entryIndex, text: m.text })), (entryIndex) => {
873
+ const selectedText = this.session.branch(entryIndex);
874
+ this.chatContainer.clear();
875
+ this.isFirstUserMessage = true;
876
+ this.renderInitialMessages(this.session.state);
877
+ this.editor.setText(selectedText);
878
+ done();
879
+ this.showStatus("Branched to new session");
880
+ }, () => {
881
+ done();
882
+ this.ui.requestRender();
883
+ });
884
+ return { component: selector, focus: selector.getMessageList() };
885
+ });
886
+ }
887
+ showSessionSelector() {
888
+ this.showSelector((done) => {
889
+ const selector = new SessionSelectorComponent(this.sessionManager, async (sessionPath) => {
890
+ done();
891
+ await this.handleResumeSession(sessionPath);
892
+ }, () => {
893
+ done();
894
+ this.ui.requestRender();
895
+ });
896
+ return { component: selector, focus: selector.getSessionList() };
897
+ });
898
+ }
899
+ async handleResumeSession(sessionPath) {
900
+ // Stop loading animation
901
+ if (this.loadingAnimation) {
902
+ this.loadingAnimation.stop();
903
+ this.loadingAnimation = null;
904
+ }
905
+ this.statusContainer.clear();
906
+ // Clear UI state
907
+ this.pendingMessagesContainer.clear();
908
+ this.streamingComponent = null;
909
+ this.pendingTools.clear();
910
+ // Switch session via AgentSession
911
+ await this.session.switchSession(sessionPath);
912
+ // Clear and re-render the chat
913
+ this.chatContainer.clear();
914
+ this.isFirstUserMessage = true;
915
+ this.renderInitialMessages(this.session.state);
916
+ this.showStatus("Resumed session");
917
+ }
918
+ async showOAuthSelector(mode) {
919
+ if (mode === "logout") {
920
+ const loggedInProviders = listOAuthProviders();
921
+ if (loggedInProviders.length === 0) {
922
+ this.showStatus("No OAuth providers logged in. Use /login first.");
923
+ return;
924
+ }
925
+ }
926
+ this.showSelector((done) => {
927
+ const selector = new OAuthSelectorComponent(mode, async (providerId) => {
928
+ done();
929
+ if (mode === "login") {
930
+ this.showStatus(`Logging in to ${providerId}...`);
931
+ try {
932
+ await login(providerId, (url) => {
933
+ this.chatContainer.addChild(new Spacer(1));
934
+ this.chatContainer.addChild(new Text(theme.fg("accent", "Opening browser to:"), 1, 0));
935
+ this.chatContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
936
+ this.chatContainer.addChild(new Spacer(1));
937
+ this.chatContainer.addChild(new Text(theme.fg("warning", "Paste the authorization code below:"), 1, 0));
938
+ this.ui.requestRender();
939
+ const openCmd = process.platform === "darwin"
940
+ ? "open"
941
+ : process.platform === "win32"
942
+ ? "start"
943
+ : "xdg-open";
944
+ exec(`${openCmd} "${url}"`);
945
+ }, async () => {
946
+ return new Promise((resolve) => {
947
+ const codeInput = new Input();
948
+ codeInput.onSubmit = () => {
949
+ const code = codeInput.getValue();
950
+ this.editorContainer.clear();
951
+ this.editorContainer.addChild(this.editor);
952
+ this.ui.setFocus(this.editor);
953
+ resolve(code);
954
+ };
955
+ this.editorContainer.clear();
956
+ this.editorContainer.addChild(codeInput);
957
+ this.ui.setFocus(codeInput);
958
+ this.ui.requestRender();
959
+ });
960
+ });
961
+ invalidateOAuthCache();
962
+ this.chatContainer.addChild(new Spacer(1));
963
+ this.chatContainer.addChild(new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0));
964
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Tokens saved to ${getOAuthPath()}`), 1, 0));
965
+ this.ui.requestRender();
966
+ }
967
+ catch (error) {
968
+ this.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
969
+ }
970
+ }
971
+ else {
972
+ try {
973
+ await logout(providerId);
974
+ invalidateOAuthCache();
975
+ this.chatContainer.addChild(new Spacer(1));
976
+ this.chatContainer.addChild(new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0));
977
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Credentials removed from ${getOAuthPath()}`), 1, 0));
978
+ this.ui.requestRender();
979
+ }
980
+ catch (error) {
981
+ this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
982
+ }
983
+ }
984
+ }, () => {
985
+ done();
986
+ this.ui.requestRender();
987
+ });
988
+ return { component: selector, focus: selector };
989
+ });
990
+ }
991
+ // =========================================================================
992
+ // Command handlers
993
+ // =========================================================================
994
+ handleExportCommand(text) {
995
+ const parts = text.split(/\s+/);
996
+ const outputPath = parts.length > 1 ? parts[1] : undefined;
997
+ try {
998
+ const filePath = this.session.exportToHtml(outputPath);
999
+ this.showStatus(`Session exported to: ${filePath}`);
1000
+ }
1001
+ catch (error) {
1002
+ this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
1003
+ }
1004
+ }
1005
+ handleCopyCommand() {
1006
+ const text = this.session.getLastAssistantText();
1007
+ if (!text) {
1008
+ this.showError("No agent messages to copy yet.");
1009
+ return;
1010
+ }
1011
+ try {
1012
+ copyToClipboard(text);
1013
+ this.showStatus("Copied last agent message to clipboard");
1014
+ }
1015
+ catch (error) {
1016
+ this.showError(error instanceof Error ? error.message : String(error));
1017
+ }
1018
+ }
1019
+ handleSessionCommand() {
1020
+ const stats = this.session.getSessionStats();
1021
+ let info = `${theme.bold("Session Info")}\n\n`;
1022
+ info += `${theme.fg("dim", "File:")} ${stats.sessionFile}\n`;
1023
+ info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
1024
+ info += `${theme.bold("Messages")}\n`;
1025
+ info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
1026
+ info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`;
1027
+ info += `${theme.fg("dim", "Tool Calls:")} ${stats.toolCalls}\n`;
1028
+ info += `${theme.fg("dim", "Tool Results:")} ${stats.toolResults}\n`;
1029
+ info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n\n`;
1030
+ info += `${theme.bold("Tokens")}\n`;
1031
+ info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
1032
+ info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
1033
+ if (stats.tokens.cacheRead > 0) {
1034
+ info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
1035
+ }
1036
+ if (stats.tokens.cacheWrite > 0) {
1037
+ info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
1038
+ }
1039
+ info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
1040
+ if (stats.cost > 0) {
1041
+ info += `\n${theme.bold("Cost")}\n`;
1042
+ info += `${theme.fg("dim", "Total:")} ${stats.cost.toFixed(4)}`;
1043
+ }
1044
+ this.chatContainer.addChild(new Spacer(1));
1045
+ this.chatContainer.addChild(new Text(info, 1, 0));
1046
+ this.ui.requestRender();
1047
+ }
1048
+ handleChangelogCommand() {
1049
+ const changelogPath = getChangelogPath();
1050
+ const allEntries = parseChangelog(changelogPath);
1051
+ const changelogMarkdown = allEntries.length > 0
1052
+ ? allEntries
1053
+ .reverse()
1054
+ .map((e) => e.content)
1055
+ .join("\n\n")
1056
+ : "No changelog entries found.";
1057
+ this.chatContainer.addChild(new Spacer(1));
1058
+ this.chatContainer.addChild(new DynamicBorder());
1059
+ this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
1060
+ this.ui.addChild(new Spacer(1));
1061
+ this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));
1062
+ this.chatContainer.addChild(new DynamicBorder());
1063
+ this.ui.requestRender();
1064
+ }
1065
+ async handleClearCommand() {
1066
+ // Stop loading animation
1067
+ if (this.loadingAnimation) {
1068
+ this.loadingAnimation.stop();
1069
+ this.loadingAnimation = null;
1070
+ }
1071
+ this.statusContainer.clear();
1072
+ // Reset via session
1073
+ await this.session.reset();
1074
+ // Clear UI state
1075
+ this.chatContainer.clear();
1076
+ this.pendingMessagesContainer.clear();
1077
+ this.streamingComponent = null;
1078
+ this.pendingTools.clear();
1079
+ this.isFirstUserMessage = true;
1080
+ this.chatContainer.addChild(new Spacer(1));
1081
+ this.chatContainer.addChild(new Text(theme.fg("accent", "✓ Context cleared") + "\n" + theme.fg("muted", "Started fresh session"), 1, 1));
1082
+ this.ui.requestRender();
1083
+ }
1084
+ handleDebugCommand() {
1085
+ const width = this.ui.terminal.columns;
1086
+ const allLines = this.ui.render(width);
1087
+ const debugLogPath = getDebugLogPath();
1088
+ const debugData = [
1089
+ `Debug output at ${new Date().toISOString()}`,
1090
+ `Terminal width: ${width}`,
1091
+ `Total lines: ${allLines.length}`,
1092
+ "",
1093
+ "=== All rendered lines with visible widths ===",
1094
+ ...allLines.map((line, idx) => {
1095
+ const vw = visibleWidth(line);
1096
+ const escaped = JSON.stringify(line);
1097
+ return `[${idx}] (w=${vw}) ${escaped}`;
1098
+ }),
1099
+ "",
1100
+ "=== Agent messages (JSONL) ===",
1101
+ ...this.session.messages.map((msg) => JSON.stringify(msg)),
1102
+ "",
1103
+ ].join("\n");
1104
+ fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
1105
+ fs.writeFileSync(debugLogPath, debugData);
1106
+ this.chatContainer.addChild(new Spacer(1));
1107
+ this.chatContainer.addChild(new Text(theme.fg("accent", "✓ Debug log written") + "\n" + theme.fg("muted", debugLogPath), 1, 1));
1108
+ this.ui.requestRender();
1109
+ }
1110
+ async handleBashCommand(command) {
1111
+ const isDeferred = this.session.isStreaming;
1112
+ this.bashComponent = new BashExecutionComponent(command, this.ui);
1113
+ if (isDeferred) {
1114
+ // Show in pending area when agent is streaming
1115
+ this.pendingMessagesContainer.addChild(this.bashComponent);
1116
+ this.pendingBashComponents.push(this.bashComponent);
1117
+ }
1118
+ else {
1119
+ // Show in chat immediately when agent is idle
1120
+ this.chatContainer.addChild(this.bashComponent);
1121
+ }
1122
+ this.ui.requestRender();
1123
+ try {
1124
+ const result = await this.session.executeBash(command, (chunk) => {
1125
+ if (this.bashComponent) {
1126
+ this.bashComponent.appendOutput(chunk);
1127
+ this.ui.requestRender();
1128
+ }
1129
+ });
1130
+ if (this.bashComponent) {
1131
+ this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
1132
+ }
1133
+ }
1134
+ catch (error) {
1135
+ if (this.bashComponent) {
1136
+ this.bashComponent.setComplete(null, false);
1137
+ }
1138
+ this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
1139
+ }
1140
+ this.bashComponent = null;
1141
+ this.ui.requestRender();
1142
+ }
1143
+ async handleCompactCommand(customInstructions) {
1144
+ const entries = this.sessionManager.loadEntries();
1145
+ const messageCount = entries.filter((e) => e.type === "message").length;
1146
+ if (messageCount < 2) {
1147
+ this.showWarning("Nothing to compact (no messages yet)");
1148
+ return;
1149
+ }
1150
+ await this.executeCompaction(customInstructions, false);
1151
+ }
1152
+ handleAutocompactCommand() {
1153
+ const newState = !this.session.autoCompactionEnabled;
1154
+ this.session.setAutoCompactionEnabled(newState);
1155
+ this.footer.setAutoCompactEnabled(newState);
1156
+ this.showStatus(`Auto-compaction: ${newState ? "on" : "off"}`);
1157
+ }
1158
+ async executeCompaction(customInstructions, isAuto = false) {
1159
+ // Stop loading animation
1160
+ if (this.loadingAnimation) {
1161
+ this.loadingAnimation.stop();
1162
+ this.loadingAnimation = null;
1163
+ }
1164
+ this.statusContainer.clear();
1165
+ // Set up escape handler during compaction
1166
+ const originalOnEscape = this.editor.onEscape;
1167
+ this.editor.onEscape = () => {
1168
+ this.session.abortCompaction();
1169
+ };
1170
+ // Show compacting status
1171
+ this.chatContainer.addChild(new Spacer(1));
1172
+ const label = isAuto ? "Auto-compacting context... (esc to cancel)" : "Compacting context... (esc to cancel)";
1173
+ const compactingLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
1174
+ this.statusContainer.addChild(compactingLoader);
1175
+ this.ui.requestRender();
1176
+ try {
1177
+ const result = await this.session.compact(customInstructions);
1178
+ // Rebuild UI
1179
+ this.chatContainer.clear();
1180
+ this.rebuildChatFromMessages();
1181
+ // Add compaction component
1182
+ const compactionComponent = new CompactionComponent(result.tokensBefore, result.summary);
1183
+ compactionComponent.setExpanded(this.toolOutputExpanded);
1184
+ this.chatContainer.addChild(compactionComponent);
1185
+ this.footer.updateState(this.session.state);
1186
+ }
1187
+ catch (error) {
1188
+ const message = error instanceof Error ? error.message : String(error);
1189
+ if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
1190
+ this.showError("Compaction cancelled");
1191
+ }
1192
+ else {
1193
+ this.showError(`Compaction failed: ${message}`);
1194
+ }
1195
+ }
1196
+ finally {
1197
+ compactingLoader.stop();
1198
+ this.statusContainer.clear();
1199
+ this.editor.onEscape = originalOnEscape;
1200
+ }
1201
+ }
1202
+ stop() {
1203
+ if (this.loadingAnimation) {
1204
+ this.loadingAnimation.stop();
1205
+ this.loadingAnimation = null;
1206
+ }
1207
+ this.footer.dispose();
1208
+ if (this.unsubscribe) {
1209
+ this.unsubscribe();
1210
+ }
1211
+ if (this.isInitialized) {
1212
+ this.ui.stop();
1213
+ this.isInitialized = false;
1214
+ }
1215
+ }
1216
+ }
1217
+ //# sourceMappingURL=interactive-mode.js.map