@linxiraos/pi-tui 1.2.1 → 1.3.1

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 (280) hide show
  1. package/CHANGELOG.md +245 -224
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.txt +55 -5
  4. package/dist/types/app-keybindings.d.ts +18 -8
  5. package/dist/types/apps/git/diff-pane.d.ts +5 -0
  6. package/dist/types/apps/git/git-tui.d.ts +16 -2
  7. package/dist/types/apps/git/state.d.ts +11 -0
  8. package/dist/types/autocomplete.d.ts +2 -0
  9. package/dist/types/charts/chart-svg.d.ts +7 -13
  10. package/dist/types/chat/advisor-message.d.ts +4 -1
  11. package/dist/types/chat/assistant-message.d.ts +9 -4
  12. package/dist/types/chat/figure-markdown.d.ts +7 -2
  13. package/dist/types/chat/messages.d.ts +3 -1
  14. package/dist/types/chat/read-tool-group.d.ts +2 -2
  15. package/dist/types/chat/table-chart.d.ts +11 -15
  16. package/dist/types/chat/tool-execution.d.ts +32 -20
  17. package/dist/types/chat/user-message.d.ts +3 -2
  18. package/dist/types/chrome/countdown-timer.d.ts +3 -2
  19. package/dist/types/chrome/diff.d.ts +2 -3
  20. package/dist/types/chrome/status-notice.d.ts +2 -0
  21. package/dist/types/chrome/transcript-container.d.ts +2 -0
  22. package/dist/types/components/image.d.ts +2 -0
  23. package/dist/types/components/markdown.d.ts +49 -3
  24. package/dist/types/components/text.d.ts +2 -0
  25. package/dist/types/debug-server.d.ts +2 -0
  26. package/dist/types/keybinding-matchers.d.ts +6 -0
  27. package/dist/types/multiplexers/tmux.d.ts +1 -0
  28. package/dist/types/multiplexers/types.d.ts +6 -0
  29. package/dist/types/native/backend.d.ts +5 -4
  30. package/dist/types/native/blobs.d.ts +24 -12
  31. package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
  32. package/dist/types/overlays/annotation-overlay.d.ts +19 -0
  33. package/dist/types/overlays/approval-dialog.d.ts +79 -0
  34. package/dist/types/overlays/ask-dialog.d.ts +11 -6
  35. package/dist/types/overlays/btw-history-panel.d.ts +2 -6
  36. package/dist/types/overlays/hook-editor.d.ts +7 -2
  37. package/dist/types/overlays/hook-input.d.ts +12 -4
  38. package/dist/types/overlays/job-tree.d.ts +50 -0
  39. package/dist/types/overlays/jobs-panel.d.ts +25 -11
  40. package/dist/types/overlays/login-dialog.d.ts +8 -3
  41. package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
  42. package/dist/types/overlays/model-browser.d.ts +6 -1
  43. package/dist/types/overlays/model-picker.d.ts +2 -0
  44. package/dist/types/overlays/pause-screen.d.ts +5 -0
  45. package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
  46. package/dist/types/overlays/report-panel.d.ts +5 -1
  47. package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
  48. package/dist/types/overlays/session-account-selector.d.ts +4 -0
  49. package/dist/types/overlays/session-info-overlay.d.ts +7 -3
  50. package/dist/types/overlays/session-selector.d.ts +3 -1
  51. package/dist/types/overlays/usage-dashboard.d.ts +6 -6
  52. package/dist/types/prompt/composer.d.ts +23 -20
  53. package/dist/types/prompt/custom-editor.d.ts +20 -10
  54. package/dist/types/prompt/image-references.d.ts +22 -0
  55. package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
  56. package/dist/types/prompt/macos-spelling.d.ts +2 -1
  57. package/dist/types/prompt/prose-gate.d.ts +3 -0
  58. package/dist/types/prompt/queued-messages.d.ts +1 -1
  59. package/dist/types/prompt/typing-word.d.ts +25 -0
  60. package/dist/types/prompt/welcome.d.ts +26 -6
  61. package/dist/types/render/hanging-wrap.d.ts +7 -0
  62. package/dist/types/render/hyperlink.d.ts +2 -0
  63. package/dist/types/render/render-utils.d.ts +9 -1
  64. package/dist/types/render/utils.d.ts +1 -1
  65. package/dist/types/setup/scenes/splash.d.ts +6 -5
  66. package/dist/types/status-line/context-usage.d.ts +4 -1
  67. package/dist/types/status-line/host.d.ts +6 -4
  68. package/dist/types/status-line/startup.d.ts +6 -6
  69. package/dist/types/status-line/types.d.ts +3 -0
  70. package/dist/types/stdin-buffer.d.ts +7 -0
  71. package/dist/types/terminal-capabilities.d.ts +16 -1
  72. package/dist/types/terminal-multiplexer.d.ts +7 -0
  73. package/dist/types/terminal.d.ts +8 -4
  74. package/dist/types/theme/symbols.d.ts +1 -1
  75. package/dist/types/theme/theme.d.ts +11 -0
  76. package/dist/types/thinking.d.ts +5 -0
  77. package/dist/types/tmux.d.ts +39 -4
  78. package/dist/types/tools/ast-edit.d.ts +2 -2
  79. package/dist/types/tools/ast-grep.d.ts +2 -2
  80. package/dist/types/tools/bash.d.ts +6 -1
  81. package/dist/types/tools/edit.d.ts +2 -0
  82. package/dist/types/tools/eval.d.ts +53 -4
  83. package/dist/types/tools/find.d.ts +14 -4
  84. package/dist/types/tools/glob.d.ts +3 -3
  85. package/dist/types/tools/grep.d.ts +2 -2
  86. package/dist/types/tools/grouped-file-output.d.ts +9 -4
  87. package/dist/types/tools/native-view.d.ts +1 -1
  88. package/dist/types/tools/read.d.ts +32 -10
  89. package/dist/types/tools/renderer.d.ts +2 -0
  90. package/dist/types/tools/task.d.ts +31 -0
  91. package/dist/types/tools/web-search-types.d.ts +1 -0
  92. package/dist/types/tools/write.d.ts +2 -0
  93. package/dist/types/tui.d.ts +20 -3
  94. package/package.json +10 -10
  95. package/src/app-keybindings.d.ts +18 -8
  96. package/src/app-keybindings.ts +20 -8
  97. package/src/apps/autoresearch-dashboard.ts +444 -27
  98. package/src/apps/debug/raw-sse.ts +2 -2
  99. package/src/apps/git/diff-pane.ts +39 -41
  100. package/src/apps/git/git-tui.ts +80 -14
  101. package/src/apps/git/sidebar.ts +10 -1
  102. package/src/apps/git/state.ts +12 -0
  103. package/src/autocomplete.d.ts +2 -0
  104. package/src/autocomplete.ts +21 -19
  105. package/src/bracketed-paste.ts +9 -2
  106. package/src/charts/chart-svg.d.ts +7 -13
  107. package/src/charts/chart-svg.ts +19 -22
  108. package/src/charts/table-data.ts +17 -6
  109. package/src/chat/advisor-message.d.ts +4 -1
  110. package/src/chat/advisor-message.ts +77 -24
  111. package/src/chat/assistant-message.d.ts +9 -4
  112. package/src/chat/assistant-message.ts +127 -31
  113. package/src/chat/bash-execution.ts +7 -2
  114. package/src/chat/figure-markdown.d.ts +7 -2
  115. package/src/chat/figure-markdown.ts +10 -3
  116. package/src/chat/image-loading.ts +9 -6
  117. package/src/chat/messages.d.ts +3 -1
  118. package/src/chat/messages.ts +4 -1
  119. package/src/chat/read-tool-group.d.ts +2 -2
  120. package/src/chat/read-tool-group.ts +97 -42
  121. package/src/chat/svg-figure.ts +3 -1
  122. package/src/chat/svg-source.ts +6 -3
  123. package/src/chat/table-chart.d.ts +11 -15
  124. package/src/chat/table-chart.ts +24 -57
  125. package/src/chat/tool-execution.d.ts +32 -20
  126. package/src/chat/tool-execution.ts +103 -36
  127. package/src/chat/user-message.d.ts +3 -2
  128. package/src/chat/user-message.ts +54 -34
  129. package/src/chrome/countdown-timer.ts +3 -3
  130. package/src/chrome/diff.d.ts +2 -3
  131. package/src/chrome/diff.ts +4 -9
  132. package/src/chrome/status-notice.ts +5 -0
  133. package/src/chrome/transcript-container.d.ts +2 -0
  134. package/src/chrome/transcript-container.ts +212 -28
  135. package/src/components/editor.ts +12 -12
  136. package/src/components/image.d.ts +2 -0
  137. package/src/components/image.ts +54 -20
  138. package/src/components/markdown.d.ts +49 -3
  139. package/src/components/markdown.ts +482 -12
  140. package/src/components/settings-list.ts +2 -4
  141. package/src/components/text.d.ts +2 -0
  142. package/src/components/text.ts +11 -1
  143. package/src/debug-server.d.ts +2 -0
  144. package/src/debug-server.ts +51 -69
  145. package/src/hotkeys-markdown.ts +2 -0
  146. package/src/keybinding-matchers.d.ts +6 -0
  147. package/src/keybinding-matchers.ts +19 -0
  148. package/src/multiplexers/tmux.d.ts +1 -0
  149. package/src/multiplexers/tmux.ts +3 -0
  150. package/src/multiplexers/types.d.ts +6 -0
  151. package/src/multiplexers/types.ts +6 -0
  152. package/src/native/backend.d.ts +5 -4
  153. package/src/native/backend.ts +13 -5
  154. package/src/native/blobs.d.ts +24 -12
  155. package/src/native/blobs.ts +56 -7
  156. package/src/native/reconcile.ts +43 -21
  157. package/src/overlays/advisor-config.ts +259 -8
  158. package/src/overlays/agent-hub-projection.d.ts +10 -5
  159. package/src/overlays/agent-hub-projection.ts +63 -12
  160. package/src/overlays/annotation-overlay.ts +422 -168
  161. package/src/overlays/approval-dialog.ts +389 -0
  162. package/src/overlays/ask-dialog.ts +244 -105
  163. package/src/overlays/btw-history-panel.ts +208 -147
  164. package/src/overlays/extensions/extension-list.ts +4 -6
  165. package/src/overlays/hook-editor.ts +90 -32
  166. package/src/overlays/hook-input.ts +105 -19
  167. package/src/overlays/job-tree.ts +177 -0
  168. package/src/overlays/jobs-panel.ts +222 -142
  169. package/src/overlays/login-dialog.ts +201 -91
  170. package/src/overlays/mcp-add-wizard.ts +562 -279
  171. package/src/overlays/model-browser.ts +32 -7
  172. package/src/overlays/model-hub.ts +4 -10
  173. package/src/overlays/model-picker.ts +58 -21
  174. package/src/overlays/pause-screen.ts +81 -23
  175. package/src/overlays/plan-review-overlay.ts +16 -0
  176. package/src/overlays/plugin-selector.ts +17 -1
  177. package/src/overlays/queue-mode-selector.ts +10 -0
  178. package/src/overlays/report-panel.ts +6 -2
  179. package/src/overlays/reset-usage-selector.ts +130 -21
  180. package/src/overlays/session-account-selector.ts +36 -0
  181. package/src/overlays/session-info-overlay.ts +199 -76
  182. package/src/overlays/session-selector.ts +15 -10
  183. package/src/overlays/settings-selector.ts +3 -0
  184. package/src/overlays/show-images-selector.ts +1 -0
  185. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  186. package/src/overlays/theme-selector.ts +6 -1
  187. package/src/overlays/thinking-selector.ts +8 -4
  188. package/src/overlays/tree-selector.ts +117 -2
  189. package/src/overlays/usage-dashboard.ts +498 -144
  190. package/src/prompt/composer-cache.ts +4 -1
  191. package/src/prompt/composer.ts +200 -74
  192. package/src/prompt/custom-editor.ts +168 -65
  193. package/src/prompt/image-references.d.ts +22 -0
  194. package/src/prompt/image-references.ts +79 -2
  195. package/src/prompt/internal-url-autocomplete.ts +1 -0
  196. package/src/prompt/macos-spelling.ts +28 -5
  197. package/src/prompt/prose-gate.ts +3 -0
  198. package/src/prompt/queued-messages.ts +9 -2
  199. package/src/prompt/tips.txt +1 -2
  200. package/src/prompt/typing-word.ts +59 -0
  201. package/src/prompt/welcome.ts +97 -45
  202. package/src/render/code-cell.ts +18 -5
  203. package/src/render/hanging-wrap.d.ts +7 -0
  204. package/src/render/hanging-wrap.ts +58 -0
  205. package/src/render/hyperlink.d.ts +2 -0
  206. package/src/render/hyperlink.ts +5 -0
  207. package/src/render/render-utils.d.ts +9 -1
  208. package/src/render/render-utils.ts +16 -2
  209. package/src/render/utils.d.ts +1 -1
  210. package/src/render/utils.ts +1 -1
  211. package/src/render/width-aware-text.ts +6 -5
  212. package/src/setup/scenes/splash.ts +22 -8
  213. package/src/status-line/component.ts +79 -11
  214. package/src/status-line/context-usage.ts +176 -73
  215. package/src/status-line/footer.ts +18 -35
  216. package/src/status-line/host.ts +6 -1
  217. package/src/status-line/segments.ts +16 -5
  218. package/src/status-line/startup.ts +6 -6
  219. package/src/status-line/types.ts +1 -0
  220. package/src/stdin-buffer.d.ts +7 -0
  221. package/src/stdin-buffer.ts +300 -62
  222. package/src/terminal-capabilities.d.ts +16 -1
  223. package/src/terminal-capabilities.ts +66 -28
  224. package/src/terminal-multiplexer.d.ts +7 -0
  225. package/src/terminal-multiplexer.ts +14 -0
  226. package/src/terminal.d.ts +8 -4
  227. package/src/terminal.ts +190 -63
  228. package/src/theme/symbols.d.ts +1 -1
  229. package/src/theme/symbols.ts +1 -0
  230. package/src/theme/theme-class.ts +1 -0
  231. package/src/theme/theme.d.ts +11 -0
  232. package/src/theme/theme.ts +46 -14
  233. package/src/thinking.d.ts +5 -0
  234. package/src/thinking.ts +9 -1
  235. package/src/tmux.d.ts +39 -4
  236. package/src/tmux.ts +110 -16
  237. package/src/tools/ask.ts +20 -9
  238. package/src/tools/ast-edit.d.ts +2 -2
  239. package/src/tools/ast-edit.ts +30 -7
  240. package/src/tools/ast-grep.d.ts +2 -2
  241. package/src/tools/ast-grep.ts +39 -35
  242. package/src/tools/bash.d.ts +6 -1
  243. package/src/tools/bash.ts +15 -9
  244. package/src/tools/debug.ts +2 -1
  245. package/src/tools/default-renderer.ts +3 -2
  246. package/src/tools/edit.d.ts +2 -0
  247. package/src/tools/edit.ts +100 -21
  248. package/src/tools/eval.d.ts +53 -4
  249. package/src/tools/eval.ts +173 -137
  250. package/src/tools/find.d.ts +14 -4
  251. package/src/tools/find.ts +71 -18
  252. package/src/tools/glob.d.ts +3 -3
  253. package/src/tools/glob.ts +51 -17
  254. package/src/tools/grep.d.ts +2 -2
  255. package/src/tools/grep.ts +99 -22
  256. package/src/tools/grouped-file-output.d.ts +9 -4
  257. package/src/tools/grouped-file-output.ts +21 -6
  258. package/src/tools/lsp.ts +30 -18
  259. package/src/tools/mcp.ts +2 -2
  260. package/src/tools/native-view.d.ts +1 -1
  261. package/src/tools/native-view.ts +2 -2
  262. package/src/tools/read.d.ts +32 -10
  263. package/src/tools/read.ts +89 -43
  264. package/src/tools/renderer.d.ts +2 -0
  265. package/src/tools/renderer.ts +2 -0
  266. package/src/tools/task.d.ts +31 -0
  267. package/src/tools/task.ts +40 -7
  268. package/src/tools/todo.ts +3 -2
  269. package/src/tools/wait.ts +16 -30
  270. package/src/tools/web-search-types.d.ts +1 -0
  271. package/src/tools/web-search-types.ts +1 -0
  272. package/src/tools/web-search.ts +2 -1
  273. package/src/tools/write.d.ts +2 -0
  274. package/src/tools/write.ts +36 -12
  275. package/src/tools/yield.ts +2 -2
  276. package/src/tui.d.ts +20 -3
  277. package/src/tui.ts +160 -43
  278. package/src/utils.ts +5 -3
  279. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  280. package/src/tools/eval-format/code-mode-declarations.ts +0 -61
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Each row is one JSON payload keyed by project (the resolved cwd) and kind.
6
6
  * Settings-derived kinds (theme/composer preferences, status-bar inputs) are
7
- * also written under the empty project, so a folder that never ran zeta-c still
7
+ * also written under the empty project, so a folder that never ran zetacode still
8
8
  * paints with the user's theme and status bar: those are rarely
9
9
  * project-specific, and path/branch render live.
10
10
  *
@@ -94,6 +94,7 @@ function parseUiState(
94
94
  showHardwareCursor,
95
95
  maxInlineImages,
96
96
  resizeScrollback,
97
+ completeScrollback,
97
98
  imeSafeCursor,
98
99
  autocompleteMaxVisible,
99
100
  spellingTypoDetection,
@@ -109,6 +110,7 @@ function parseUiState(
109
110
  resizeScrollback !== "append" &&
110
111
  resizeScrollback !== "rebuild" &&
111
112
  resizeScrollback !== "preserve") ||
113
+ (completeScrollback !== undefined && typeof completeScrollback !== "boolean") ||
112
114
  typeof imeSafeCursor !== "boolean" ||
113
115
  typeof autocompleteMaxVisible !== "number" ||
114
116
  typeof spellingTypoDetection !== "boolean" ||
@@ -136,6 +138,7 @@ function parseUiState(
136
138
  showHardwareCursor,
137
139
  maxInlineImages,
138
140
  resizeScrollback: resizeScrollback ?? "rebuild",
141
+ completeScrollback: completeScrollback ?? false,
139
142
  imeSafeCursor,
140
143
  autocompleteMaxVisible,
141
144
  spellingTypoDetection,
@@ -8,6 +8,7 @@ import { ProcessTerminal, type Terminal } from "../terminal";
8
8
  import {
9
9
  type Component,
10
10
  Container,
11
+ type RenderTimer,
11
12
  type ResizeScrollbackMode,
12
13
  type TerminalFramePlan,
13
14
  type TerminalFrameProvider,
@@ -26,6 +27,8 @@ import { WelcomeComponent } from "./welcome";
26
27
  import { ensureThemeSync, getEditorTheme, theme } from "../theme/theme";
27
28
 
28
29
  const DOUBLE_INTERRUPT_MS = 500;
30
+ // How long idle chrome must hold its height after a turn before it is treated as permanent.
31
+ const IDLE_CHROME_SETTLE_MS = 250;
29
32
 
30
33
  /** Live settings that affect the composer before and after session adoption. */
31
34
  export interface ComposerPreferences {
@@ -34,6 +37,8 @@ export interface ComposerPreferences {
34
37
  readonly showHardwareCursor: boolean;
35
38
  readonly maxInlineImages: number;
36
39
  readonly resizeScrollback: ResizeScrollbackMode;
40
+ /** Send rows a growing HUD or dialog pushes off screen straight to scrollback instead of holding them back until it shrinks. */
41
+ readonly completeScrollback: boolean;
37
42
  readonly imeSafeCursor: boolean;
38
43
  readonly autocompleteMaxVisible: number;
39
44
  readonly spellingTypoDetection: boolean;
@@ -48,6 +53,7 @@ export const COMPOSER_DEFAULTS: ComposerPreferences = {
48
53
  showHardwareCursor: true,
49
54
  maxInlineImages: 8,
50
55
  resizeScrollback: "rebuild",
56
+ completeScrollback: false,
51
57
  imeSafeCursor: false,
52
58
  autocompleteMaxVisible: 10,
53
59
  spellingTypoDetection: true,
@@ -86,18 +92,8 @@ export interface ComposerOptions {
86
92
  readonly now?: () => number;
87
93
  }
88
94
 
89
- /** How {@link Composer.setRuntimeChildren} bills below-transcript roots against transcript retirement. */
95
+ /** How {@link Composer.setRuntimeChildren} lays out the roots below the transcript. */
90
96
  export interface RuntimeChildrenOptions {
91
- /**
92
- * Below-transcript roots whose growth is momentary (inline dialogs, a tall
93
- * multi-line editor). They are billed at their smallest height so expansion
94
- * clips the live tail instead of retiring rows a later shrink could not
95
- * reclaim (#11007). Every other root is billed at its current height, so
96
- * settled rows it displaces retire to native scrollback. Decision panels
97
- * declaring `retireDisplacedTranscript` opt out of this floor even when
98
- * mounted as a child of a transient editor container.
99
- */
100
- readonly transient?: readonly Component[];
101
97
  /**
102
98
  * Dock order on a TSP terminal (no status strip; the composer carries its
103
99
  * facts): the native dock stacks HUD pills, the working row and queued messages over
@@ -105,6 +101,20 @@ export interface RuntimeChildrenOptions {
105
101
  * to the roots below the transcript in render order.
106
102
  */
107
103
  readonly nativeDock?: readonly Component[];
104
+ /**
105
+ * Below-transcript roots the user opens and closes (the editor surface that
106
+ * drafts and dialogs swap into, docked reports). Their rows never become
107
+ * part of the post-run chrome floor, so collapsing them refills the screen.
108
+ */
109
+ readonly transientChrome?: readonly Component[];
110
+ /**
111
+ * Transient roots that clear themselves (todo and subagent HUDs, progress).
112
+ * Their first paint can be their tallest, e.g. a restored todo list on a
113
+ * resumed session, so unlike {@link transientChrome} it is never treated as
114
+ * their resting height: all of their rows stay out of the floor. Roots listed
115
+ * here are transient and need not also appear in `transientChrome`.
116
+ */
117
+ readonly selfClearingChrome?: readonly Component[];
108
118
  }
109
119
 
110
120
  /** Controls the first terminal paint for a composer that does not already own the terminal. */
@@ -257,19 +267,44 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
257
267
  #retiredHeaderStart = 0;
258
268
  #resizeRetiredHeaderStart: number | undefined;
259
269
  #lastNormalRows = 0;
260
- // Roots from `RuntimeChildrenOptions.transient`, and the smallest height
261
- // they have rendered at since mount. Retirement bills transient roots at
262
- // this floor, never their peak, so a transient dialog or tall editor that later
263
- // shrinks never leaves committed transcript rows the live viewport cannot
264
- // reclaim (#11007). Persistent roots (loader, todo/subagent HUDs) bill at
265
- // their current height: they stay up for a whole turn, and billing them at
266
- // an idle floor hid the settled rows they displaced until the turn ended.
267
- // The floor is terminal-height independent — editor and dialog heights do
268
- // not scale with rows — so it is retained across resizes rather than
269
- // rediscovered from whatever chrome is expanded when the height changes.
270
+ // Retirement bills below-transcript chrome at this floor, not its current
271
+ // height. It follows contractions immediately and promotes stable eligible
272
+ // growth only while idle. Transient roots count at their minimum painted
273
+ // height: a growing dialog or draft clips the live tail without committing
274
+ // displaced rows, so those rows refill the screen when it shrinks (#11007).
275
+ // Displaced rows reach history when newer output pushes them past the floor
276
+ // or persistent idle chrome settles and raises it.
277
+ // With `completeScrollback`, rows the chrome pushes off screen go straight
278
+ // to scrollback, and when it shrinks the freed rows stay blank.
279
+ // The floor is terminal-height independent — chrome heights do not scale
280
+ // with rows — so it is retained across resizes rather than rediscovered
281
+ // from whatever chrome is expanded when the height changes.
282
+ #chromeFloor: number | undefined;
283
+ // Settled idle chrome has retired rows; contraction must preserve native history.
284
+ #chromeFloorPromoted = false;
285
+ #runActive = false;
270
286
  #transientChrome: ReadonlySet<Component> = new Set();
271
- #transientChromeFloor: number | undefined;
272
- #anchorAfterInlineRetirement = false;
287
+ #selfClearingChrome: ReadonlySet<Component> = new Set();
288
+ // Fewest rows the user-driven transient roots have painted since mount; only rows above it are excluded from the floor.
289
+ #transientFloor: number | undefined;
290
+ // Editor and shape the transient floor was measured under, plus the last frame's transient rows.
291
+ // A shape swap (band → pi) changes the editor's resting height; a minimum carried over from the
292
+ // shorter shape would exclude the taller shape's fixed rows forever and clip that many transcript rows.
293
+ #transientEditor: Component | undefined;
294
+ #transientEditorStyle: string | undefined;
295
+ #lastTransientRows = 0;
296
+ // Fewest rows the bare editor painted before the runtime mounted, and the shape it was painted in.
297
+ // A draft replayed from the startup keystroke buffer can already be present on the first runtime
298
+ // frame, so that frame is not the editor's resting height; this is, because the bootstrap frame
299
+ // paints before replay. Only valid while the shape is unchanged.
300
+ #bootstrapEditorRows: number | undefined;
301
+ #bootstrapEditorStyle: string | undefined;
302
+ // Idle chrome height awaiting the settle window, and whether it has elapsed.
303
+ // Turn-scoped HUDs can close a few frames after the last block finalizes;
304
+ // billing them first would commit rows they cover and strand a band.
305
+ #idleChrome: number | undefined;
306
+ #idleChromeSettled = false;
307
+ #idleChromeTimer: RenderTimer | undefined;
273
308
  /** Rows the chrome below each below-transcript root took in the last frame (see {@link rowsBelow}). */
274
309
  #rowsBelow = new Map<Component, number>();
275
310
  #lastInterruptAt = 0;
@@ -339,6 +374,9 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
339
374
  this.ui.addChild(this.editor);
340
375
  this.ui.addChild(this.#statusHost);
341
376
  this.ui.setFocus(this.editor);
377
+ // A settle timer that fires while the TUI is stopped (external editor, suspend) is dropped;
378
+ // forget it on restart so the next frame re-arms instead of waiting on it forever.
379
+ this.ui.addStartListener(() => this.#resetIdleChrome());
342
380
  }
343
381
  /**
344
382
  * Rows the below-transcript chrome under `root` (editor, status line, …)
@@ -349,18 +387,6 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
349
387
  return this.#rowsBelow.get(root);
350
388
  }
351
389
 
352
- /**
353
- * Keep the input on the bottom row while the live rows cannot fill the
354
- * screen, as after an inline decision panel closes. A tall block that just
355
- * left the chrome above the editor (a command report) may have scrolled
356
- * rows into native history that cannot be pulled back; without the pin the
357
- * editor would jump up to where the shorter frame now ends. The pin lifts
358
- * once live rows fill the screen again.
359
- */
360
- pinInputToBottom(): void {
361
- this.#anchorAfterInlineRetirement = true;
362
- }
363
-
364
390
  /** Compose the bounded mutable viewport and the next ordered history append. */
365
391
  renderFrame(viewport: ViewportSize): TerminalFramePlan {
366
392
  if (!this.#started || this.#stopped) return { viewport: [] };
@@ -377,62 +403,57 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
377
403
  const transcriptIndex = roots.findIndex(root => root instanceof TranscriptContainer);
378
404
  if (transcriptIndex < 0) {
379
405
  this.#lastClickSpans = [];
380
- return { viewport: this.#renderRoots(roots, width).slice(-rows) };
406
+ const viewport: string[] = [];
407
+ for (const root of roots) {
408
+ const rendered = root.render(width);
409
+ if (!this.#runtimeMounted && root === this.editor) this.#recordBootstrapEditorRows(rendered.length);
410
+ viewport.push(...rendered);
411
+ }
412
+ return { viewport: viewport.slice(-rows) };
381
413
  }
382
414
  const transcript = roots[transcriptIndex] as TranscriptContainer;
383
415
  const preRoots = this.#renderRoots(roots.slice(0, transcriptIndex), width);
384
416
  const afterRoots = roots.slice(transcriptIndex + 1);
385
417
  const after: string[] = [];
386
418
  const afterSpans: ViewportClickSpan[] = [];
387
- let transientRows = 0;
388
- let displacingRows = 0;
389
- let decisionPanelOpen = false;
390
419
  const ends: { root: Component; end: number }[] = [];
420
+ let transientRows = 0;
421
+ let selfClearingRows = 0;
391
422
  for (const root of afterRoots) {
392
- const chrome: Component = root;
393
423
  const start = after.length;
394
424
  this.#renderBelowRoot(root, width, after, afterSpans);
425
+ if (this.#selfClearingChrome.has(root)) selfClearingRows += after.length - start;
426
+ else if (this.#transientChrome.has(root)) transientRows += after.length - start;
395
427
  ends.push({ root, end: after.length });
396
- if (this.#transientChrome.has(root)) transientRows += after.length - start;
397
- if (
398
- chrome.retireDisplacedTranscript ||
399
- (root instanceof Container && root.children.some(child => child.retireDisplacedTranscript))
400
- ) {
401
- if (this.#transientChrome.has(root)) displacingRows += after.length - start;
402
- decisionPanelOpen = true;
403
- }
404
428
  }
405
429
  this.#rowsBelow = new Map(ends.map(({ root, end }) => [root, after.length - end]));
430
+ const transientFloor = this.#syncTransientFloor(transientRows);
431
+ // Eligible chrome excludes user-driven transient rows above their resting height and every
432
+ // self-clearing row; contraction, floor and rebaseline all use it.
433
+ const eligibleChrome = after.length - (transientRows - transientFloor) - selfClearingRows;
434
+ const promotedChromeContracted = this.#chromeFloorPromoted && eligibleChrome < (this.#chromeFloor ?? 0);
435
+ this.#chromeFloor = Math.min(this.#chromeFloor ?? eligibleChrome, eligibleChrome);
436
+ this.#rebaselineChromeFloor(this.#runActive || transcript.hasActiveBlock(), eligibleChrome);
437
+ const belowFloor = this.#preferences.completeScrollback ? after.length : this.#chromeFloor;
406
438
  // Offer history under capacity pressure only: blocks stay live (and keep
407
439
  // reflowing to the current width) while the screen has room. A batch
408
440
  // leaves the mutable viewport in the same frame it is appended, so its
409
441
  // rows are never painted twice.
410
- //
411
- // Retirement bills ordinary transient roots at their floor, not their
412
- // peak: a confirmation dialog or a tall multi-line editor clips the live tail for
413
- // its lifetime, but must not permanently commit transcript rows to native
414
- // history — otherwise a later shrink cannot refill the freed rows and the
415
- // editor drifts up above a band of blank rows (#11007).
416
- this.#transientChromeFloor = Math.min(this.#transientChromeFloor ?? transientRows, transientRows);
417
- // An ask panel remains open while the user reads the preceding response.
418
- // Its displaced settled rows must be reachable in scrollback now, rather
419
- // than clipped until the panel closes. Ordinary drafts retain their floor.
420
- const belowFloor = after.length - transientRows + Math.max(this.#transientChromeFloor, displacingRows);
421
442
  const now = performance.now();
422
443
  const frame: AnimationFrame = { now, tick: Math.floor(now / 80) };
423
444
  // Retirement measures the same live blocks the viewport lays out below;
424
445
  // one open frame renders each of them once for both.
425
446
  transcript.beginFrame(frame);
426
447
  const history = this.#offerHistory(transcript, width, rows, preRoots.length + belowFloor);
427
- if (decisionPanelOpen && history !== undefined) this.#anchorAfterInlineRetirement = true;
428
448
  const headerVisible = !this.#headerRetired && this.#offeredHistory?.source !== "header";
429
449
  const headerRows = headerVisible ? this.#header.render(width) : [];
430
450
  const before = [...headerRows, ...preRoots];
431
451
  // The live tail is laid out against the same baseline retirement is
432
452
  // billed against, so its compaction allocator (one row per block, no
433
453
  // inter-block blanks) engages only when a block genuinely cannot retire.
434
- // Rows the transient chrome peak displaces are clipped from the top by
435
- // the `drop` slice below, which is what scrollback would have done.
454
+ // Rows the chrome above its floor displaces are clipped from the top by
455
+ // the `drop` slice below; they stay live until the chrome shrinks. With
456
+ // `completeScrollback` none are held back.
436
457
  const active = transcript.renderViewport(width, Math.max(0, rows - before.length - belowFloor), frame);
437
458
  const activeSpans: ViewportClickSpan[] = [];
438
459
  for (const span of transcript.getLastViewportSpans()) {
@@ -442,13 +463,39 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
442
463
  }
443
464
  const drop = Math.max(0, before.length + active.length + after.length - rows);
444
465
  const mutable = [...before, ...active, ...after].slice(drop);
445
- // Once live rows fill the screen again, the retired gap is gone.
446
- if (!decisionPanelOpen && mutable.length >= rows) this.#anchorAfterInlineRetirement = false;
447
- // Rows retired during a decision panel cannot be pulled back from native
448
- // history when it closes. Keep the input pinned to the bottom without
449
- // replaying those rows (which would duplicate them) or clearing history.
450
- const topPadding = this.#anchorAfterInlineRetirement ? Math.max(0, rows - mutable.length) : 0;
451
- if (topPadding > 0) mutable.unshift(...Array<string>(topPadding).fill(""));
466
+ // Once a paint reaches the bottom row over committed history, rows above
467
+ // it may already be in native scrollback, which no later frame can pull
468
+ // back down. Keep the input on the bottom row whenever the frame shrinks
469
+ // — a closing dialog, report or HUD, or a live block collapsing. The
470
+ // slack that leaves has to sit somewhere: rows above the live area hold
471
+ // committed history still on screen, so the live rows cannot move up into
472
+ // them, and replaying those rows would duplicate them while clearing
473
+ // history would lose them. Replays position themselves. Without native
474
+ // history the live viewport is the whole document and shrinks in place.
475
+ const filledHistoryRows =
476
+ history?.kind === "replay" || !(history !== undefined || this.ui.hasProviderHistory())
477
+ ? undefined
478
+ : this.ui.getFilledHistoryRows();
479
+ // Once settled chrome has retired rows, shrinking it cannot pull those
480
+ // rows back out of native scrollback. Let the editor move up instead of
481
+ // inserting a seam band; subsequent frames retain that natural anchor
482
+ // until new output fills the screen again. Shell history stays untouched.
483
+ const slack =
484
+ filledHistoryRows === undefined || promotedChromeContracted
485
+ ? 0
486
+ : Math.max(0, rows - Math.min(rows, filledHistoryRows + (history?.rows.length ?? 0)) - mutable.length);
487
+ // Park the slack below the live rows rather than above them. Above, it
488
+ // splits the on-screen history from the live tail and reads as a hole
489
+ // punched into whatever was streaming — a table's rows included — and
490
+ // each slack row is written with no cells, so no repaint, mark or
491
+ // selection can cover it until something scrolls it away. Below, the
492
+ // transcript stays contiguous with the history, the slack reads as the
493
+ // empty tail of a short document, and growth consumes it before any row
494
+ // can scroll.
495
+ if (slack > 0) {
496
+ const at = Math.max(0, Math.min(mutable.length, before.length + active.length - drop));
497
+ mutable.splice(at, 0, ...Array<string>(slack).fill(""));
498
+ }
452
499
  const viewportLength = mutable.length;
453
500
  const spans: ViewportClickSpan[] = [];
454
501
  const shift = (span: ViewportClickSpan, base: number): void => {
@@ -462,8 +509,8 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
462
509
  spans.push({ start: clamped, end, candidates: (local: number) => span.candidates(local + skew) });
463
510
  }
464
511
  };
465
- for (const span of activeSpans) shift(span, topPadding + before.length - drop);
466
- for (const span of afterSpans) shift(span, topPadding + before.length + active.length - drop);
512
+ for (const span of activeSpans) shift(span, before.length - drop);
513
+ for (const span of afterSpans) shift(span, slack + before.length + active.length - drop);
467
514
  this.#lastClickSpans = spans;
468
515
  if (history !== undefined && this.#offeredHistory?.source === "header") {
469
516
  const visibleHeaderRows = Math.max(0, rows - (mutable.length + drop));
@@ -672,6 +719,71 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
672
719
  }
673
720
  }
674
721
 
722
+ /** Raise the floor to eligible idle chrome that survives the settle window, even if it appeared after the run. */
723
+ #rebaselineChromeFloor(busy: boolean, chrome: number): void {
724
+ if (busy || chrome <= (this.#chromeFloor ?? chrome)) {
725
+ this.#resetIdleChrome();
726
+ return;
727
+ }
728
+ if (this.#idleChromeSettled && this.#idleChrome === chrome) {
729
+ this.#chromeFloorPromoted = true;
730
+ this.#chromeFloor = chrome;
731
+ this.#resetIdleChrome();
732
+ return;
733
+ }
734
+ if (this.#idleChrome === chrome && this.#idleChromeTimer !== undefined) return;
735
+ this.#resetIdleChrome();
736
+ this.#idleChrome = chrome;
737
+ this.#idleChromeTimer = this.ui.scheduleAfter(IDLE_CHROME_SETTLE_MS, () => {
738
+ this.#idleChromeTimer = undefined;
739
+ this.#idleChromeSettled = true;
740
+ this.ui.requestRender();
741
+ });
742
+ }
743
+
744
+ #resetIdleChrome(): void {
745
+ this.#idleChromeTimer?.cancel();
746
+ this.#idleChromeTimer = undefined;
747
+ this.#idleChrome = undefined;
748
+ this.#idleChromeSettled = false;
749
+ }
750
+
751
+ /** Track the empty editor's pre-mount height per shape; a shape change restarts the measurement. */
752
+ #recordBootstrapEditorRows(rows: number): void {
753
+ const style = this.editor.getBorderStyle();
754
+ this.#bootstrapEditorRows =
755
+ style === this.#bootstrapEditorStyle ? Math.min(this.#bootstrapEditorRows ?? rows, rows) : rows;
756
+ this.#bootstrapEditorStyle = style;
757
+ }
758
+
759
+ /**
760
+ * Follow the transient roots' resting height. Normally a running minimum; when the editor or its
761
+ * shape changes, the floor shifts by that frame's height change instead, so draft growth stays
762
+ * excluded while the new shape's fixed rows are billed.
763
+ */
764
+ #syncTransientFloor(transientRows: number): number {
765
+ const editor = this.editor;
766
+ const style = editor.getBorderStyle();
767
+ let floor: number;
768
+ if (this.#transientFloor === undefined) {
769
+ // The first runtime frame may already carry a replayed startup draft; seed from the empty
770
+ // editor's pre-mount height when it was painted in the same shape.
771
+ const bootstrap = this.#bootstrapEditorStyle === style ? this.#bootstrapEditorRows : undefined;
772
+ floor = bootstrap === undefined ? transientRows : Math.min(transientRows, bootstrap);
773
+ } else if (editor !== this.#transientEditor || style !== this.#transientEditorStyle) {
774
+ floor = Math.max(0, Math.min(transientRows, this.#transientFloor + transientRows - this.#lastTransientRows));
775
+ } else {
776
+ floor = Math.min(this.#transientFloor, transientRows);
777
+ }
778
+ this.#transientFloor = floor;
779
+ this.#bootstrapEditorRows = undefined;
780
+ this.#bootstrapEditorStyle = undefined;
781
+ this.#transientEditor = editor;
782
+ this.#transientEditorStyle = style;
783
+ this.#lastTransientRows = transientRows;
784
+ return floor;
785
+ }
786
+
675
787
  /** Header retires first; replay coalesces it with the complete transcript ledger. */
676
788
  #offerHistory(
677
789
  transcript: TranscriptContainer,
@@ -888,6 +1000,17 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
888
1000
  this.ui.requestRender();
889
1001
  }
890
1002
 
1003
+ /**
1004
+ * Mark whether a host agent run is in flight. Mid-run gaps with no streaming
1005
+ * block (tool arguments not yet parseable, between tool calls) then do not
1006
+ * count as the run ending when the post-run chrome floor is decided.
1007
+ */
1008
+ setRunActive(active: boolean): void {
1009
+ if (this.#runActive === active) return;
1010
+ this.#runActive = active;
1011
+ this.ui.requestRender();
1012
+ }
1013
+
891
1014
  /** Replace optional header content around the stable welcome scene. */
892
1015
  setHeaderExtras(before: readonly Component[], after: readonly Component[]): void {
893
1016
  if (this.#stopped) return;
@@ -924,10 +1047,13 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
924
1047
  /** Mount or replace session-aware root children while preserving the header and status hosts. */
925
1048
  setRuntimeChildren(children: readonly Component[], options: RuntimeChildrenOptions = {}): void {
926
1049
  if (this.#stopped) return;
927
- this.#transientChrome = new Set(options.transient);
928
- this.#transientChromeFloor = undefined;
929
- this.#anchorAfterInlineRetirement = false;
1050
+ this.#chromeFloor = undefined;
1051
+ this.#chromeFloorPromoted = false;
1052
+ this.#resetIdleChrome();
930
1053
  this.#nativeDock = options.nativeDock;
1054
+ this.#transientChrome = new Set(options.transientChrome);
1055
+ this.#selfClearingChrome = new Set(options.selfClearingChrome);
1056
+ this.#transientFloor = undefined;
931
1057
  this.ui.removeChild(this.#statusHost);
932
1058
  if (this.#runtimeMounted) {
933
1059
  for (const child of this.#runtimeChildren) this.ui.removeChild(child);