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