@linxiraos/pi-tui 1.2.0 → 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 (276) hide show
  1. package/CHANGELOG.md +188 -120
  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 +22 -26
  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 +35 -43
  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/agent-hub.ts +2 -2
  157. package/src/overlays/annotation-overlay.ts +422 -168
  158. package/src/overlays/approval-dialog.ts +389 -0
  159. package/src/overlays/ask-dialog.ts +244 -105
  160. package/src/overlays/btw-history-panel.ts +208 -147
  161. package/src/overlays/extensions/extension-list.ts +4 -6
  162. package/src/overlays/hook-editor.ts +90 -32
  163. package/src/overlays/hook-input.ts +105 -19
  164. package/src/overlays/job-tree.ts +177 -0
  165. package/src/overlays/jobs-panel.ts +222 -142
  166. package/src/overlays/login-dialog.ts +201 -91
  167. package/src/overlays/mcp-add-wizard.ts +562 -279
  168. package/src/overlays/model-browser.ts +32 -7
  169. package/src/overlays/model-hub.ts +4 -10
  170. package/src/overlays/model-picker.ts +58 -21
  171. package/src/overlays/pause-screen.ts +81 -23
  172. package/src/overlays/plan-review-overlay.ts +16 -0
  173. package/src/overlays/plugin-selector.ts +17 -1
  174. package/src/overlays/plugin-settings.ts +8 -2
  175. package/src/overlays/queue-mode-selector.ts +10 -0
  176. package/src/overlays/report-panel.ts +6 -2
  177. package/src/overlays/reset-usage-selector.ts +130 -21
  178. package/src/overlays/session-account-selector.ts +36 -0
  179. package/src/overlays/session-info-overlay.ts +199 -76
  180. package/src/overlays/session-selector.ts +15 -10
  181. package/src/overlays/settings-selector.ts +3 -0
  182. package/src/overlays/show-images-selector.ts +1 -0
  183. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  184. package/src/overlays/theme-selector.ts +6 -1
  185. package/src/overlays/thinking-selector.ts +8 -4
  186. package/src/overlays/tree-selector.ts +117 -2
  187. package/src/overlays/usage-dashboard.ts +498 -144
  188. package/src/prompt/composer-cache.ts +4 -6
  189. package/src/prompt/composer.ts +196 -96
  190. package/src/prompt/custom-editor.ts +168 -65
  191. package/src/prompt/image-references.d.ts +22 -0
  192. package/src/prompt/image-references.ts +79 -2
  193. package/src/prompt/internal-url-autocomplete.ts +1 -0
  194. package/src/prompt/macos-spelling.ts +28 -5
  195. package/src/prompt/prose-gate.ts +3 -0
  196. package/src/prompt/queued-messages.ts +9 -2
  197. package/src/prompt/tips.txt +1 -2
  198. package/src/prompt/typing-word.ts +59 -0
  199. package/src/prompt/welcome.ts +256 -423
  200. package/src/render/code-cell.ts +18 -5
  201. package/src/render/hanging-wrap.d.ts +7 -0
  202. package/src/render/hanging-wrap.ts +58 -0
  203. package/src/render/hyperlink.d.ts +2 -0
  204. package/src/render/hyperlink.ts +5 -0
  205. package/src/render/render-utils.d.ts +9 -1
  206. package/src/render/render-utils.ts +16 -2
  207. package/src/render/utils.d.ts +1 -1
  208. package/src/render/utils.ts +1 -1
  209. package/src/render/width-aware-text.ts +6 -5
  210. package/src/setup/scenes/splash.ts +137 -60
  211. package/src/status-line/component.ts +23 -10
  212. package/src/status-line/context-usage.ts +176 -73
  213. package/src/status-line/footer.ts +18 -35
  214. package/src/status-line/host.ts +6 -1
  215. package/src/status-line/segments.ts +16 -5
  216. package/src/status-line/startup.ts +6 -6
  217. package/src/status-line/types.ts +1 -0
  218. package/src/stdin-buffer.d.ts +7 -0
  219. package/src/stdin-buffer.ts +11 -1
  220. package/src/terminal-capabilities.d.ts +16 -1
  221. package/src/terminal-capabilities.ts +66 -28
  222. package/src/terminal-multiplexer.d.ts +7 -0
  223. package/src/terminal-multiplexer.ts +14 -0
  224. package/src/terminal.d.ts +8 -4
  225. package/src/terminal.ts +160 -46
  226. package/src/theme/symbols.d.ts +1 -1
  227. package/src/theme/symbols.ts +1 -0
  228. package/src/theme/theme-class.ts +1 -0
  229. package/src/theme/theme.d.ts +11 -0
  230. package/src/theme/theme.ts +46 -14
  231. package/src/thinking.d.ts +5 -0
  232. package/src/thinking.ts +9 -1
  233. package/src/tmux.d.ts +39 -4
  234. package/src/tmux.ts +110 -16
  235. package/src/tools/ask.ts +20 -9
  236. package/src/tools/ast-edit.d.ts +2 -2
  237. package/src/tools/ast-edit.ts +30 -7
  238. package/src/tools/ast-grep.d.ts +2 -2
  239. package/src/tools/ast-grep.ts +39 -35
  240. package/src/tools/bash.d.ts +6 -1
  241. package/src/tools/bash.ts +15 -9
  242. package/src/tools/debug.ts +2 -1
  243. package/src/tools/default-renderer.ts +3 -2
  244. package/src/tools/edit.d.ts +2 -0
  245. package/src/tools/edit.ts +100 -21
  246. package/src/tools/eval.d.ts +53 -4
  247. package/src/tools/eval.ts +173 -137
  248. package/src/tools/find.d.ts +14 -4
  249. package/src/tools/find.ts +71 -18
  250. package/src/tools/glob.d.ts +3 -3
  251. package/src/tools/glob.ts +51 -17
  252. package/src/tools/grep.d.ts +2 -2
  253. package/src/tools/grep.ts +99 -22
  254. package/src/tools/grouped-file-output.d.ts +9 -4
  255. package/src/tools/grouped-file-output.ts +21 -6
  256. package/src/tools/lsp.ts +30 -18
  257. package/src/tools/mcp.ts +2 -2
  258. package/src/tools/native-view.d.ts +1 -1
  259. package/src/tools/native-view.ts +2 -2
  260. package/src/tools/read.d.ts +32 -10
  261. package/src/tools/read.ts +89 -43
  262. package/src/tools/renderer.d.ts +2 -0
  263. package/src/tools/renderer.ts +2 -0
  264. package/src/tools/task.d.ts +31 -0
  265. package/src/tools/task.ts +40 -7
  266. package/src/tools/todo.ts +3 -2
  267. package/src/tools/wait.ts +16 -30
  268. package/src/tools/web-search.ts +2 -1
  269. package/src/tools/write.d.ts +2 -0
  270. package/src/tools/write.ts +36 -12
  271. package/src/tools/yield.ts +2 -2
  272. package/src/tui.d.ts +20 -3
  273. package/src/tui.ts +140 -36
  274. package/src/utils.ts +5 -3
  275. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  276. package/src/tools/eval-format/code-mode-declarations.ts +0 -61
@@ -3,11 +3,10 @@
3
3
  * (`~/.zeta/agent/cache/composer.db`).
4
4
  *
5
5
  * Each row is one JSON payload keyed by project (the resolved cwd) and kind.
6
- * Settings-derived kinds (theme/composer preferences, welcome model labels,
7
- * status-bar inputs) are also written under the empty project, so a folder that
8
- * never ran zeta-c still paints with the user's theme and status bar: those are
9
- * rarely project-specific, and path/branch render live. Recent sessions and LSP
10
- * rows stay per project.
6
+ * Settings-derived kinds (theme/composer preferences, status-bar inputs) are
7
+ * also written under the empty project, so a folder that never ran zetacode still
8
+ * paints with the user's theme and status bar: those are rarely
9
+ * project-specific, and path/branch render live.
11
10
  *
12
11
  * ```text
13
12
  * entries (project TEXT, kind TEXT, value TEXT JSON, PRIMARY KEY (project, kind))
@@ -25,7 +24,6 @@ import * as logger from "@linxiraos/pi-utils/logger";
25
24
  import * as postmortem from "@linxiraos/pi-utils/postmortem";
26
25
  import { openSqliteDatabaseSync } from "@linxiraos/pi-utils/sqlite";
27
26
  import { isRecord } from "@linxiraos/pi-utils/type-guards";
28
- import type { LspServerInfo, RecentSession } from "./welcome";
29
27
  import type { ComposerPreferences, ComposerStatusCache } from "./composer";
30
28
  import { readStatusLineStartupData } from "../status-line/startup";
31
29
  import type { SymbolPreset } from "../theme/theme";
@@ -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,
@@ -22,10 +23,12 @@ import { postmortem } from "@linxiraos/pi-utils";
22
23
  import { CustomEditor } from "./custom-editor";
23
24
  import type { WordCompletionMethod } from "./word-completion";
24
25
  import { type AnimationFrame, TranscriptContainer } from "../chrome/transcript-container";
25
- import { WelcomeComponent, type RecentSession, type LspServerInfo } from "./welcome";
26
+ 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 {
@@ -58,11 +61,6 @@ export const COMPOSER_DEFAULTS: ComposerPreferences = {
58
61
  /** Welcome data that can be supplied initially or patched as startup resolves it. */
59
62
  export interface ComposerWelcomeUpdate {
60
63
  readonly version?: string;
61
- readonly modelName?: string;
62
- readonly providerName?: string;
63
- readonly recentSessions?: readonly RecentSession[];
64
- /** Detected project servers; `null` means LSP is disabled and hides the welcome section. */
65
- readonly lspServers?: readonly LspServerInfo[] | null;
66
64
  }
67
65
 
68
66
  /**
@@ -91,18 +89,8 @@ export interface ComposerOptions {
91
89
  readonly now?: () => number;
92
90
  }
93
91
 
94
- /** How {@link Composer.setRuntimeChildren} bills below-transcript roots against transcript retirement. */
92
+ /** How {@link Composer.setRuntimeChildren} lays out the roots below the transcript. */
95
93
  export interface RuntimeChildrenOptions {
96
- /**
97
- * Below-transcript roots whose growth is momentary (inline dialogs, a tall
98
- * multi-line editor). They are billed at their smallest height so expansion
99
- * clips the live tail instead of retiring rows a later shrink could not
100
- * reclaim (#11007). Every other root is billed at its current height, so
101
- * settled rows it displaces retire to native scrollback. Decision panels
102
- * declaring `retireDisplacedTranscript` opt out of this floor even when
103
- * mounted as a child of a transient editor container.
104
- */
105
- readonly transient?: readonly Component[];
106
94
  /**
107
95
  * Dock order on a TSP terminal (no status strip; the composer carries its
108
96
  * facts): the native dock stacks HUD pills, the working row and queued messages over
@@ -110,6 +98,20 @@ export interface RuntimeChildrenOptions {
110
98
  * to the roots below the transcript in render order.
111
99
  */
112
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[];
113
115
  }
114
116
 
115
117
  /** Controls the first terminal paint for a composer that does not already own the terminal. */
@@ -215,10 +217,6 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
215
217
  #preferences: ComposerPreferences;
216
218
  #welcome: WelcomeComponent | undefined;
217
219
  #version = "";
218
- #modelName = "";
219
- #providerName = "";
220
- #recentSessions: RecentSession[] = [];
221
- #lspServers: LspServerInfo[] | null = [];
222
220
  #headerBefore: readonly Component[] = [];
223
221
  #headerAfter: readonly Component[] = [];
224
222
  #runtimeChildren: readonly Component[] = [];
@@ -266,19 +264,42 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
266
264
  #retiredHeaderStart = 0;
267
265
  #resizeRetiredHeaderStart: number | undefined;
268
266
  #lastNormalRows = 0;
269
- // Roots from `RuntimeChildrenOptions.transient`, and the smallest height
270
- // they have rendered at since mount. Retirement bills transient roots at
271
- // this floor, never their peak, so a transient dialog or tall editor that later
272
- // shrinks never leaves committed transcript rows the live viewport cannot
273
- // reclaim (#11007). Persistent roots (loader, todo/subagent HUDs) bill at
274
- // their current height: they stay up for a whole turn, and billing them at
275
- // an idle floor hid the settled rows they displaced until the turn ended.
276
- // The floor is terminal-height independent — editor and dialog heights do
277
- // not scale with rows — so it is retained across resizes rather than
278
- // 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;
279
281
  #transientChrome: ReadonlySet<Component> = new Set();
280
- #transientChromeFloor: number | undefined;
281
- #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;
282
303
  /** Rows the chrome below each below-transcript root took in the last frame (see {@link rowsBelow}). */
283
304
  #rowsBelow = new Map<Component, number>();
284
305
  #lastInterruptAt = 0;
@@ -348,6 +369,9 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
348
369
  this.ui.addChild(this.editor);
349
370
  this.ui.addChild(this.#statusHost);
350
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());
351
375
  }
352
376
  /**
353
377
  * Rows the below-transcript chrome under `root` (editor, status line, …)
@@ -358,18 +382,6 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
358
382
  return this.#rowsBelow.get(root);
359
383
  }
360
384
 
361
- /**
362
- * Keep the input on the bottom row while the live rows cannot fill the
363
- * screen, as after an inline decision panel closes. A tall block that just
364
- * left the chrome above the editor (a command report) may have scrolled
365
- * rows into native history that cannot be pulled back; without the pin the
366
- * editor would jump up to where the shorter frame now ends. The pin lifts
367
- * once live rows fill the screen again.
368
- */
369
- pinInputToBottom(): void {
370
- this.#anchorAfterInlineRetirement = true;
371
- }
372
-
373
385
  /** Compose the bounded mutable viewport and the next ordered history append. */
374
386
  renderFrame(viewport: ViewportSize): TerminalFramePlan {
375
387
  if (!this.#started || this.#stopped) return { viewport: [] };
@@ -386,62 +398,56 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
386
398
  const transcriptIndex = roots.findIndex(root => root instanceof TranscriptContainer);
387
399
  if (transcriptIndex < 0) {
388
400
  this.#lastClickSpans = [];
389
- 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) };
390
408
  }
391
409
  const transcript = roots[transcriptIndex] as TranscriptContainer;
392
410
  const preRoots = this.#renderRoots(roots.slice(0, transcriptIndex), width);
393
411
  const afterRoots = roots.slice(transcriptIndex + 1);
394
412
  const after: string[] = [];
395
413
  const afterSpans: ViewportClickSpan[] = [];
396
- let transientRows = 0;
397
- let displacingRows = 0;
398
- let decisionPanelOpen = false;
399
414
  const ends: { root: Component; end: number }[] = [];
415
+ let transientRows = 0;
416
+ let selfClearingRows = 0;
400
417
  for (const root of afterRoots) {
401
- const chrome: Component = root;
402
418
  const start = after.length;
403
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;
404
422
  ends.push({ root, end: after.length });
405
- if (this.#transientChrome.has(root)) transientRows += after.length - start;
406
- if (
407
- chrome.retireDisplacedTranscript ||
408
- (root instanceof Container && root.children.some(child => child.retireDisplacedTranscript))
409
- ) {
410
- if (this.#transientChrome.has(root)) displacingRows += after.length - start;
411
- decisionPanelOpen = true;
412
- }
413
423
  }
414
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;
415
433
  // Offer history under capacity pressure only: blocks stay live (and keep
416
434
  // reflowing to the current width) while the screen has room. A batch
417
435
  // leaves the mutable viewport in the same frame it is appended, so its
418
436
  // rows are never painted twice.
419
- //
420
- // Retirement bills ordinary transient roots at their floor, not their
421
- // peak: a confirmation dialog or a tall multi-line editor clips the live tail for
422
- // its lifetime, but must not permanently commit transcript rows to native
423
- // history — otherwise a later shrink cannot refill the freed rows and the
424
- // editor drifts up above a band of blank rows (#11007).
425
- this.#transientChromeFloor = Math.min(this.#transientChromeFloor ?? transientRows, transientRows);
426
- // An ask panel remains open while the user reads the preceding response.
427
- // Its displaced settled rows must be reachable in scrollback now, rather
428
- // than clipped until the panel closes. Ordinary drafts retain their floor.
429
- const belowFloor = after.length - transientRows + Math.max(this.#transientChromeFloor, displacingRows);
430
437
  const now = performance.now();
431
438
  const frame: AnimationFrame = { now, tick: Math.floor(now / 80) };
432
439
  // Retirement measures the same live blocks the viewport lays out below;
433
440
  // one open frame renders each of them once for both.
434
441
  transcript.beginFrame(frame);
435
442
  const history = this.#offerHistory(transcript, width, rows, preRoots.length + belowFloor);
436
- if (decisionPanelOpen && history !== undefined) this.#anchorAfterInlineRetirement = true;
437
443
  const headerVisible = !this.#headerRetired && this.#offeredHistory?.source !== "header";
438
444
  const headerRows = headerVisible ? this.#header.render(width) : [];
439
445
  const before = [...headerRows, ...preRoots];
440
446
  // The live tail is laid out against the same baseline retirement is
441
447
  // billed against, so its compaction allocator (one row per block, no
442
448
  // inter-block blanks) engages only when a block genuinely cannot retire.
443
- // Rows the transient chrome peak displaces are clipped from the top by
444
- // 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.
445
451
  const active = transcript.renderViewport(width, Math.max(0, rows - before.length - belowFloor), frame);
446
452
  const activeSpans: ViewportClickSpan[] = [];
447
453
  for (const span of transcript.getLastViewportSpans()) {
@@ -451,13 +457,39 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
451
457
  }
452
458
  const drop = Math.max(0, before.length + active.length + after.length - rows);
453
459
  const mutable = [...before, ...active, ...after].slice(drop);
454
- // Once live rows fill the screen again, the retired gap is gone.
455
- if (!decisionPanelOpen && mutable.length >= rows) this.#anchorAfterInlineRetirement = false;
456
- // Rows retired during a decision panel cannot be pulled back from native
457
- // history when it closes. Keep the input pinned to the bottom without
458
- // replaying those rows (which would duplicate them) or clearing history.
459
- const topPadding = this.#anchorAfterInlineRetirement ? Math.max(0, rows - mutable.length) : 0;
460
- 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
+ }
461
493
  const viewportLength = mutable.length;
462
494
  const spans: ViewportClickSpan[] = [];
463
495
  const shift = (span: ViewportClickSpan, base: number): void => {
@@ -471,8 +503,8 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
471
503
  spans.push({ start: clamped, end, candidates: (local: number) => span.candidates(local + skew) });
472
504
  }
473
505
  };
474
- for (const span of activeSpans) shift(span, topPadding + before.length - drop);
475
- 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);
476
508
  this.#lastClickSpans = spans;
477
509
  if (history !== undefined && this.#offeredHistory?.source === "header") {
478
510
  const visibleHeaderRows = Math.max(0, rows - (mutable.length + drop));
@@ -681,6 +713,71 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
681
713
  }
682
714
  }
683
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
+
684
781
  /** Header retires first; replay coalesces it with the complete transcript ledger. */
685
782
  #offerHistory(
686
783
  transcript: TranscriptContainer,
@@ -894,7 +991,17 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
894
991
  const welcome = this.#welcome;
895
992
  if (!welcome) return;
896
993
  if (update.version !== undefined) welcome.setVersion(this.#version);
897
- welcome.setModel(this.#modelName, this.#providerName);
994
+ this.ui.requestRender();
995
+ }
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;
898
1005
  this.ui.requestRender();
899
1006
  }
900
1007
 
@@ -934,10 +1041,13 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
934
1041
  /** Mount or replace session-aware root children while preserving the header and status hosts. */
935
1042
  setRuntimeChildren(children: readonly Component[], options: RuntimeChildrenOptions = {}): void {
936
1043
  if (this.#stopped) return;
937
- this.#transientChrome = new Set(options.transient);
938
- this.#transientChromeFloor = undefined;
939
- this.#anchorAfterInlineRetirement = false;
1044
+ this.#chromeFloor = undefined;
1045
+ this.#chromeFloorPromoted = false;
1046
+ this.#resetIdleChrome();
940
1047
  this.#nativeDock = options.nativeDock;
1048
+ this.#transientChrome = new Set(options.transientChrome);
1049
+ this.#selfClearingChrome = new Set(options.selfClearingChrome);
1050
+ this.#transientFloor = undefined;
941
1051
  this.ui.removeChild(this.#statusHost);
942
1052
  if (this.#runtimeMounted) {
943
1053
  for (const child of this.#runtimeChildren) this.ui.removeChild(child);
@@ -976,20 +1086,10 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
976
1086
 
977
1087
  #applyWelcomeUpdate(update: ComposerWelcomeUpdate): void {
978
1088
  if (update.version !== undefined) this.#version = update.version;
979
- if (update.modelName !== undefined || update.providerName !== undefined) {
980
- this.#modelName = update.modelName ?? this.#modelName;
981
- this.#providerName = update.providerName ?? this.#providerName;
982
- }
983
1089
  }
984
1090
 
985
1091
  #ensureWelcome(): void {
986
- this.#welcome ??= new WelcomeComponent(
987
- this.#version,
988
- this.#modelName,
989
- this.#providerName,
990
- this.#recentSessions,
991
- this.#lspServers,
992
- );
1092
+ this.#welcome ??= new WelcomeComponent(this.#version);
993
1093
  }
994
1094
 
995
1095
  #rebuildHeader(): void {