@mariozechner/pi-coding-agent 0.30.2 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/CHANGELOG.md +244 -1
  2. package/README.md +105 -84
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +5 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/file-processor.d.ts +3 -3
  7. package/dist/cli/file-processor.d.ts.map +1 -1
  8. package/dist/cli/file-processor.js +7 -10
  9. package/dist/cli/file-processor.js.map +1 -1
  10. package/dist/config.d.ts +9 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +18 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/core/agent-session.d.ts +73 -34
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +464 -210
  17. package/dist/core/agent-session.js.map +1 -1
  18. package/dist/core/auth-storage.d.ts +2 -2
  19. package/dist/core/auth-storage.d.ts.map +1 -1
  20. package/dist/core/auth-storage.js +2 -2
  21. package/dist/core/auth-storage.js.map +1 -1
  22. package/dist/core/bash-executor.d.ts +2 -2
  23. package/dist/core/bash-executor.d.ts.map +1 -1
  24. package/dist/core/bash-executor.js +2 -2
  25. package/dist/core/bash-executor.js.map +1 -1
  26. package/dist/core/compaction/branch-summarization.d.ts +84 -0
  27. package/dist/core/compaction/branch-summarization.d.ts.map +1 -0
  28. package/dist/core/compaction/branch-summarization.js +233 -0
  29. package/dist/core/compaction/branch-summarization.js.map +1 -0
  30. package/dist/core/{compaction.d.ts → compaction/compaction.d.ts} +38 -19
  31. package/dist/core/compaction/compaction.d.ts.map +1 -0
  32. package/dist/core/compaction/compaction.js +558 -0
  33. package/dist/core/compaction/compaction.js.map +1 -0
  34. package/dist/core/compaction/index.d.ts +7 -0
  35. package/dist/core/compaction/index.d.ts.map +1 -0
  36. package/dist/core/compaction/index.js +7 -0
  37. package/dist/core/compaction/index.js.map +1 -0
  38. package/dist/core/compaction/utils.d.ts +35 -0
  39. package/dist/core/compaction/utils.d.ts.map +1 -0
  40. package/dist/core/compaction/utils.js +138 -0
  41. package/dist/core/compaction/utils.js.map +1 -0
  42. package/dist/core/custom-tools/index.d.ts +2 -1
  43. package/dist/core/custom-tools/index.d.ts.map +1 -1
  44. package/dist/core/custom-tools/index.js +1 -0
  45. package/dist/core/custom-tools/index.js.map +1 -1
  46. package/dist/core/custom-tools/loader.d.ts.map +1 -1
  47. package/dist/core/custom-tools/loader.js +13 -80
  48. package/dist/core/custom-tools/loader.js.map +1 -1
  49. package/dist/core/custom-tools/types.d.ts +84 -59
  50. package/dist/core/custom-tools/types.d.ts.map +1 -1
  51. package/dist/core/custom-tools/types.js.map +1 -1
  52. package/dist/core/custom-tools/wrapper.d.ts +15 -0
  53. package/dist/core/custom-tools/wrapper.d.ts.map +1 -0
  54. package/dist/core/custom-tools/wrapper.js +23 -0
  55. package/dist/core/custom-tools/wrapper.js.map +1 -0
  56. package/dist/core/exec.d.ts +29 -0
  57. package/dist/core/exec.d.ts.map +1 -0
  58. package/dist/core/exec.js +71 -0
  59. package/dist/core/exec.js.map +1 -0
  60. package/dist/core/export-html/index.d.ts +17 -0
  61. package/dist/core/export-html/index.d.ts.map +1 -0
  62. package/dist/core/export-html/index.js +171 -0
  63. package/dist/core/export-html/index.js.map +1 -0
  64. package/dist/core/export-html/template.css +781 -0
  65. package/dist/core/export-html/template.html +54 -0
  66. package/dist/core/export-html/template.js +1185 -0
  67. package/dist/core/export-html/vendor/highlight.min.js +1213 -0
  68. package/dist/core/export-html/vendor/marked.min.js +6 -0
  69. package/dist/core/hooks/index.d.ts +4 -4
  70. package/dist/core/hooks/index.d.ts.map +1 -1
  71. package/dist/core/hooks/index.js +3 -3
  72. package/dist/core/hooks/index.js.map +1 -1
  73. package/dist/core/hooks/loader.d.ts +40 -5
  74. package/dist/core/hooks/loader.d.ts.map +1 -1
  75. package/dist/core/hooks/loader.js +43 -10
  76. package/dist/core/hooks/loader.js.map +1 -1
  77. package/dist/core/hooks/runner.d.ts +94 -18
  78. package/dist/core/hooks/runner.d.ts.map +1 -1
  79. package/dist/core/hooks/runner.js +199 -120
  80. package/dist/core/hooks/runner.js.map +1 -1
  81. package/dist/core/hooks/tool-wrapper.d.ts +1 -1
  82. package/dist/core/hooks/tool-wrapper.d.ts.map +1 -1
  83. package/dist/core/hooks/tool-wrapper.js +36 -19
  84. package/dist/core/hooks/tool-wrapper.js.map +1 -1
  85. package/dist/core/hooks/types.d.ts +407 -96
  86. package/dist/core/hooks/types.d.ts.map +1 -1
  87. package/dist/core/hooks/types.js.map +1 -1
  88. package/dist/core/index.d.ts +4 -3
  89. package/dist/core/index.d.ts.map +1 -1
  90. package/dist/core/index.js.map +1 -1
  91. package/dist/core/messages.d.ts +44 -12
  92. package/dist/core/messages.d.ts.map +1 -1
  93. package/dist/core/messages.js +82 -34
  94. package/dist/core/messages.js.map +1 -1
  95. package/dist/core/model-registry.d.ts +5 -5
  96. package/dist/core/model-registry.d.ts.map +1 -1
  97. package/dist/core/model-registry.js +7 -7
  98. package/dist/core/model-registry.js.map +1 -1
  99. package/dist/core/model-resolver.d.ts +7 -7
  100. package/dist/core/model-resolver.d.ts.map +1 -1
  101. package/dist/core/model-resolver.js +45 -14
  102. package/dist/core/model-resolver.js.map +1 -1
  103. package/dist/core/sdk.d.ts +7 -10
  104. package/dist/core/sdk.d.ts.map +1 -1
  105. package/dist/core/sdk.js +88 -32
  106. package/dist/core/sdk.js.map +1 -1
  107. package/dist/core/session-manager.d.ts +202 -36
  108. package/dist/core/session-manager.d.ts.map +1 -1
  109. package/dist/core/session-manager.js +565 -133
  110. package/dist/core/session-manager.js.map +1 -1
  111. package/dist/core/settings-manager.d.ts +9 -3
  112. package/dist/core/settings-manager.d.ts.map +1 -1
  113. package/dist/core/settings-manager.js +13 -12
  114. package/dist/core/settings-manager.js.map +1 -1
  115. package/dist/core/system-prompt.d.ts.map +1 -1
  116. package/dist/core/system-prompt.js +6 -3
  117. package/dist/core/system-prompt.js.map +1 -1
  118. package/dist/core/tools/bash.d.ts +1 -1
  119. package/dist/core/tools/bash.d.ts.map +1 -1
  120. package/dist/core/tools/bash.js.map +1 -1
  121. package/dist/core/tools/edit-diff.d.ts +33 -0
  122. package/dist/core/tools/edit-diff.d.ts.map +1 -0
  123. package/dist/core/tools/edit-diff.js +171 -0
  124. package/dist/core/tools/edit-diff.js.map +1 -0
  125. package/dist/core/tools/edit.d.ts +7 -1
  126. package/dist/core/tools/edit.d.ts.map +1 -1
  127. package/dist/core/tools/edit.js +20 -95
  128. package/dist/core/tools/edit.js.map +1 -1
  129. package/dist/core/tools/find.d.ts +1 -1
  130. package/dist/core/tools/find.d.ts.map +1 -1
  131. package/dist/core/tools/find.js.map +1 -1
  132. package/dist/core/tools/grep.d.ts +1 -1
  133. package/dist/core/tools/grep.d.ts.map +1 -1
  134. package/dist/core/tools/grep.js.map +1 -1
  135. package/dist/core/tools/index.d.ts +1 -1
  136. package/dist/core/tools/index.d.ts.map +1 -1
  137. package/dist/core/tools/index.js.map +1 -1
  138. package/dist/core/tools/ls.d.ts +1 -1
  139. package/dist/core/tools/ls.d.ts.map +1 -1
  140. package/dist/core/tools/ls.js.map +1 -1
  141. package/dist/core/tools/read.d.ts +1 -1
  142. package/dist/core/tools/read.d.ts.map +1 -1
  143. package/dist/core/tools/read.js.map +1 -1
  144. package/dist/core/tools/write.d.ts +1 -1
  145. package/dist/core/tools/write.d.ts.map +1 -1
  146. package/dist/core/tools/write.js.map +1 -1
  147. package/dist/index.d.ts +8 -7
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +5 -5
  150. package/dist/index.js.map +1 -1
  151. package/dist/main.d.ts.map +1 -1
  152. package/dist/main.js +22 -21
  153. package/dist/main.js.map +1 -1
  154. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  155. package/dist/modes/interactive/components/assistant-message.js +3 -4
  156. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  157. package/dist/modes/interactive/components/bash-execution.d.ts +1 -1
  158. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  159. package/dist/modes/interactive/components/bash-execution.js +6 -2
  160. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  161. package/dist/modes/interactive/components/bordered-loader.d.ts +12 -0
  162. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -0
  163. package/dist/modes/interactive/components/bordered-loader.js +30 -0
  164. package/dist/modes/interactive/components/bordered-loader.js.map +1 -0
  165. package/dist/modes/interactive/components/branch-summary-message.d.ts +14 -0
  166. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -0
  167. package/dist/modes/interactive/components/branch-summary-message.js +35 -0
  168. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -0
  169. package/dist/modes/interactive/components/compaction-summary-message.d.ts +14 -0
  170. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -0
  171. package/dist/modes/interactive/components/compaction-summary-message.js +36 -0
  172. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -0
  173. package/dist/modes/interactive/components/dynamic-border.d.ts +5 -1
  174. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  175. package/dist/modes/interactive/components/dynamic-border.js +5 -1
  176. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  177. package/dist/modes/interactive/components/footer.d.ts +12 -6
  178. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  179. package/dist/modes/interactive/components/footer.js +57 -25
  180. package/dist/modes/interactive/components/footer.js.map +1 -1
  181. package/dist/modes/interactive/components/hook-editor.d.ts +15 -0
  182. package/dist/modes/interactive/components/hook-editor.d.ts.map +1 -0
  183. package/dist/modes/interactive/components/hook-editor.js +95 -0
  184. package/dist/modes/interactive/components/hook-editor.js.map +1 -0
  185. package/dist/modes/interactive/components/hook-message.d.ts +18 -0
  186. package/dist/modes/interactive/components/hook-message.d.ts.map +1 -0
  187. package/dist/modes/interactive/components/hook-message.js +80 -0
  188. package/dist/modes/interactive/components/hook-message.js.map +1 -0
  189. package/dist/modes/interactive/components/model-selector.d.ts +3 -3
  190. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  191. package/dist/modes/interactive/components/model-selector.js +1 -1
  192. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  193. package/dist/modes/interactive/components/tool-execution.d.ts +15 -2
  194. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  195. package/dist/modes/interactive/components/tool-execution.js +70 -21
  196. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  197. package/dist/modes/interactive/components/tree-selector.d.ts +52 -0
  198. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -0
  199. package/dist/modes/interactive/components/tree-selector.js +745 -0
  200. package/dist/modes/interactive/components/tree-selector.js.map +1 -0
  201. package/dist/modes/interactive/components/user-message-selector.d.ts +3 -3
  202. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  203. package/dist/modes/interactive/components/user-message-selector.js +1 -1
  204. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  205. package/dist/modes/interactive/components/user-message.d.ts +1 -1
  206. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  207. package/dist/modes/interactive/components/user-message.js +2 -5
  208. package/dist/modes/interactive/components/user-message.js.map +1 -1
  209. package/dist/modes/interactive/interactive-mode.d.ts +29 -12
  210. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  211. package/dist/modes/interactive/interactive-mode.js +589 -208
  212. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  213. package/dist/modes/interactive/theme/dark.json +13 -1
  214. package/dist/modes/interactive/theme/light.json +13 -1
  215. package/dist/modes/interactive/theme/theme-schema.json +34 -0
  216. package/dist/modes/interactive/theme/theme.d.ts +20 -2
  217. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  218. package/dist/modes/interactive/theme/theme.js +135 -2
  219. package/dist/modes/interactive/theme/theme.js.map +1 -1
  220. package/dist/modes/print-mode.d.ts +3 -3
  221. package/dist/modes/print-mode.d.ts.map +1 -1
  222. package/dist/modes/print-mode.js +26 -20
  223. package/dist/modes/print-mode.js.map +1 -1
  224. package/dist/modes/rpc/rpc-client.d.ts +13 -10
  225. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  226. package/dist/modes/rpc/rpc-client.js +11 -10
  227. package/dist/modes/rpc/rpc-client.js.map +1 -1
  228. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  229. package/dist/modes/rpc/rpc-mode.js +88 -35
  230. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  231. package/dist/modes/rpc/rpc-types.d.ts +30 -11
  232. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  233. package/dist/modes/rpc/rpc-types.js.map +1 -1
  234. package/dist/utils/shell.d.ts +4 -2
  235. package/dist/utils/shell.d.ts.map +1 -1
  236. package/dist/utils/shell.js +36 -7
  237. package/dist/utils/shell.js.map +1 -1
  238. package/dist/utils/tools-manager.d.ts +1 -1
  239. package/dist/utils/tools-manager.d.ts.map +1 -1
  240. package/dist/utils/tools-manager.js +2 -2
  241. package/dist/utils/tools-manager.js.map +1 -1
  242. package/docs/compaction.md +388 -0
  243. package/docs/custom-tools.md +146 -43
  244. package/docs/extension-loading.md +1004 -0
  245. package/docs/hooks.md +562 -596
  246. package/docs/rpc.md +33 -19
  247. package/docs/sdk.md +93 -21
  248. package/docs/session-tree-plan.md +441 -0
  249. package/docs/session.md +172 -21
  250. package/docs/skills.md +2 -0
  251. package/docs/theme.md +31 -2
  252. package/docs/tree.md +197 -0
  253. package/docs/tui.md +343 -0
  254. package/examples/README.md +1 -9
  255. package/examples/custom-tools/hello/index.ts +4 -3
  256. package/examples/custom-tools/question/index.ts +4 -4
  257. package/examples/custom-tools/subagent/index.ts +7 -6
  258. package/examples/custom-tools/todo/index.ts +11 -5
  259. package/examples/hooks/README.md +29 -71
  260. package/examples/hooks/auto-commit-on-exit.ts +8 -9
  261. package/examples/hooks/confirm-destructive.ts +29 -30
  262. package/examples/hooks/custom-compaction.ts +20 -21
  263. package/examples/hooks/dirty-repo-guard.ts +41 -40
  264. package/examples/hooks/file-trigger.ts +10 -5
  265. package/examples/hooks/git-checkpoint.ts +16 -12
  266. package/examples/hooks/handoff.ts +150 -0
  267. package/examples/hooks/permission-gate.ts +1 -1
  268. package/examples/hooks/protected-paths.ts +1 -1
  269. package/examples/hooks/qna.ts +119 -0
  270. package/examples/hooks/snake.ts +343 -0
  271. package/examples/hooks/status-line.ts +40 -0
  272. package/examples/sdk/01-minimal.ts +1 -1
  273. package/examples/sdk/02-custom-model.ts +1 -1
  274. package/examples/sdk/03-custom-prompt.ts +1 -1
  275. package/examples/sdk/04-skills.ts +1 -1
  276. package/examples/sdk/05-tools.ts +4 -4
  277. package/examples/sdk/06-hooks.ts +1 -1
  278. package/examples/sdk/07-context-files.ts +1 -1
  279. package/examples/sdk/08-slash-commands.ts +6 -1
  280. package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
  281. package/examples/sdk/10-settings.ts +1 -1
  282. package/examples/sdk/11-sessions.ts +1 -1
  283. package/examples/sdk/12-full-control.ts +4 -7
  284. package/package.json +6 -6
  285. package/dist/core/compaction.d.ts.map +0 -1
  286. package/dist/core/compaction.js +0 -412
  287. package/dist/core/compaction.js.map +0 -1
  288. package/dist/core/export-html.d.ts +0 -23
  289. package/dist/core/export-html.d.ts.map +0 -1
  290. package/dist/core/export-html.js +0 -1185
  291. package/dist/core/export-html.js.map +0 -1
  292. package/dist/modes/interactive/components/compaction.d.ts +0 -15
  293. package/dist/modes/interactive/components/compaction.d.ts.map +0 -1
  294. package/dist/modes/interactive/components/compaction.js +0 -41
  295. package/dist/modes/interactive/components/compaction.js.map +0 -1
  296. package/docs/hooks-v2.md +0 -385
  297. package/docs/session-tree.md +0 -452
@@ -6,10 +6,10 @@ import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import { CombinedAutocompleteProvider, Container, Input, Loader, Markdown, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
9
- import { exec, spawnSync } from "child_process";
9
+ import { exec, spawn, spawnSync } from "child_process";
10
10
  import { APP_NAME, getAuthPath, getDebugLogPath } from "../../config.js";
11
- import { isBashExecutionMessage } from "../../core/messages.js";
12
- import { getLatestCompactionEntry, SessionManager, SUMMARY_PREFIX, SUMMARY_SUFFIX, } from "../../core/session-manager.js";
11
+ import { createCompactionSummaryMessage } from "../../core/messages.js";
12
+ import { SessionManager } from "../../core/session-manager.js";
13
13
  import { loadSkills } from "../../core/skills.js";
14
14
  import { loadProjectContextFiles } from "../../core/system-prompt.js";
15
15
  import { getChangelogPath, parseChangelog } from "../../utils/changelog.js";
@@ -17,20 +17,28 @@ import { copyToClipboard } from "../../utils/clipboard.js";
17
17
  import { ArminComponent } from "./components/armin.js";
18
18
  import { AssistantMessageComponent } from "./components/assistant-message.js";
19
19
  import { BashExecutionComponent } from "./components/bash-execution.js";
20
- import { CompactionComponent } from "./components/compaction.js";
20
+ import { BorderedLoader } from "./components/bordered-loader.js";
21
+ import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
22
+ import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
21
23
  import { CustomEditor } from "./components/custom-editor.js";
22
24
  import { DynamicBorder } from "./components/dynamic-border.js";
23
25
  import { FooterComponent } from "./components/footer.js";
26
+ import { HookEditorComponent } from "./components/hook-editor.js";
24
27
  import { HookInputComponent } from "./components/hook-input.js";
28
+ import { HookMessageComponent } from "./components/hook-message.js";
25
29
  import { HookSelectorComponent } from "./components/hook-selector.js";
26
30
  import { ModelSelectorComponent } from "./components/model-selector.js";
27
31
  import { OAuthSelectorComponent } from "./components/oauth-selector.js";
28
32
  import { SessionSelectorComponent } from "./components/session-selector.js";
29
33
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
30
34
  import { ToolExecutionComponent } from "./components/tool-execution.js";
35
+ import { TreeSelectorComponent } from "./components/tree-selector.js";
31
36
  import { UserMessageComponent } from "./components/user-message.js";
32
37
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
33
- import { getAvailableThemes, getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "./theme/theme.js";
38
+ import { getAvailableThemes, getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme, } from "./theme/theme.js";
39
+ function isExpandable(obj) {
40
+ return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
41
+ }
34
42
  export class InteractiveMode {
35
43
  setToolUIContext;
36
44
  session;
@@ -44,16 +52,18 @@ export class InteractiveMode {
44
52
  version;
45
53
  isInitialized = false;
46
54
  onInputCallback;
47
- loadingAnimation = null;
55
+ loadingAnimation = undefined;
48
56
  lastSigintTime = 0;
49
57
  lastEscapeTime = 0;
50
- changelogMarkdown = null;
58
+ changelogMarkdown = undefined;
59
+ // Status line tracking (for mutating immediately-sequential status updates)
60
+ lastStatusSpacer = undefined;
61
+ lastStatusText = undefined;
51
62
  // Streaming message tracking
52
- streamingComponent = null;
63
+ streamingComponent = undefined;
64
+ streamingMessage = undefined;
53
65
  // Tool execution tracking: toolCallId -> component
54
66
  pendingTools = new Map();
55
- // Track if this is the first user message (to skip spacer)
56
- isFirstUserMessage = true;
57
67
  // Tool output expansion state
58
68
  toolOutputExpanded = false;
59
69
  // Thinking block visibility state
@@ -63,18 +73,19 @@ export class InteractiveMode {
63
73
  // Track if editor is in bash mode (text starts with !)
64
74
  isBashMode = false;
65
75
  // Track current bash execution component
66
- bashComponent = null;
76
+ bashComponent = undefined;
67
77
  // Track pending bash components (shown in pending area, moved to chat on submit)
68
78
  pendingBashComponents = [];
69
79
  // Auto-compaction state
70
- autoCompactionLoader = null;
80
+ autoCompactionLoader = undefined;
71
81
  autoCompactionEscapeHandler;
72
82
  // Auto-retry state
73
- retryLoader = null;
83
+ retryLoader = undefined;
74
84
  retryEscapeHandler;
75
85
  // Hook UI state
76
- hookSelector = null;
77
- hookInput = null;
86
+ hookSelector = undefined;
87
+ hookInput = undefined;
88
+ hookEditor = undefined;
78
89
  // Custom tools for custom rendering
79
90
  customTools;
80
91
  // Convenience accessors
@@ -87,7 +98,7 @@ export class InteractiveMode {
87
98
  get settingsManager() {
88
99
  return this.session.settingsManager;
89
100
  }
90
- constructor(session, version, changelogMarkdown = null, customTools = [], setToolUIContext = () => { }, fdPath = null) {
101
+ constructor(session, version, changelogMarkdown = undefined, customTools = [], setToolUIContext = () => { }, fdPath = undefined) {
91
102
  this.setToolUIContext = setToolUIContext;
92
103
  this.session = session;
93
104
  this.version = version;
@@ -100,18 +111,20 @@ export class InteractiveMode {
100
111
  this.editor = new CustomEditor(getEditorTheme());
101
112
  this.editorContainer = new Container();
102
113
  this.editorContainer.addChild(this.editor);
103
- this.footer = new FooterComponent(session.state, session.modelRegistry);
114
+ this.footer = new FooterComponent(session);
104
115
  this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
105
116
  // Define slash commands for autocomplete
106
117
  const slashCommands = [
107
118
  { name: "settings", description: "Open settings menu" },
108
119
  { name: "model", description: "Select model (opens selector UI)" },
109
120
  { name: "export", description: "Export session to HTML file" },
121
+ { name: "share", description: "Share session as a secret GitHub gist" },
110
122
  { name: "copy", description: "Copy last agent message to clipboard" },
111
123
  { name: "session", description: "Show session info and stats" },
112
124
  { name: "changelog", description: "Show changelog entries" },
113
125
  { name: "hotkeys", description: "Show all keyboard shortcuts" },
114
126
  { name: "branch", description: "Create a new branch from a previous message" },
127
+ { name: "tree", description: "Navigate session tree (switch branches)" },
115
128
  { name: "login", description: "Login with OAuth provider" },
116
129
  { name: "logout", description: "Logout from OAuth provider" },
117
130
  { name: "new", description: "Start a new session" },
@@ -125,8 +138,13 @@ export class InteractiveMode {
125
138
  name: cmd.name,
126
139
  description: cmd.description,
127
140
  }));
141
+ // Convert hook commands to SlashCommand format
142
+ const hookCommands = (this.session.hookRunner?.getRegisteredCommands() ?? []).map((cmd) => ({
143
+ name: cmd.name,
144
+ description: cmd.description ?? "(hook command)",
145
+ }));
128
146
  // Setup autocomplete
129
- const autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...fileSlashCommands], process.cwd(), fdPath);
147
+ const autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...fileSlashCommands, ...hookCommands], process.cwd(), fdPath);
130
148
  this.editor.setAutocompleteProvider(autocompleteProvider);
131
149
  }
132
150
  async init() {
@@ -267,33 +285,117 @@ export class InteractiveMode {
267
285
  this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded custom tools:\n") + toolList, 0, 0));
268
286
  this.chatContainer.addChild(new Spacer(1));
269
287
  }
270
- // Load session entries if any
271
- const entries = this.session.sessionManager.getEntries();
272
- // Set TUI-based UI context for custom tools
273
- const uiContext = this.createHookUIContext();
288
+ // Create and set hook & tool UI context
289
+ const uiContext = {
290
+ select: (title, options) => this.showHookSelector(title, options),
291
+ confirm: (title, message) => this.showHookConfirm(title, message),
292
+ input: (title, placeholder) => this.showHookInput(title, placeholder),
293
+ notify: (message, type) => this.showHookNotify(message, type),
294
+ setStatus: (key, text) => this.setHookStatus(key, text),
295
+ custom: (factory) => this.showHookCustom(factory),
296
+ setEditorText: (text) => this.editor.setText(text),
297
+ getEditorText: () => this.editor.getText(),
298
+ editor: (title, prefill) => this.showHookEditor(title, prefill),
299
+ get theme() {
300
+ return theme;
301
+ },
302
+ };
274
303
  this.setToolUIContext(uiContext, true);
275
304
  // Notify custom tools of session start
276
- await this.emitToolSessionEvent({
277
- entries,
278
- sessionFile: this.session.sessionFile,
279
- previousSessionFile: null,
305
+ await this.emitCustomToolSessionEvent({
280
306
  reason: "start",
307
+ previousSessionFile: undefined,
281
308
  });
282
309
  const hookRunner = this.session.hookRunner;
283
310
  if (!hookRunner) {
284
311
  return; // No hooks loaded
285
312
  }
286
- // Set UI context on hook runner
287
- hookRunner.setUIContext(uiContext, true);
288
- hookRunner.setSessionFile(this.session.sessionFile);
313
+ hookRunner.initialize({
314
+ getModel: () => this.session.model,
315
+ sendMessageHandler: (message, triggerTurn) => {
316
+ const wasStreaming = this.session.isStreaming;
317
+ this.session
318
+ .sendHookMessage(message, triggerTurn)
319
+ .then(() => {
320
+ // For non-streaming cases with display=true, update UI
321
+ // (streaming cases update via message_end event)
322
+ if (!wasStreaming && message.display) {
323
+ this.rebuildChatFromMessages();
324
+ }
325
+ })
326
+ .catch((err) => {
327
+ this.showError(`Hook sendMessage failed: ${err instanceof Error ? err.message : String(err)}`);
328
+ });
329
+ },
330
+ appendEntryHandler: (customType, data) => {
331
+ this.sessionManager.appendCustomEntry(customType, data);
332
+ },
333
+ newSessionHandler: async (options) => {
334
+ // Stop any loading animation
335
+ if (this.loadingAnimation) {
336
+ this.loadingAnimation.stop();
337
+ this.loadingAnimation = undefined;
338
+ }
339
+ this.statusContainer.clear();
340
+ // Create new session
341
+ const success = await this.session.newSession({ parentSession: options?.parentSession });
342
+ if (!success) {
343
+ return { cancelled: true };
344
+ }
345
+ // Call setup callback if provided
346
+ if (options?.setup) {
347
+ await options.setup(this.sessionManager);
348
+ }
349
+ // Clear UI state
350
+ this.chatContainer.clear();
351
+ this.pendingMessagesContainer.clear();
352
+ this.streamingComponent = undefined;
353
+ this.streamingMessage = undefined;
354
+ this.pendingTools.clear();
355
+ this.chatContainer.addChild(new Spacer(1));
356
+ this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
357
+ this.ui.requestRender();
358
+ return { cancelled: false };
359
+ },
360
+ branchHandler: async (entryId) => {
361
+ const result = await this.session.branch(entryId);
362
+ if (result.cancelled) {
363
+ return { cancelled: true };
364
+ }
365
+ // Update UI
366
+ this.chatContainer.clear();
367
+ this.renderInitialMessages();
368
+ this.editor.setText(result.selectedText);
369
+ this.showStatus("Branched to new session");
370
+ return { cancelled: false };
371
+ },
372
+ navigateTreeHandler: async (targetId, options) => {
373
+ const result = await this.session.navigateTree(targetId, { summarize: options?.summarize });
374
+ if (result.cancelled) {
375
+ return { cancelled: true };
376
+ }
377
+ // Update UI
378
+ this.chatContainer.clear();
379
+ this.renderInitialMessages();
380
+ if (result.editorText) {
381
+ this.editor.setText(result.editorText);
382
+ }
383
+ this.showStatus("Navigated to selected point");
384
+ return { cancelled: false };
385
+ },
386
+ isIdle: () => !this.session.isStreaming,
387
+ waitForIdle: () => this.session.agent.waitForIdle(),
388
+ abort: () => {
389
+ this.session.abort();
390
+ },
391
+ hasQueuedMessages: () => this.session.queuedMessageCount > 0,
392
+ uiContext,
393
+ hasUI: true,
394
+ });
289
395
  // Subscribe to hook errors
290
396
  hookRunner.onError((error) => {
291
397
  this.showHookError(error.hookPath, error.error);
292
398
  });
293
- // Set up send handler for pi.send()
294
- hookRunner.setSendHandler((text, attachments) => {
295
- this.handleHookSend(text, attachments);
296
- });
297
399
  // Show loaded hooks
298
400
  const hookPaths = hookRunner.getHookPaths();
299
401
  if (hookPaths.length > 0) {
@@ -301,23 +403,28 @@ export class InteractiveMode {
301
403
  this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded hooks:\n") + hookList, 0, 0));
302
404
  this.chatContainer.addChild(new Spacer(1));
303
405
  }
304
- // Emit session event
406
+ // Emit session_start event
305
407
  await hookRunner.emit({
306
- type: "session",
307
- entries,
308
- sessionFile: this.session.sessionFile,
309
- previousSessionFile: null,
310
- reason: "start",
408
+ type: "session_start",
311
409
  });
312
410
  }
313
411
  /**
314
412
  * Emit session event to all custom tools.
315
413
  */
316
- async emitToolSessionEvent(event) {
414
+ async emitCustomToolSessionEvent(event) {
317
415
  for (const { tool } of this.customTools.values()) {
318
416
  if (tool.onSession) {
319
417
  try {
320
- await tool.onSession(event);
418
+ await tool.onSession(event, {
419
+ sessionManager: this.session.sessionManager,
420
+ modelRegistry: this.session.modelRegistry,
421
+ model: this.session.model,
422
+ isIdle: () => !this.session.isStreaming,
423
+ hasQueuedMessages: () => this.session.queuedMessageCount > 0,
424
+ abort: () => {
425
+ this.session.abort();
426
+ },
427
+ });
321
428
  }
322
429
  catch (err) {
323
430
  this.showToolError(tool.name, err instanceof Error ? err.message : String(err));
@@ -334,15 +441,11 @@ export class InteractiveMode {
334
441
  this.ui.requestRender();
335
442
  }
336
443
  /**
337
- * Create the UI context for hooks.
444
+ * Set hook status text in the footer.
338
445
  */
339
- createHookUIContext() {
340
- return {
341
- select: (title, options) => this.showHookSelector(title, options),
342
- confirm: (title, message) => this.showHookConfirm(title, message),
343
- input: (title, placeholder) => this.showHookInput(title, placeholder),
344
- notify: (message, type) => this.showHookNotify(message, type),
345
- };
446
+ setHookStatus(key, text) {
447
+ this.footer.setHookStatus(key, text);
448
+ this.ui.requestRender();
346
449
  }
347
450
  /**
348
451
  * Show a selector for hooks.
@@ -354,7 +457,7 @@ export class InteractiveMode {
354
457
  resolve(option);
355
458
  }, () => {
356
459
  this.hideHookSelector();
357
- resolve(null);
460
+ resolve(undefined);
358
461
  });
359
462
  this.editorContainer.clear();
360
463
  this.editorContainer.addChild(this.hookSelector);
@@ -368,7 +471,7 @@ export class InteractiveMode {
368
471
  hideHookSelector() {
369
472
  this.editorContainer.clear();
370
473
  this.editorContainer.addChild(this.editor);
371
- this.hookSelector = null;
474
+ this.hookSelector = undefined;
372
475
  this.ui.setFocus(this.editor);
373
476
  this.ui.requestRender();
374
477
  }
@@ -389,7 +492,7 @@ export class InteractiveMode {
389
492
  resolve(value);
390
493
  }, () => {
391
494
  this.hideHookInput();
392
- resolve(null);
495
+ resolve(undefined);
393
496
  });
394
497
  this.editorContainer.clear();
395
498
  this.editorContainer.addChild(this.hookInput);
@@ -403,7 +506,35 @@ export class InteractiveMode {
403
506
  hideHookInput() {
404
507
  this.editorContainer.clear();
405
508
  this.editorContainer.addChild(this.editor);
406
- this.hookInput = null;
509
+ this.hookInput = undefined;
510
+ this.ui.setFocus(this.editor);
511
+ this.ui.requestRender();
512
+ }
513
+ /**
514
+ * Show a multi-line editor for hooks (with Ctrl+G support).
515
+ */
516
+ showHookEditor(title, prefill) {
517
+ return new Promise((resolve) => {
518
+ this.hookEditor = new HookEditorComponent(this.ui, title, prefill, (value) => {
519
+ this.hideHookEditor();
520
+ resolve(value);
521
+ }, () => {
522
+ this.hideHookEditor();
523
+ resolve(undefined);
524
+ });
525
+ this.editorContainer.clear();
526
+ this.editorContainer.addChild(this.hookEditor);
527
+ this.ui.setFocus(this.hookEditor);
528
+ this.ui.requestRender();
529
+ });
530
+ }
531
+ /**
532
+ * Hide the hook editor.
533
+ */
534
+ hideHookEditor() {
535
+ this.editorContainer.clear();
536
+ this.editorContainer.addChild(this.editor);
537
+ this.hookEditor = undefined;
407
538
  this.ui.setFocus(this.editor);
408
539
  this.ui.requestRender();
409
540
  }
@@ -421,6 +552,31 @@ export class InteractiveMode {
421
552
  this.showStatus(message);
422
553
  }
423
554
  }
555
+ /**
556
+ * Show a custom component with keyboard focus.
557
+ */
558
+ async showHookCustom(factory) {
559
+ const savedText = this.editor.getText();
560
+ return new Promise((resolve) => {
561
+ let component;
562
+ const close = (result) => {
563
+ component.dispose?.();
564
+ this.editorContainer.clear();
565
+ this.editorContainer.addChild(this.editor);
566
+ this.editor.setText(savedText);
567
+ this.ui.setFocus(this.editor);
568
+ this.ui.requestRender();
569
+ resolve(result);
570
+ };
571
+ Promise.resolve(factory(this.ui, theme, close)).then((c) => {
572
+ component = c;
573
+ this.editorContainer.clear();
574
+ this.editorContainer.addChild(component);
575
+ this.ui.setFocus(component);
576
+ this.ui.requestRender();
577
+ });
578
+ });
579
+ }
424
580
  /**
425
581
  * Show a hook error in the UI.
426
582
  */
@@ -433,19 +589,6 @@ export class InteractiveMode {
433
589
  * Handle pi.send() from hooks.
434
590
  * If streaming, queue the message. Otherwise, start a new agent loop.
435
591
  */
436
- handleHookSend(text, attachments) {
437
- if (this.session.isStreaming) {
438
- // Queue the message for later (note: attachments are lost when queuing)
439
- this.session.queueMessage(text);
440
- this.updatePendingMessagesDisplay();
441
- }
442
- else {
443
- // Start a new agent loop immediately
444
- this.session.prompt(text, { attachments }).catch((err) => {
445
- this.showError(err instanceof Error ? err.message : String(err));
446
- });
447
- }
448
- }
449
592
  // =========================================================================
450
593
  // Key Handlers
451
594
  // =========================================================================
@@ -487,6 +630,8 @@ export class InteractiveMode {
487
630
  this.editor.onShiftTab = () => this.cycleThinkingLevel();
488
631
  this.editor.onCtrlP = () => this.cycleModel("forward");
489
632
  this.editor.onShiftCtrlP = () => this.cycleModel("backward");
633
+ // Global debug handler on TUI (works regardless of focus)
634
+ this.ui.onDebug = () => this.handleDebugCommand();
490
635
  this.editor.onCtrlL = () => this.showModelSelector();
491
636
  this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
492
637
  this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
@@ -520,6 +665,11 @@ export class InteractiveMode {
520
665
  this.editor.setText("");
521
666
  return;
522
667
  }
668
+ if (text === "/share") {
669
+ await this.handleShareCommand();
670
+ this.editor.setText("");
671
+ return;
672
+ }
523
673
  if (text === "/copy") {
524
674
  this.handleCopyCommand();
525
675
  this.editor.setText("");
@@ -545,6 +695,11 @@ export class InteractiveMode {
545
695
  this.editor.setText("");
546
696
  return;
547
697
  }
698
+ if (text === "/tree") {
699
+ this.showTreeSelector();
700
+ this.editor.setText("");
701
+ return;
702
+ }
548
703
  if (text === "/login") {
549
704
  this.showOAuthSelector("login");
550
705
  this.editor.setText("");
@@ -607,7 +762,20 @@ export class InteractiveMode {
607
762
  if (this.session.isCompacting) {
608
763
  return;
609
764
  }
610
- // Queue message if agent is streaming
765
+ // Hook commands always run immediately, even during streaming
766
+ // (if they need to interact with LLM, they use pi.sendMessage which handles queueing)
767
+ if (text.startsWith("/") && this.session.hookRunner) {
768
+ const spaceIndex = text.indexOf(" ");
769
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
770
+ const command = this.session.hookRunner.getCommand(commandName);
771
+ if (command) {
772
+ this.editor.addToHistory(text);
773
+ this.editor.setText("");
774
+ await this.session.prompt(text);
775
+ return;
776
+ }
777
+ }
778
+ // Queue regular messages if agent is streaming
611
779
  if (this.session.isStreaming) {
612
780
  await this.session.queueMessage(text);
613
781
  this.updatePendingMessagesDisplay();
@@ -627,14 +795,14 @@ export class InteractiveMode {
627
795
  }
628
796
  subscribeToAgent() {
629
797
  this.unsubscribe = this.session.subscribe(async (event) => {
630
- await this.handleEvent(event, this.session.state);
798
+ await this.handleEvent(event);
631
799
  });
632
800
  }
633
- async handleEvent(event, state) {
801
+ async handleEvent(event) {
634
802
  if (!this.isInitialized) {
635
803
  await this.init();
636
804
  }
637
- this.footer.updateState(state);
805
+ this.footer.invalidate();
638
806
  switch (event.type) {
639
807
  case "agent_start":
640
808
  if (this.loadingAnimation) {
@@ -646,7 +814,11 @@ export class InteractiveMode {
646
814
  this.ui.requestRender();
647
815
  break;
648
816
  case "message_start":
649
- if (event.message.role === "user") {
817
+ if (event.message.role === "hookMessage") {
818
+ this.addMessageToChat(event.message);
819
+ this.ui.requestRender();
820
+ }
821
+ else if (event.message.role === "user") {
650
822
  this.addMessageToChat(event.message);
651
823
  this.editor.setText("");
652
824
  this.updatePendingMessagesDisplay();
@@ -654,22 +826,24 @@ export class InteractiveMode {
654
826
  }
655
827
  else if (event.message.role === "assistant") {
656
828
  this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock);
829
+ this.streamingMessage = event.message;
657
830
  this.chatContainer.addChild(this.streamingComponent);
658
- this.streamingComponent.updateContent(event.message);
831
+ this.streamingComponent.updateContent(this.streamingMessage);
659
832
  this.ui.requestRender();
660
833
  }
661
834
  break;
662
835
  case "message_update":
663
836
  if (this.streamingComponent && event.message.role === "assistant") {
664
- const assistantMsg = event.message;
665
- this.streamingComponent.updateContent(assistantMsg);
666
- for (const content of assistantMsg.content) {
837
+ this.streamingMessage = event.message;
838
+ this.streamingComponent.updateContent(this.streamingMessage);
839
+ for (const content of this.streamingMessage.content) {
667
840
  if (content.type === "toolCall") {
668
841
  if (!this.pendingTools.has(content.id)) {
669
842
  this.chatContainer.addChild(new Text("", 0, 0));
670
843
  const component = new ToolExecutionComponent(content.name, content.arguments, {
671
844
  showImages: this.settingsManager.getShowImages(),
672
845
  }, this.customTools.get(content.name)?.tool, this.ui);
846
+ component.setExpanded(this.toolOutputExpanded);
673
847
  this.chatContainer.addChild(component);
674
848
  this.pendingTools.set(content.id, component);
675
849
  }
@@ -688,10 +862,12 @@ export class InteractiveMode {
688
862
  if (event.message.role === "user")
689
863
  break;
690
864
  if (this.streamingComponent && event.message.role === "assistant") {
691
- const assistantMsg = event.message;
692
- this.streamingComponent.updateContent(assistantMsg);
693
- if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
694
- const errorMessage = assistantMsg.stopReason === "aborted" ? "Operation aborted" : assistantMsg.errorMessage || "Error";
865
+ this.streamingMessage = event.message;
866
+ this.streamingComponent.updateContent(this.streamingMessage);
867
+ if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
868
+ const errorMessage = this.streamingMessage.stopReason === "aborted"
869
+ ? "Operation aborted"
870
+ : this.streamingMessage.errorMessage || "Error";
695
871
  for (const [, component] of this.pendingTools.entries()) {
696
872
  component.updateResult({
697
873
  content: [{ type: "text", text: errorMessage }],
@@ -700,7 +876,14 @@ export class InteractiveMode {
700
876
  }
701
877
  this.pendingTools.clear();
702
878
  }
703
- this.streamingComponent = null;
879
+ else {
880
+ // Args are now complete - trigger diff computation for edit tools
881
+ for (const [, component] of this.pendingTools.entries()) {
882
+ component.setArgsComplete();
883
+ }
884
+ }
885
+ this.streamingComponent = undefined;
886
+ this.streamingMessage = undefined;
704
887
  this.footer.invalidate();
705
888
  }
706
889
  this.ui.requestRender();
@@ -710,6 +893,7 @@ export class InteractiveMode {
710
893
  const component = new ToolExecutionComponent(event.toolName, event.args, {
711
894
  showImages: this.settingsManager.getShowImages(),
712
895
  }, this.customTools.get(event.toolName)?.tool, this.ui);
896
+ component.setExpanded(this.toolOutputExpanded);
713
897
  this.chatContainer.addChild(component);
714
898
  this.pendingTools.set(event.toolCallId, component);
715
899
  this.ui.requestRender();
@@ -736,12 +920,13 @@ export class InteractiveMode {
736
920
  case "agent_end":
737
921
  if (this.loadingAnimation) {
738
922
  this.loadingAnimation.stop();
739
- this.loadingAnimation = null;
923
+ this.loadingAnimation = undefined;
740
924
  this.statusContainer.clear();
741
925
  }
742
926
  if (this.streamingComponent) {
743
927
  this.chatContainer.removeChild(this.streamingComponent);
744
- this.streamingComponent = null;
928
+ this.streamingComponent = undefined;
929
+ this.streamingMessage = undefined;
745
930
  }
746
931
  this.pendingTools.clear();
747
932
  this.ui.requestRender();
@@ -773,7 +958,7 @@ export class InteractiveMode {
773
958
  // Stop loader
774
959
  if (this.autoCompactionLoader) {
775
960
  this.autoCompactionLoader.stop();
776
- this.autoCompactionLoader = null;
961
+ this.autoCompactionLoader = undefined;
777
962
  this.statusContainer.clear();
778
963
  }
779
964
  // Handle result
@@ -784,11 +969,14 @@ export class InteractiveMode {
784
969
  // Rebuild chat to show compacted state
785
970
  this.chatContainer.clear();
786
971
  this.rebuildChatFromMessages();
787
- // Add compaction component (same as manual /compact)
788
- const compactionComponent = new CompactionComponent(event.result.tokensBefore, event.result.summary);
789
- compactionComponent.setExpanded(this.toolOutputExpanded);
790
- this.chatContainer.addChild(compactionComponent);
791
- this.footer.updateState(this.session.state);
972
+ // Add compaction component at bottom so user sees it without scrolling
973
+ this.addMessageToChat({
974
+ role: "compactionSummary",
975
+ tokensBefore: event.result.tokensBefore,
976
+ summary: event.result.summary,
977
+ timestamp: Date.now(),
978
+ });
979
+ this.footer.invalidate();
792
980
  }
793
981
  this.ui.requestRender();
794
982
  break;
@@ -816,7 +1004,7 @@ export class InteractiveMode {
816
1004
  // Stop loader
817
1005
  if (this.retryLoader) {
818
1006
  this.retryLoader.stop();
819
- this.retryLoader = null;
1007
+ this.retryLoader = undefined;
820
1008
  this.statusContainer.clear();
821
1009
  }
822
1010
  // Show error only on final failure (success shows normal response)
@@ -837,87 +1025,110 @@ export class InteractiveMode {
837
1025
  : message.content.filter((c) => c.type === "text");
838
1026
  return textBlocks.map((c) => c.text).join("");
839
1027
  }
840
- /** Show a status message in the chat */
1028
+ /**
1029
+ * Show a status message in the chat.
1030
+ *
1031
+ * If multiple status messages are emitted back-to-back (without anything else being added to the chat),
1032
+ * we update the previous status line instead of appending new ones to avoid log spam.
1033
+ */
841
1034
  showStatus(message) {
842
- this.chatContainer.addChild(new Spacer(1));
843
- this.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
1035
+ const children = this.chatContainer.children;
1036
+ const last = children.length > 0 ? children[children.length - 1] : undefined;
1037
+ const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
1038
+ if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
1039
+ this.lastStatusText.setText(theme.fg("dim", message));
1040
+ this.ui.requestRender();
1041
+ return;
1042
+ }
1043
+ const spacer = new Spacer(1);
1044
+ const text = new Text(theme.fg("dim", message), 1, 0);
1045
+ this.chatContainer.addChild(spacer);
1046
+ this.chatContainer.addChild(text);
1047
+ this.lastStatusSpacer = spacer;
1048
+ this.lastStatusText = text;
844
1049
  this.ui.requestRender();
845
1050
  }
846
- addMessageToChat(message) {
847
- if (isBashExecutionMessage(message)) {
848
- const component = new BashExecutionComponent(message.command, this.ui);
849
- if (message.output) {
850
- component.appendOutput(message.output);
1051
+ addMessageToChat(message, options) {
1052
+ switch (message.role) {
1053
+ case "bashExecution": {
1054
+ const component = new BashExecutionComponent(message.command, this.ui);
1055
+ if (message.output) {
1056
+ component.appendOutput(message.output);
1057
+ }
1058
+ component.setComplete(message.exitCode, message.cancelled, message.truncated ? { truncated: true } : undefined, message.fullOutputPath);
1059
+ this.chatContainer.addChild(component);
1060
+ break;
851
1061
  }
852
- component.setComplete(message.exitCode, message.cancelled, message.truncated ? { truncated: true } : undefined, message.fullOutputPath);
853
- this.chatContainer.addChild(component);
854
- return;
855
- }
856
- if (message.role === "user") {
857
- const textContent = this.getUserMessageText(message);
858
- if (textContent) {
859
- const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
860
- this.chatContainer.addChild(userComponent);
861
- this.isFirstUserMessage = false;
1062
+ case "hookMessage": {
1063
+ if (message.display) {
1064
+ const renderer = this.session.hookRunner?.getMessageRenderer(message.customType);
1065
+ this.chatContainer.addChild(new HookMessageComponent(message, renderer));
1066
+ }
1067
+ break;
1068
+ }
1069
+ case "compactionSummary": {
1070
+ this.chatContainer.addChild(new Spacer(1));
1071
+ const component = new CompactionSummaryMessageComponent(message);
1072
+ component.setExpanded(this.toolOutputExpanded);
1073
+ this.chatContainer.addChild(component);
1074
+ break;
1075
+ }
1076
+ case "branchSummary": {
1077
+ this.chatContainer.addChild(new Spacer(1));
1078
+ const component = new BranchSummaryMessageComponent(message);
1079
+ component.setExpanded(this.toolOutputExpanded);
1080
+ this.chatContainer.addChild(component);
1081
+ break;
1082
+ }
1083
+ case "user": {
1084
+ const textContent = this.getUserMessageText(message);
1085
+ if (textContent) {
1086
+ const userComponent = new UserMessageComponent(textContent);
1087
+ this.chatContainer.addChild(userComponent);
1088
+ if (options?.populateHistory) {
1089
+ this.editor.addToHistory(textContent);
1090
+ }
1091
+ }
1092
+ break;
1093
+ }
1094
+ case "assistant": {
1095
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock);
1096
+ this.chatContainer.addChild(assistantComponent);
1097
+ break;
1098
+ }
1099
+ case "toolResult": {
1100
+ // Tool results are rendered inline with tool calls, handled separately
1101
+ break;
1102
+ }
1103
+ default: {
1104
+ const _exhaustive = message;
862
1105
  }
863
- }
864
- else if (message.role === "assistant") {
865
- const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock);
866
- this.chatContainer.addChild(assistantComponent);
867
1106
  }
868
1107
  }
869
1108
  /**
870
- * Render messages to chat. Used for initial load and rebuild after compaction.
871
- * @param messages Messages to render
1109
+ * Render session context to chat. Used for initial load and rebuild after compaction.
1110
+ * @param sessionContext Session context to render
872
1111
  * @param options.updateFooter Update footer state
873
1112
  * @param options.populateHistory Add user messages to editor history
874
1113
  */
875
- renderMessages(messages, options = {}) {
876
- this.isFirstUserMessage = true;
1114
+ renderSessionContext(sessionContext, options = {}) {
877
1115
  this.pendingTools.clear();
878
1116
  if (options.updateFooter) {
879
- this.footer.updateState(this.session.state);
1117
+ this.footer.invalidate();
880
1118
  this.updateEditorBorderColor();
881
1119
  }
882
- const compactionEntry = getLatestCompactionEntry(this.sessionManager.getEntries());
883
- for (const message of messages) {
884
- if (isBashExecutionMessage(message)) {
1120
+ for (const message of sessionContext.messages) {
1121
+ // Assistant messages need special handling for tool calls
1122
+ if (message.role === "assistant") {
885
1123
  this.addMessageToChat(message);
886
- continue;
887
- }
888
- if (message.role === "user") {
889
- const textContent = this.getUserMessageText(message);
890
- if (textContent) {
891
- if (textContent.startsWith(SUMMARY_PREFIX) && compactionEntry) {
892
- const summary = textContent.slice(SUMMARY_PREFIX.length, -SUMMARY_SUFFIX.length);
893
- const component = new CompactionComponent(compactionEntry.tokensBefore, summary);
894
- component.setExpanded(this.toolOutputExpanded);
895
- this.chatContainer.addChild(component);
896
- }
897
- else {
898
- const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
899
- this.chatContainer.addChild(userComponent);
900
- this.isFirstUserMessage = false;
901
- if (options.populateHistory) {
902
- this.editor.addToHistory(textContent);
903
- }
904
- }
905
- }
906
- }
907
- else if (message.role === "assistant") {
908
- const assistantMsg = message;
909
- const assistantComponent = new AssistantMessageComponent(assistantMsg, this.hideThinkingBlock);
910
- this.chatContainer.addChild(assistantComponent);
911
- for (const content of assistantMsg.content) {
1124
+ // Render tool call components
1125
+ for (const content of message.content) {
912
1126
  if (content.type === "toolCall") {
913
- const component = new ToolExecutionComponent(content.name, content.arguments, {
914
- showImages: this.settingsManager.getShowImages(),
915
- }, this.customTools.get(content.name)?.tool, this.ui);
1127
+ const component = new ToolExecutionComponent(content.name, content.arguments, { showImages: this.settingsManager.getShowImages() }, this.customTools.get(content.name)?.tool, this.ui);
1128
+ component.setExpanded(this.toolOutputExpanded);
916
1129
  this.chatContainer.addChild(component);
917
- if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
918
- const errorMessage = assistantMsg.stopReason === "aborted"
919
- ? "Operation aborted"
920
- : assistantMsg.errorMessage || "Error";
1130
+ if (message.stopReason === "aborted" || message.stopReason === "error") {
1131
+ const errorMessage = message.stopReason === "aborted" ? "Operation aborted" : message.errorMessage || "Error";
921
1132
  component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
922
1133
  }
923
1134
  else {
@@ -927,21 +1138,31 @@ export class InteractiveMode {
927
1138
  }
928
1139
  }
929
1140
  else if (message.role === "toolResult") {
1141
+ // Match tool results to pending tool components
930
1142
  const component = this.pendingTools.get(message.toolCallId);
931
1143
  if (component) {
932
1144
  component.updateResult(message);
933
1145
  this.pendingTools.delete(message.toolCallId);
934
1146
  }
935
1147
  }
1148
+ else {
1149
+ // All other messages use standard rendering
1150
+ this.addMessageToChat(message, options);
1151
+ }
936
1152
  }
937
1153
  this.pendingTools.clear();
938
1154
  this.ui.requestRender();
939
1155
  }
940
- renderInitialMessages(state) {
941
- this.renderMessages(state.messages, { updateFooter: true, populateHistory: true });
1156
+ renderInitialMessages() {
1157
+ // Get aligned messages and entries from session context
1158
+ const context = this.sessionManager.buildSessionContext();
1159
+ this.renderSessionContext(context, {
1160
+ updateFooter: true,
1161
+ populateHistory: true,
1162
+ });
942
1163
  // Show compaction info if session was compacted
943
- const entries = this.sessionManager.getEntries();
944
- const compactionCount = entries.filter((e) => e.type === "compaction").length;
1164
+ const allEntries = this.sessionManager.getEntries();
1165
+ const compactionCount = allEntries.filter((e) => e.type === "compaction").length;
945
1166
  if (compactionCount > 0) {
946
1167
  const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`;
947
1168
  this.showStatus(`Session compacted ${times}`);
@@ -956,7 +1177,9 @@ export class InteractiveMode {
956
1177
  });
957
1178
  }
958
1179
  rebuildChatFromMessages() {
959
- this.renderMessages(this.session.messages);
1180
+ this.chatContainer.clear();
1181
+ const context = this.sessionManager.buildSessionContext();
1182
+ this.renderSessionContext(context);
960
1183
  }
961
1184
  // =========================================================================
962
1185
  // Key handlers
@@ -977,21 +1200,18 @@ export class InteractiveMode {
977
1200
  }
978
1201
  /**
979
1202
  * Gracefully shutdown the agent.
980
- * Emits shutdown event to hooks, then exits.
1203
+ * Emits shutdown event to hooks and tools, then exits.
981
1204
  */
982
1205
  async shutdown() {
983
1206
  // Emit shutdown event to hooks
984
1207
  const hookRunner = this.session.hookRunner;
985
- if (hookRunner?.hasHandlers("session")) {
986
- const entries = this.sessionManager.getEntries();
1208
+ if (hookRunner?.hasHandlers("session_shutdown")) {
987
1209
  await hookRunner.emit({
988
- type: "session",
989
- entries,
990
- sessionFile: this.session.sessionFile,
991
- previousSessionFile: null,
992
- reason: "shutdown",
1210
+ type: "session_shutdown",
993
1211
  });
994
1212
  }
1213
+ // Emit shutdown event to custom tools
1214
+ await this.session.emitCustomToolSessionEvent("shutdown");
995
1215
  this.stop();
996
1216
  process.exit(0);
997
1217
  }
@@ -1018,11 +1238,11 @@ export class InteractiveMode {
1018
1238
  }
1019
1239
  cycleThinkingLevel() {
1020
1240
  const newLevel = this.session.cycleThinkingLevel();
1021
- if (newLevel === null) {
1241
+ if (newLevel === undefined) {
1022
1242
  this.showStatus("Current model does not support thinking");
1023
1243
  }
1024
1244
  else {
1025
- this.footer.updateState(this.session.state);
1245
+ this.footer.invalidate();
1026
1246
  this.updateEditorBorderColor();
1027
1247
  this.showStatus(`Thinking level: ${newLevel}`);
1028
1248
  }
@@ -1030,12 +1250,12 @@ export class InteractiveMode {
1030
1250
  async cycleModel(direction) {
1031
1251
  try {
1032
1252
  const result = await this.session.cycleModel(direction);
1033
- if (result === null) {
1253
+ if (result === undefined) {
1034
1254
  const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
1035
1255
  this.showStatus(msg);
1036
1256
  }
1037
1257
  else {
1038
- this.footer.updateState(this.session.state);
1258
+ this.footer.invalidate();
1039
1259
  this.updateEditorBorderColor();
1040
1260
  const thinkingStr = result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
1041
1261
  this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
@@ -1048,13 +1268,7 @@ export class InteractiveMode {
1048
1268
  toggleToolOutputExpansion() {
1049
1269
  this.toolOutputExpanded = !this.toolOutputExpanded;
1050
1270
  for (const child of this.chatContainer.children) {
1051
- if (child instanceof ToolExecutionComponent) {
1052
- child.setExpanded(this.toolOutputExpanded);
1053
- }
1054
- else if (child instanceof CompactionComponent) {
1055
- child.setExpanded(this.toolOutputExpanded);
1056
- }
1057
- else if (child instanceof BashExecutionComponent) {
1271
+ if (isExpandable(child)) {
1058
1272
  child.setExpanded(this.toolOutputExpanded);
1059
1273
  }
1060
1274
  }
@@ -1063,13 +1277,15 @@ export class InteractiveMode {
1063
1277
  toggleThinkingBlockVisibility() {
1064
1278
  this.hideThinkingBlock = !this.hideThinkingBlock;
1065
1279
  this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
1066
- for (const child of this.chatContainer.children) {
1067
- if (child instanceof AssistantMessageComponent) {
1068
- child.setHideThinkingBlock(this.hideThinkingBlock);
1069
- }
1070
- }
1280
+ // Rebuild chat from session messages
1071
1281
  this.chatContainer.clear();
1072
1282
  this.rebuildChatFromMessages();
1283
+ // If streaming, re-add the streaming component with updated visibility and re-render
1284
+ if (this.streamingComponent && this.streamingMessage) {
1285
+ this.streamingComponent.setHideThinkingBlock(this.hideThinkingBlock);
1286
+ this.streamingComponent.updateContent(this.streamingMessage);
1287
+ this.chatContainer.addChild(this.streamingComponent);
1288
+ }
1073
1289
  this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
1074
1290
  }
1075
1291
  openExternalEditor() {
@@ -1207,7 +1423,7 @@ export class InteractiveMode {
1207
1423
  },
1208
1424
  onThinkingLevelChange: (level) => {
1209
1425
  this.session.setThinkingLevel(level);
1210
- this.footer.updateState(this.session.state);
1426
+ this.footer.invalidate();
1211
1427
  this.updateEditorBorderColor();
1212
1428
  },
1213
1429
  onThemeChange: (themeName) => {
@@ -1252,7 +1468,7 @@ export class InteractiveMode {
1252
1468
  const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
1253
1469
  try {
1254
1470
  await this.session.setModel(model);
1255
- this.footer.updateState(this.session.state);
1471
+ this.footer.invalidate();
1256
1472
  this.updateEditorBorderColor();
1257
1473
  done();
1258
1474
  this.showStatus(`Model: ${model.id}`);
@@ -1275,8 +1491,8 @@ export class InteractiveMode {
1275
1491
  return;
1276
1492
  }
1277
1493
  this.showSelector((done) => {
1278
- const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ index: m.entryIndex, text: m.text })), async (entryIndex) => {
1279
- const result = await this.session.branch(entryIndex);
1494
+ const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
1495
+ const result = await this.session.branch(entryId);
1280
1496
  if (result.cancelled) {
1281
1497
  // Hook cancelled the branch
1282
1498
  done();
@@ -1284,8 +1500,7 @@ export class InteractiveMode {
1284
1500
  return;
1285
1501
  }
1286
1502
  this.chatContainer.clear();
1287
- this.isFirstUserMessage = true;
1288
- this.renderInitialMessages(this.session.state);
1503
+ this.renderInitialMessages();
1289
1504
  this.editor.setText(result.selectedText);
1290
1505
  done();
1291
1506
  this.showStatus("Branched to new session");
@@ -1296,6 +1511,86 @@ export class InteractiveMode {
1296
1511
  return { component: selector, focus: selector.getMessageList() };
1297
1512
  });
1298
1513
  }
1514
+ showTreeSelector() {
1515
+ const tree = this.sessionManager.getTree();
1516
+ const realLeafId = this.sessionManager.getLeafId();
1517
+ // Find the visible leaf for display (skip metadata entries like labels)
1518
+ let visibleLeafId = realLeafId;
1519
+ while (visibleLeafId) {
1520
+ const entry = this.sessionManager.getEntry(visibleLeafId);
1521
+ if (!entry)
1522
+ break;
1523
+ if (entry.type !== "label" && entry.type !== "custom")
1524
+ break;
1525
+ visibleLeafId = entry.parentId ?? null;
1526
+ }
1527
+ if (tree.length === 0) {
1528
+ this.showStatus("No entries in session");
1529
+ return;
1530
+ }
1531
+ this.showSelector((done) => {
1532
+ const selector = new TreeSelectorComponent(tree, visibleLeafId, this.ui.terminal.rows, async (entryId) => {
1533
+ // Selecting the visible leaf is a no-op (already there)
1534
+ if (entryId === visibleLeafId) {
1535
+ done();
1536
+ this.showStatus("Already at this point");
1537
+ return;
1538
+ }
1539
+ // Ask about summarization
1540
+ done(); // Close selector first
1541
+ const wantsSummary = await this.showHookConfirm("Summarize branch?", "Create a summary of the branch you're leaving?");
1542
+ // Set up escape handler and loader if summarizing
1543
+ let summaryLoader;
1544
+ const originalOnEscape = this.editor.onEscape;
1545
+ if (wantsSummary) {
1546
+ this.editor.onEscape = () => {
1547
+ this.session.abortBranchSummary();
1548
+ };
1549
+ this.chatContainer.addChild(new Spacer(1));
1550
+ summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), "Summarizing branch... (esc to cancel)");
1551
+ this.statusContainer.addChild(summaryLoader);
1552
+ this.ui.requestRender();
1553
+ }
1554
+ try {
1555
+ const result = await this.session.navigateTree(entryId, { summarize: wantsSummary });
1556
+ if (result.aborted) {
1557
+ // Summarization aborted - re-show tree selector
1558
+ this.showStatus("Branch summarization cancelled");
1559
+ this.showTreeSelector();
1560
+ return;
1561
+ }
1562
+ if (result.cancelled) {
1563
+ this.showStatus("Navigation cancelled");
1564
+ return;
1565
+ }
1566
+ // Update UI
1567
+ this.chatContainer.clear();
1568
+ this.renderInitialMessages();
1569
+ if (result.editorText) {
1570
+ this.editor.setText(result.editorText);
1571
+ }
1572
+ this.showStatus("Navigated to selected point");
1573
+ }
1574
+ catch (error) {
1575
+ this.showError(error instanceof Error ? error.message : String(error));
1576
+ }
1577
+ finally {
1578
+ if (summaryLoader) {
1579
+ summaryLoader.stop();
1580
+ this.statusContainer.clear();
1581
+ }
1582
+ this.editor.onEscape = originalOnEscape;
1583
+ }
1584
+ }, () => {
1585
+ done();
1586
+ this.ui.requestRender();
1587
+ }, (entryId, label) => {
1588
+ this.sessionManager.appendLabelChange(entryId, label);
1589
+ this.ui.requestRender();
1590
+ });
1591
+ return { component: selector, focus: selector };
1592
+ });
1593
+ }
1299
1594
  showSessionSelector() {
1300
1595
  this.showSelector((done) => {
1301
1596
  const sessions = SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir());
@@ -1315,19 +1610,19 @@ export class InteractiveMode {
1315
1610
  // Stop loading animation
1316
1611
  if (this.loadingAnimation) {
1317
1612
  this.loadingAnimation.stop();
1318
- this.loadingAnimation = null;
1613
+ this.loadingAnimation = undefined;
1319
1614
  }
1320
1615
  this.statusContainer.clear();
1321
1616
  // Clear UI state
1322
1617
  this.pendingMessagesContainer.clear();
1323
- this.streamingComponent = null;
1618
+ this.streamingComponent = undefined;
1619
+ this.streamingMessage = undefined;
1324
1620
  this.pendingTools.clear();
1325
1621
  // Switch session via AgentSession (emits hook and tool session events)
1326
1622
  await this.session.switchSession(sessionPath);
1327
1623
  // Clear and re-render the chat
1328
1624
  this.chatContainer.clear();
1329
- this.isFirstUserMessage = true;
1330
- this.renderInitialMessages(this.session.state);
1625
+ this.renderInitialMessages();
1331
1626
  this.showStatus("Resumed session");
1332
1627
  }
1333
1628
  async showOAuthSelector(mode) {
@@ -1348,8 +1643,9 @@ export class InteractiveMode {
1348
1643
  await this.session.modelRegistry.authStorage.login(providerId, {
1349
1644
  onAuth: (info) => {
1350
1645
  this.chatContainer.addChild(new Spacer(1));
1351
- this.chatContainer.addChild(new Text(theme.fg("accent", "Opening browser to:"), 1, 0));
1352
- this.chatContainer.addChild(new Text(theme.fg("accent", info.url), 1, 0));
1646
+ // Use OSC 8 hyperlink escape sequence for clickable link
1647
+ const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
1648
+ this.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
1353
1649
  if (info.instructions) {
1354
1650
  this.chatContainer.addChild(new Spacer(1));
1355
1651
  this.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
@@ -1435,6 +1731,93 @@ export class InteractiveMode {
1435
1731
  this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
1436
1732
  }
1437
1733
  }
1734
+ async handleShareCommand() {
1735
+ // Check if gh is available and logged in
1736
+ try {
1737
+ const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });
1738
+ if (authResult.status !== 0) {
1739
+ this.showError("GitHub CLI is not logged in. Run 'gh auth login' first.");
1740
+ return;
1741
+ }
1742
+ }
1743
+ catch {
1744
+ this.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/");
1745
+ return;
1746
+ }
1747
+ // Export to a temp file
1748
+ const tmpFile = path.join(os.tmpdir(), "session.html");
1749
+ try {
1750
+ this.session.exportToHtml(tmpFile);
1751
+ }
1752
+ catch (error) {
1753
+ this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
1754
+ return;
1755
+ }
1756
+ // Show cancellable loader, replacing the editor
1757
+ const loader = new BorderedLoader(this.ui, theme, "Creating gist...");
1758
+ this.editorContainer.clear();
1759
+ this.editorContainer.addChild(loader);
1760
+ this.ui.setFocus(loader);
1761
+ this.ui.requestRender();
1762
+ const restoreEditor = () => {
1763
+ loader.dispose();
1764
+ this.editorContainer.clear();
1765
+ this.editorContainer.addChild(this.editor);
1766
+ this.ui.setFocus(this.editor);
1767
+ try {
1768
+ fs.unlinkSync(tmpFile);
1769
+ }
1770
+ catch {
1771
+ // Ignore cleanup errors
1772
+ }
1773
+ };
1774
+ // Create a secret gist asynchronously
1775
+ let proc = null;
1776
+ loader.onAbort = () => {
1777
+ proc?.kill();
1778
+ restoreEditor();
1779
+ this.showStatus("Share cancelled");
1780
+ };
1781
+ try {
1782
+ const result = await new Promise((resolve) => {
1783
+ proc = spawn("gh", ["gist", "create", "--public=false", tmpFile]);
1784
+ let stdout = "";
1785
+ let stderr = "";
1786
+ proc.stdout?.on("data", (data) => {
1787
+ stdout += data.toString();
1788
+ });
1789
+ proc.stderr?.on("data", (data) => {
1790
+ stderr += data.toString();
1791
+ });
1792
+ proc.on("close", (code) => resolve({ stdout, stderr, code }));
1793
+ });
1794
+ if (loader.signal.aborted)
1795
+ return;
1796
+ restoreEditor();
1797
+ if (result.code !== 0) {
1798
+ const errorMsg = result.stderr?.trim() || "Unknown error";
1799
+ this.showError(`Failed to create gist: ${errorMsg}`);
1800
+ return;
1801
+ }
1802
+ // Extract gist ID from the URL returned by gh
1803
+ // gh returns something like: https://gist.github.com/username/GIST_ID
1804
+ const gistUrl = result.stdout?.trim();
1805
+ const gistId = gistUrl?.split("/").pop();
1806
+ if (!gistId) {
1807
+ this.showError("Failed to parse gist ID from gh output");
1808
+ return;
1809
+ }
1810
+ // Create the preview URL
1811
+ const previewUrl = `https://shittycodingagent.ai/session?${gistId}`;
1812
+ this.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
1813
+ }
1814
+ catch (error) {
1815
+ if (!loader.signal.aborted) {
1816
+ restoreEditor();
1817
+ this.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`);
1818
+ }
1819
+ }
1820
+ }
1438
1821
  handleCopyCommand() {
1439
1822
  const text = this.session.getLastAssistantText();
1440
1823
  if (!text) {
@@ -1542,17 +1925,17 @@ export class InteractiveMode {
1542
1925
  // Stop loading animation
1543
1926
  if (this.loadingAnimation) {
1544
1927
  this.loadingAnimation.stop();
1545
- this.loadingAnimation = null;
1928
+ this.loadingAnimation = undefined;
1546
1929
  }
1547
1930
  this.statusContainer.clear();
1548
- // Reset via session (emits hook and tool session events)
1549
- await this.session.reset();
1931
+ // New session via session (emits hook and tool session events)
1932
+ await this.session.newSession();
1550
1933
  // Clear UI state
1551
1934
  this.chatContainer.clear();
1552
1935
  this.pendingMessagesContainer.clear();
1553
- this.streamingComponent = null;
1936
+ this.streamingComponent = undefined;
1937
+ this.streamingMessage = undefined;
1554
1938
  this.pendingTools.clear();
1555
- this.isFirstUserMessage = true;
1556
1939
  this.chatContainer.addChild(new Spacer(1));
1557
1940
  this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
1558
1941
  this.ui.requestRender();
@@ -1614,11 +1997,11 @@ export class InteractiveMode {
1614
1997
  }
1615
1998
  catch (error) {
1616
1999
  if (this.bashComponent) {
1617
- this.bashComponent.setComplete(null, false);
2000
+ this.bashComponent.setComplete(undefined, false);
1618
2001
  }
1619
2002
  this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
1620
2003
  }
1621
- this.bashComponent = null;
2004
+ this.bashComponent = undefined;
1622
2005
  this.ui.requestRender();
1623
2006
  }
1624
2007
  async handleCompactCommand(customInstructions) {
@@ -1634,7 +2017,7 @@ export class InteractiveMode {
1634
2017
  // Stop loading animation
1635
2018
  if (this.loadingAnimation) {
1636
2019
  this.loadingAnimation.stop();
1637
- this.loadingAnimation = null;
2020
+ this.loadingAnimation = undefined;
1638
2021
  }
1639
2022
  this.statusContainer.clear();
1640
2023
  // Set up escape handler during compaction
@@ -1651,13 +2034,11 @@ export class InteractiveMode {
1651
2034
  try {
1652
2035
  const result = await this.session.compact(customInstructions);
1653
2036
  // Rebuild UI
1654
- this.chatContainer.clear();
1655
2037
  this.rebuildChatFromMessages();
1656
- // Add compaction component
1657
- const compactionComponent = new CompactionComponent(result.tokensBefore, result.summary);
1658
- compactionComponent.setExpanded(this.toolOutputExpanded);
1659
- this.chatContainer.addChild(compactionComponent);
1660
- this.footer.updateState(this.session.state);
2038
+ // Add compaction component at bottom so user sees it without scrolling
2039
+ const msg = createCompactionSummaryMessage(result.summary, result.tokensBefore, new Date().toISOString());
2040
+ this.addMessageToChat(msg);
2041
+ this.footer.invalidate();
1661
2042
  }
1662
2043
  catch (error) {
1663
2044
  const message = error instanceof Error ? error.message : String(error);
@@ -1677,7 +2058,7 @@ export class InteractiveMode {
1677
2058
  stop() {
1678
2059
  if (this.loadingAnimation) {
1679
2060
  this.loadingAnimation.stop();
1680
- this.loadingAnimation = null;
2061
+ this.loadingAnimation = undefined;
1681
2062
  }
1682
2063
  this.footer.dispose();
1683
2064
  if (this.unsubscribe) {