@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
@@ -10,7 +10,7 @@
10
10
  import { ThinkingLevel } from "@linxiraos/pi-agent-core";
11
11
  import type { Model } from "@linxiraos/pi-ai";
12
12
  import { buildModel } from "@linxiraos/pi-catalog/build";
13
- import { getModelPricingStatus, modelsAreEqual } from "@linxiraos/pi-catalog/models";
13
+ import { getEffectiveTokenCost, getModelPricingStatus, modelsAreEqual } from "@linxiraos/pi-catalog/models";
14
14
  import { modelKind, type ModelKind, type ModelPricingStatus } from "@linxiraos/pi-catalog/types";
15
15
  import type { Component } from "../tui";
16
16
  import { FuzzyCorpus, fuzzyRank } from "../fuzzy";
@@ -37,7 +37,14 @@ import {
37
37
  } from "../keybinding-matchers";
38
38
  import { MenuSelection } from "../components/menu-selection";
39
39
  import { clampScrollOffset, scrollOffsetForRow } from "../components/scroll-viewport";
40
- import type { TspPickerColumn, TspPickerGroup, TspPickerItem, TspSpan, TspText } from "@linxiraos/pi-wire";
40
+ import type {
41
+ TspPickerColumn,
42
+ TspPickerGroup,
43
+ TspPickerItem,
44
+ TspPickerMark,
45
+ TspSpan,
46
+ TspText,
47
+ } from "@linxiraos/pi-wire";
41
48
  import { col, md, node, row, span, text } from "../native/describe";
42
49
  import { pickerFuzzyHits } from "../native/picker";
43
50
  import type { NativeChild, NativeNode, NativeUiEvent } from "../native/node";
@@ -735,7 +742,7 @@ function declaredPricingStatus(model: Model): Exclude<ModelPricingStatus, "fixed
735
742
  function isFreeModel(model: Model): boolean {
736
743
  const declared = declaredPricingStatus(model);
737
744
  if (declared !== undefined) return declared === "free";
738
- const cost = model.cost;
745
+ const cost = model.cost ? getEffectiveTokenCost(model.cost) : undefined;
739
746
  const credits = model.factoryDroidCredits;
740
747
  return (!cost || (cost.input === 0 && cost.output === 0)) && (credits === undefined || credits === 0);
741
748
  }
@@ -759,7 +766,8 @@ function withCreditBadge(model: Model, price: string): string {
759
766
  const credits = model.factoryDroidCredits;
760
767
  if (credits === undefined) return price;
761
768
  const badge = `${formatCostLeg(credits)}×`;
762
- return model.cost.input !== 0 || model.cost.output !== 0 ? `${price} ${badge}` : badge;
769
+ const cost = getEffectiveTokenCost(model.cost);
770
+ return cost.input !== 0 || cost.output !== 0 ? `${price} ${badge}` : badge;
763
771
  }
764
772
 
765
773
  const PRICING_STATUS_LABELS: Record<Exclude<ModelPricingStatus, "fixed">, { short: string; detail: string }> = {
@@ -774,7 +782,8 @@ function formatCostPair(model: Model): string {
774
782
  const declared = declaredPricingStatus(model);
775
783
  if (declared !== undefined) return PRICING_STATUS_LABELS[declared].short;
776
784
  if (isFreeModel(model)) return "free";
777
- return withCreditBadge(model, `$${formatCostLeg(model.cost.input)}/${formatCostLeg(model.cost.output)}`);
785
+ const cost = getEffectiveTokenCost(model.cost);
786
+ return withCreditBadge(model, `$${formatCostLeg(cost.input)}/${formatCostLeg(cost.output)}`);
778
787
  }
779
788
 
780
789
  /** Detail-pane price fact: `$3/15 per M`, or the declared pricing state in words. */
@@ -796,19 +805,34 @@ function pickerPrice(model: Model): string {
796
805
  const declared = declaredPricingStatus(model);
797
806
  if (declared !== undefined) return PRICING_STATUS_LABELS[declared].short;
798
807
  if (isFreeModel(model)) return "free";
799
- return withCreditBadge(model, `$${formatCostLeg(model.cost.input)}·${formatCostLeg(model.cost.output)}`);
808
+ const cost = getEffectiveTokenCost(model.cost);
809
+ return withCreditBadge(model, `$${formatCostLeg(cost.input)}·${formatCostLeg(cost.output)}`);
800
810
  }
801
811
 
802
812
  /** `$2 in · $10 out · $0.2 cache` for a model preview. */
803
813
  function previewPrice(model: Model): string {
804
814
  const declared = declaredPricingStatus(model);
805
815
  if (declared !== undefined) return PRICING_STATUS_LABELS[declared].detail;
806
- const cost = model.cost;
816
+ const cost = getEffectiveTokenCost(model.cost);
807
817
  const parts = [`$${formatCostLeg(cost.input)} in`, `$${formatCostLeg(cost.output)} out`];
808
818
  if (cost.cacheRead > 0) parts.push(`$${formatCostLeg(cost.cacheRead)} cache`);
809
819
  return withCreditBadge(model, parts.join(" · "));
810
820
  }
811
821
 
822
+ /**
823
+ * A provider's picker mark: the terminal's logo for it, else its initials
824
+ * (`amazon-bedrock` → `AB`, `anthropic` → `An`) on its own gradient.
825
+ */
826
+ export function providerMark(providerId: string): TspPickerMark {
827
+ const words = providerId.split(/[-_\s.]+/).filter(word => word.length > 0);
828
+ const word = words[0] ?? providerId;
829
+ const text =
830
+ words.length >= 2
831
+ ? `${words[0]!.charAt(0)}${words[1]!.charAt(0)}`.toUpperCase()
832
+ : `${word.charAt(0).toUpperCase()}${word.charAt(1)}`;
833
+ return { text, seed: providerId, icon: `provider:${providerId}` };
834
+ }
835
+
812
836
  /** The omp theme token of a thinking level's dot (`thinkingHigh`); none for inherit and auto. */
813
837
  export function thinkingDotToken(level: ConfiguredThinkingLevel): string | undefined {
814
838
  if (level === ThinkingLevel.Inherit || level === AUTO_THINKING) return undefined;
@@ -1999,6 +2023,7 @@ export class ModelBrowser implements Component {
1999
2023
  const value: TspPickerItem = {
2000
2024
  id: item.selector,
2001
2025
  label: quickRole ? item.id : item.selector,
2026
+ mark: providerMark(item.model.provider),
2002
2027
  mono: true,
2003
2028
  ...(quickRole ? { detail: `${item.model.provider}/${item.model.id}` } : {}),
2004
2029
  facts,
@@ -56,6 +56,7 @@ import {
56
56
  ModelBrowser,
57
57
  type ModelBrowserItem,
58
58
  modelSearchText,
59
+ providerMark,
59
60
  thinkingDotToken,
60
61
  type RoleAssignments,
61
62
  resolveRoleAssignments,
@@ -314,14 +315,6 @@ const MODEL_KIND_LABELS: Record<"all" | ModelKind, string> = {
314
315
  video: "Video",
315
316
  };
316
317
 
317
- /** A provider mark's initials: `amazon-bedrock` → `AB`, `anthropic` → `An`. */
318
- function providerInitials(providerId: string): string {
319
- const words = providerId.split(/[-_\s.]+/).filter(word => word.length > 0);
320
- if (words.length >= 2) return `${words[0]!.charAt(0)}${words[1]!.charAt(0)}`.toUpperCase();
321
- const word = words[0] ?? providerId;
322
- return `${word.charAt(0).toUpperCase()}${word.charAt(1)}`;
323
- }
324
-
325
318
  const PROVIDER_REFRESH_DEBOUNCE_MS = 120;
326
319
  const RECENT_LIMIT = 15;
327
320
  /** Accepted compaction limit input, shown beside the field: a scaled base, a fixed trigger, or a percentage. */
@@ -549,7 +542,7 @@ export class ModelHubComponent implements Component {
549
542
  }
550
543
 
551
544
  // Reconcile catalogs in the background. This is online discovery only —
552
- // it must not re-run `!command` credential helpers (F5 / `zeta-c models
545
+ // it must not re-run `!command` credential helpers (F5 / `zetacode models
553
546
  // refresh` pass refreshCommandCredentials for that). A --models scope is
554
547
  // registry-independent, so the reload would only repeat the hydration
555
548
  // above.
@@ -1335,6 +1328,7 @@ export class ModelHubComponent implements Component {
1335
1328
  */
1336
1329
  #thinkingOptionsFor(model: Model): ConfiguredThinkingLevel[] {
1337
1330
  if (!model.reasoning) return [];
1331
+ if (model.thinking?.unverified) return [ThinkingLevel.Inherit, ThinkingLevel.Off, ...getSupportedEfforts(model)];
1338
1332
  return [ThinkingLevel.Inherit, ThinkingLevel.Off, AUTO_THINKING, ...getSupportedEfforts(model)];
1339
1333
  }
1340
1334
 
@@ -3458,7 +3452,7 @@ export class ModelHubComponent implements Component {
3458
3452
  break;
3459
3453
  case "provider": {
3460
3454
  const providerId = entry.providerId ?? entry.label;
3461
- const mark = { text: providerInitials(providerId), seed: providerId };
3455
+ const mark = providerMark(providerId);
3462
3456
  if (entry.locked) {
3463
3457
  const envVars = providerEntry(providerId)?.envVars ?? [];
3464
3458
  scopes.push({
@@ -76,6 +76,8 @@ export interface ModelPickerOptions {
76
76
  taskModeKeys?: readonly KeyId[];
77
77
  /** `provider/id` highlighted and preselected in task mode (current Task subagent model). */
78
78
  taskSelector?: string;
79
+ /** Present selection-only chrome; consumers handle the chosen selector without mutating a session. */
80
+ selectOnly?: boolean;
79
81
  }
80
82
 
81
83
  /** Fixed chrome rows: top border, status row, footer, bottom border. */
@@ -90,9 +92,10 @@ const HEIGHT_FRACTION = 0.4;
90
92
  const STATUS_HINT = "Session-only switch — role models stay unchanged";
91
93
  const QUICK_ROLE_STATUS_HINT = "Quick role switch — applies its model and thinking for this session";
92
94
  const TASK_STATUS_HINT = "Task subagent switch — spawned task agents use this model (session-only)";
95
+ const SELECT_ONLY_STATUS_HINT = "Selection only — does not change the session model";
93
96
 
94
97
  /** Footer hint for the active mode; keys resolve at render time so theme/keybinding changes apply. */
95
- function footerHint(mode: "session" | "role" | "task"): string {
98
+ function footerHint(mode: "session" | "role" | "task" | "selection"): string {
96
99
  const upDown = editorKeys("tui.select.up", "tui.select.down");
97
100
  const enter = formatKeyHint("enter");
98
101
  const close = `${editorKey("tui.select.cancel")} close`;
@@ -101,6 +104,8 @@ function footerHint(mode: "session" | "role" | "task"): string {
101
104
  return `${upDown} roles · ${enter} apply role model · type to search · ${close}`;
102
105
  case "task":
103
106
  return `${upDown} models · ${enter} use for Task subagents · type to search · ${close}`;
107
+ case "selection":
108
+ return `${upDown} models · ${enter} select model · type to search · ${close}`;
104
109
  default:
105
110
  return `${upDown} models · ${enter} use for this session · type to search · @ quick roles · ${close}`;
106
111
  }
@@ -128,6 +133,7 @@ export class ModelPickerComponent implements Component {
128
133
  #taskMatchKeys = new Set<string>();
129
134
  #taskModeKey: KeyId | undefined;
130
135
  #taskSelector: string | undefined;
136
+ #selectOnly: boolean;
131
137
  #nativeRoot: { memo: string; node: NativeNode } | undefined;
132
138
  #pickerRoot:
133
139
  | {
@@ -151,10 +157,11 @@ export class ModelPickerComponent implements Component {
151
157
  this.#settings = settings;
152
158
  this.#registry = registry;
153
159
  this.#scopedModels = scopedModels;
160
+ this.#selectOnly = options.selectOnly ?? false;
154
161
  this.#currentSelector = options.currentSelector;
155
162
  this.#currentQuickRoleSelector = options.currentQuickRole ? `@${options.currentQuickRole}` : undefined;
156
163
  this.#taskSelector = options.taskSelector;
157
- if (callbacks.onPickTask) {
164
+ if (!this.#selectOnly && callbacks.onPickTask) {
158
165
  this.#taskModeKey = options.taskModeKeys?.[0];
159
166
  for (const key of options.taskModeKeys ?? []) addKeyAliases(this.#taskMatchKeys, key);
160
167
  }
@@ -165,7 +172,7 @@ export class ModelPickerComponent implements Component {
165
172
 
166
173
  this.#browser = new ModelBrowser(settings, {
167
174
  currentContextTokens: options.currentContextTokens,
168
- markOverContext: true,
175
+ markOverContext: !this.#selectOnly,
169
176
  emptyText: () =>
170
177
  this.#roleMode
171
178
  ? ` No quick roles in the ${editorKey("app.model.cycleForward") || formatKeyHint("ctrl+p")} cycle`
@@ -250,13 +257,13 @@ export class ModelPickerComponent implements Component {
250
257
 
251
258
  /** Switch browser content only when a leading `@` changes the search mode. */
252
259
  #syncItemsForQuery(query: string, refresh = false): void {
253
- const roleMode = query.startsWith("@") && !this.#taskMode;
260
+ const roleMode = !this.#selectOnly && query.startsWith("@") && !this.#taskMode;
254
261
  const modeChanged = roleMode !== this.#roleMode;
255
262
  if (!modeChanged && !refresh) return;
256
263
 
257
264
  this.#roleMode = roleMode;
258
265
  this.#browser.setShowProvider(!roleMode);
259
- this.#browser.setMarkOverContext(!roleMode && !this.#taskMode);
266
+ this.#browser.setMarkOverContext(!this.#selectOnly && !roleMode && !this.#taskMode);
260
267
  this.#browser.setPreserveQueryOrder(roleMode);
261
268
  const currentSelector = roleMode
262
269
  ? this.#currentQuickRoleSelector
@@ -299,20 +306,32 @@ export class ModelPickerComponent implements Component {
299
306
  this.#browser.setMaxVisible(Math.max(MIN_VISIBLE, listBudget));
300
307
 
301
308
  const inner = Math.max(1, width - 4);
309
+ const selectionOnly = this.#selectOnly;
302
310
  const status = this.#configError
303
311
  ? theme.fg("error", ` ${this.#configError}`)
304
312
  : this.#taskMode
305
313
  ? theme.fg("error", ` ${TASK_STATUS_HINT}`)
306
- : theme.fg("muted", ` ${this.#roleMode ? QUICK_ROLE_STATUS_HINT : STATUS_HINT}`);
314
+ : theme.fg(
315
+ "muted",
316
+ ` ${selectionOnly ? SELECT_ONLY_STATUS_HINT : this.#roleMode ? QUICK_ROLE_STATUS_HINT : STATUS_HINT}`,
317
+ );
307
318
 
308
319
  const borderColor: ThemeColor | undefined = this.#taskMode ? "error" : undefined;
309
- let footer = footerHint(this.#taskMode ? "task" : this.#roleMode ? "role" : "session");
320
+ let footer = footerHint(
321
+ this.#taskMode ? "task" : this.#roleMode ? "role" : selectionOnly ? "selection" : "session",
322
+ );
310
323
  if (this.#taskModeKey !== undefined && !this.#roleMode) {
311
324
  footer += ` · ${formatKeyHint(this.#taskModeKey)} ${this.#taskMode ? "session model" : "task model"}`;
312
325
  }
313
326
 
314
327
  const out: string[] = [];
315
- out.push(topBorder(width, this.#taskMode ? "Switch Task Model" : "Switch Model", borderColor));
328
+ out.push(
329
+ topBorder(
330
+ width,
331
+ this.#taskMode ? "Switch Task Model" : selectionOnly ? "Select Model" : "Switch Model",
332
+ borderColor,
333
+ ),
334
+ );
316
335
  out.push(row(status, width, borderColor));
317
336
  for (const line of this.#browser.render(inner)) {
318
337
  out.push(row(line, width, borderColor));
@@ -359,7 +378,7 @@ export class ModelPickerComponent implements Component {
359
378
  }
360
379
 
361
380
  #describePicker(): NativeNode {
362
- const mode = this.#taskMode ? "task" : this.#roleMode ? "role" : "session";
381
+ const mode = this.#selectOnly ? "selection" : this.#taskMode ? "task" : this.#roleMode ? "role" : "session";
363
382
  const query = this.#browser.query;
364
383
  const view = this.#browser.pickerOrder({
365
384
  providers: false,
@@ -390,13 +409,15 @@ export class ModelPickerComponent implements Component {
390
409
  ? [span(this.#configError, "error")]
391
410
  : mode === "task"
392
411
  ? [span(TASK_STATUS_HINT, "warning")]
393
- : mode === "role"
394
- ? QUICK_ROLE_STATUS_HINT
395
- : STATUS_HINT;
412
+ : mode === "selection"
413
+ ? SELECT_ONLY_STATUS_HINT
414
+ : mode === "role"
415
+ ? QUICK_ROLE_STATUS_HINT
416
+ : STATUS_HINT;
396
417
  const cycleKey = editorKey("app.model.cycleForward") || formatKeyHint("ctrl+p");
397
418
  const node = picker(
398
419
  {
399
- title: mode === "task" ? "Switch task model" : "Switch model",
420
+ title: mode === "task" ? "Switch task model" : mode === "selection" ? "Select model" : "Switch model",
400
421
  subtitle,
401
422
  icon: "cpu",
402
423
  noun: mode === "role" ? "roles" : "models",
@@ -405,7 +426,8 @@ export class ModelPickerComponent implements Component {
405
426
  preview: "below",
406
427
  query,
407
428
  cursor,
408
- placeholder: mode === "task" ? "Search models…" : "Search models, @ for quick roles…",
429
+ placeholder:
430
+ mode === "task" || mode === "selection" ? "Search models…" : "Search models, @ for quick roles…",
409
431
  columns: MODEL_PICKER_COLUMNS,
410
432
  ...catalogue,
411
433
  order: view.order,
@@ -417,11 +439,17 @@ export class ModelPickerComponent implements Component {
417
439
  actions: compact([
418
440
  pickerAction(
419
441
  "use",
420
- mode === "role" ? "Apply role" : mode === "task" ? "Use for Task subagents" : "Use for session",
442
+ mode === "role"
443
+ ? "Apply role"
444
+ : mode === "task"
445
+ ? "Use for Task subagents"
446
+ : mode === "selection"
447
+ ? "Select model"
448
+ : "Use for session",
421
449
  "enter",
422
450
  { primary: true },
423
451
  ),
424
- this.#taskModeKey !== undefined && mode !== "role"
452
+ this.#taskModeKey !== undefined && mode !== "role" && mode !== "selection"
425
453
  ? pickerAction("task", "Task model", this.#taskModeKey, { on: mode === "task" })
426
454
  : undefined,
427
455
  query.length > 0 ? { ...CLOSE_ACTION, label: "Clear search" } : CLOSE_ACTION,
@@ -435,7 +463,7 @@ export class ModelPickerComponent implements Component {
435
463
 
436
464
  /** Root card over the status line, the embedded browser, and the mode's key hints. */
437
465
  #describeCard(): NativeNode {
438
- const mode = this.#taskMode ? "task" : this.#roleMode ? "role" : "session";
466
+ const mode = this.#selectOnly ? "selection" : this.#taskMode ? "task" : this.#roleMode ? "role" : "session";
439
467
  const memo = `${mode}\0${this.#configError ?? ""}\0${this.#taskModeKey ?? ""}`;
440
468
  if (this.#nativeRoot?.memo === memo) return this.#nativeRoot.node;
441
469
 
@@ -443,7 +471,14 @@ export class ModelPickerComponent implements Component {
443
471
  ? span(this.#configError, "error")
444
472
  : this.#taskMode
445
473
  ? span(TASK_STATUS_HINT, "error")
446
- : span(this.#roleMode ? QUICK_ROLE_STATUS_HINT : STATUS_HINT, "muted");
474
+ : span(
475
+ mode === "selection"
476
+ ? SELECT_ONLY_STATUS_HINT
477
+ : this.#roleMode
478
+ ? QUICK_ROLE_STATUS_HINT
479
+ : STATUS_HINT,
480
+ "muted",
481
+ );
447
482
  const hints: (NativeHint | undefined)[] = [
448
483
  actionHint(["tui.select.up", "tui.select.down"], mode === "role" ? "roles" : "models"),
449
484
  {
@@ -453,20 +488,22 @@ export class ModelPickerComponent implements Component {
453
488
  ? "apply role model"
454
489
  : mode === "task"
455
490
  ? "use for Task subagents"
456
- : "use for this session",
491
+ : mode === "selection"
492
+ ? "select model"
493
+ : "use for this session",
457
494
  },
458
495
  { keys: [], label: "type to search" },
459
496
  ];
460
497
  if (mode === "session") hints.push({ keys: ["@"], label: "quick roles" });
461
498
  hints.push(actionHint("tui.select.cancel", "close"));
462
- if (this.#taskModeKey !== undefined && !this.#roleMode) {
499
+ if (this.#taskModeKey !== undefined && !this.#roleMode && mode !== "selection") {
463
500
  hints.push({ keys: [this.#taskModeKey], label: this.#taskMode ? "session model" : "task model" });
464
501
  }
465
502
  // `card` directly: the task-mode tone is a common prop `overlayCard` doesn't take.
466
503
  const node = card(
467
504
  {
468
505
  role: "zeta.overlay.model-picker",
469
- head: this.#taskMode ? "Switch Task Model" : "Switch Model",
506
+ head: mode === "task" ? "Switch Task Model" : mode === "selection" ? "Select Model" : "Switch Model",
470
507
  tone: this.#taskMode ? "error" : undefined,
471
508
  },
472
509
  [text([status], { wrap: "word" }), this.#browser, hintsRow(hints)],
@@ -20,7 +20,7 @@ import { matchesAppInterrupt } from "../keybinding-matchers";
20
20
  import { formatKeyHint, formatTooltipKey } from "../app-keybindings";
21
21
  import { boundKeys, interruptKey } from "../chrome/keybinding-hints";
22
22
  import type { NativeNode, NativeUiEvent } from "../native/node";
23
- import { col, node, row, span, text } from "../native/describe";
23
+ import { col, keyed, node, row, span, text } from "../native/describe";
24
24
  import { actionBar, actionButton } from "../native/overlay";
25
25
  import { isNativeRendering } from "../native/state";
26
26
 
@@ -57,6 +57,14 @@ const BODY_LINES = [
57
57
  "In-flight calls finish; nothing new starts until you resume.",
58
58
  ] as const;
59
59
 
60
+ /** What the hold means, one fact per row of the native sheet: icon, what, detail. */
61
+ const HOLD_FACTS = [
62
+ ["users", "Every agent holds", "Main agent, subagents and advisor stop at their next step."],
63
+ ["activity", "Running calls finish", "A reply or tool already under way completes; nothing new starts."],
64
+ ["message", "Queued messages wait", "Steering and follow-ups already queued send after you resume."],
65
+ ["pen", "The repo is yours", "Edit files freely, then tell the agent what changed."],
66
+ ] as const;
67
+
60
68
  /** Live hold clock, seconds-precise: `0:07`, `12:34`, `1:02:03`. */
61
69
  function formatClock(ms: number): string {
62
70
  const totalSeconds = Math.max(0, Math.floor(ms / 1000));
@@ -176,33 +184,83 @@ export class PauseScreenComponent implements Component, OverlayFocusOwner {
176
184
  /** A glass sheet titled "Paused" (not the whole pane: the transcript stays visible, frozen). */
177
185
  readonly nativeOverlay = { role: "zeta.overlay.pause", size: "md", anchor: "center", head: "Paused" } as const;
178
186
 
187
+ /**
188
+ * The sheet: a hero (the pause glyph beside the live hold clock and the
189
+ * session it holds), what the hold means as four facts, and a footer that
190
+ * reassures and resumes. The resume keys live in the button's tooltip.
191
+ */
179
192
  describe(): NativeNode {
180
193
  if (this.#native) return this.#native;
181
- const children: NativeNode[] = [];
182
- if (this.host.sessionName) children.push(text([span(this.host.sessionName, "strong")]));
183
194
  const resumeKey = boundKeys("app.interrupt", ["escape"])[0] ?? "escape";
184
- children.push(
185
- text(
186
- BODY_LINES.map((line, index) => span(index === 0 ? line : `\n${line}`, "muted")),
187
- { wrap: "word" },
188
- ),
189
- row(
190
- [
191
- text([span("Paused for", "dim")]),
192
- node("elapsed", { age: Date.now() - this.#startedAt, format: "clock" }),
193
- ],
194
- { gap: "xs", align: "baseline" },
195
+ const hero = row(
196
+ [
197
+ node("icon", { name: "pause", role: "zeta.pause.glyph" }, undefined, "glyph"),
198
+ col(
199
+ [
200
+ node(
201
+ "elapsed",
202
+ {
203
+ age: Date.now() - this.#startedAt,
204
+ format: "clock",
205
+ role: "zeta.pause.clock",
206
+ title: "Paused for",
207
+ },
208
+ undefined,
209
+ "clock",
210
+ ),
211
+ keyed(
212
+ text([span(this.host.sessionName ?? "Every agent in this session", "muted")], {
213
+ role: "zeta.pause.session",
214
+ wrap: "none",
215
+ truncate: "end",
216
+ }),
217
+ "session",
218
+ ),
219
+ ],
220
+ { gap: "none", grow: 1, shrink: 1 },
221
+ ),
222
+ ],
223
+ { role: "zeta.pause.hero", gap: "md", align: "center" },
224
+ );
225
+ const facts = col(
226
+ HOLD_FACTS.map(([icon, what, detail], index) =>
227
+ keyed(
228
+ row(
229
+ [
230
+ node("icon", { name: icon }),
231
+ col([text([span(what, "strong")]), text([span(detail, "muted")], { wrap: "word" })], {
232
+ gap: "none",
233
+ grow: 1,
234
+ shrink: 1,
235
+ }),
236
+ ],
237
+ { role: "zeta.pause.fact", gap: "md", align: "start" },
238
+ ),
239
+ `fact-${index}`,
240
+ ),
195
241
  ),
196
- actionBar([
197
- null,
198
- actionButton("Resume", "resume", {
199
- keys: resumeKey,
200
- tone: "accent",
201
- title: `Resume ${formatTooltipKey(resumeKey)} · enter · space`,
202
- }),
203
- ]),
242
+ { role: "zeta.pause.facts", gap: "md" },
243
+ );
244
+ this.#native = col(
245
+ [
246
+ keyed(hero, "hero"),
247
+ keyed(facts, "facts"),
248
+ actionBar([
249
+ keyed(
250
+ text([span("Each agent continues where it stopped.", "muted")], { role: "zeta.pause.note" }),
251
+ "note",
252
+ ),
253
+ null,
254
+ actionButton("Resume", "resume", {
255
+ keys: resumeKey,
256
+ tone: "accent",
257
+ // Each ` · ` clause ending in two spaces and a key is a keycap: `Resume [esc] · [⏎] · [space]`.
258
+ title: `Resume ${formatTooltipKey(resumeKey)} · ${formatTooltipKey("enter")} · ${formatTooltipKey("space")}`,
259
+ }),
260
+ ]),
261
+ ],
262
+ { gap: "md" },
204
263
  );
205
- this.#native = col(children, { gap: "md" });
206
264
  return this.#native;
207
265
  }
208
266
 
@@ -606,6 +606,22 @@ export class PlanReviewOverlay implements Component {
606
606
  slider.onChange?.(next);
607
607
  }
608
608
 
609
+ /** Synchronize an external tier pick with the displayed slider before committing. */
610
+ setSliderIndex(index: number): void {
611
+ if (this.#committed || !this.#slider) return;
612
+ this.#moveSlider(index - this.#sliderIndex);
613
+ }
614
+
615
+ /** Commit an external answer through the same input-locking path as Enter; false when nothing was committed. */
616
+ selectOption(label: string): boolean {
617
+ if (this.#committed) return false;
618
+ const index = this.#options.indexOf(label);
619
+ if (index < 0 || this.#disabled.has(index)) return false;
620
+ this.#selectedIndex = index;
621
+ this.#confirmSelection();
622
+ return this.#committed;
623
+ }
624
+
609
625
  #confirmSelection(): void {
610
626
  const index = this.#selectedIndex;
611
627
  if (index >= 0 && index < this.#options.length && !this.#disabled.has(index)) {
@@ -11,6 +11,7 @@ import { routeSelectListMouseWithTopBorder } from "../chrome/select-list-mouse-r
11
11
  import { tuiText } from "../i18n";
12
12
  import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node";
13
13
  import { SelectListSheet } from "../native/picker";
14
+ import type { TspPickerItem } from "@linxiraos/pi-wire";
14
15
 
15
16
  export interface PluginSelectorCallbacks {
16
17
  onSelect: (pluginName: string, marketplace: string, scope?: "user" | "project") => void;
@@ -36,6 +37,8 @@ export class PluginSelectorComponent extends OverlayPanel {
36
37
  ) {
37
38
  super(tuiText("ssTabPlugins", "Plugins"), "zeta.overlay.plugins");
38
39
 
40
+ // The sheet draws version, install state and scope as badges instead of bracketed label text.
41
+ const badgesByValue = new Map<string, NonNullable<TspPickerItem["badges"]>>();
39
42
  const items: SelectItem[] = plugins.map(({ plugin, marketplace, scope }) => {
40
43
  // Encode scope into the value so onSelect can recover it without a parallel Map.
41
44
  // Format: "name@marketplace" or "name@marketplace#scope"
@@ -46,6 +49,11 @@ export class PluginSelectorComponent extends OverlayPanel {
46
49
  const scopeTag = scope
47
50
  ? ` [${scope === "user" ? tuiText("mcpScopeWordUser", "user") : tuiText("mcpScopeWordProject", "project")}]`
48
51
  : "";
52
+ badgesByValue.set(id, [
53
+ ...(plugin.version ? [{ text: plugin.version }] : []),
54
+ ...(installed ? [{ text: "installed", tone: "success" as const }] : []),
55
+ ...(scope ? [{ text: scope, tone: "accent" as const }] : []),
56
+ ]);
49
57
 
50
58
  return {
51
59
  value: id,
@@ -86,7 +94,15 @@ export class PluginSelectorComponent extends OverlayPanel {
86
94
  icon: "plus",
87
95
  noun: "plugins",
88
96
  confirm: "Install",
89
- decorate: item => (item.hint ? { facts: { marketplace: item.hint } } : undefined),
97
+ decorate: item => {
98
+ const [name] = splitPluginId(item.value);
99
+ if (!name) return undefined;
100
+ return {
101
+ label: name,
102
+ badges: badgesByValue.get(item.value) ?? [],
103
+ ...(item.hint ? { facts: { marketplace: item.hint } } : {}),
104
+ };
105
+ },
90
106
  columns: [{ id: "marketplace", format: "dim" }],
91
107
  });
92
108
  }
@@ -307,9 +307,15 @@ export class PluginListComponent extends OverlayPanel {
307
307
  if (entries.length === 0) {
308
308
  this.addChild(new Text(theme.fg("muted", "No plugins installed"), 0, 0));
309
309
  this.addChild(new Spacer(1));
310
- this.addChild(new Text(theme.fg("dim", "Install npm plugins: zeta-c plugin install <package>"), 0, 0));
311
310
  this.addChild(
312
- new Text(theme.fg("dim", "Install marketplace plugins: zeta-c plugin install <name>@<marketplace>"), 0, 0),
311
+ new Text(theme.fg("dim", "Install npm plugins: zetacode plugin install <package>"), 0, 0),
312
+ );
313
+ this.addChild(
314
+ new Text(
315
+ theme.fg("dim", "Install marketplace plugins: zetacode plugin install <name>@<marketplace>"),
316
+ 0,
317
+ 0,
318
+ ),
313
319
  );
314
320
  this.addChild(new Spacer(1));
315
321
 
@@ -53,9 +53,19 @@ export class QueueModeSelectorComponent extends OverlayPanel {
53
53
  this.addChild(this.#selectList);
54
54
  this.#sheet = new SelectListSheet(this.#selectList, {
55
55
  title: "Queue mode",
56
+ subtitle: "How messages you send while omp works are delivered",
56
57
  icon: "list",
57
58
  noun: "modes",
58
59
  current: [currentMode],
60
+ // The terminal list shows the setting's values; the sheet names them for people.
61
+ decorate: item =>
62
+ item.value === "one-at-a-time"
63
+ ? {
64
+ label: "One at a time",
65
+ detail: "Each queued message gets its own turn",
66
+ badges: [{ text: "recommended" }],
67
+ }
68
+ : { label: "All at once", detail: "Queued messages are sent together in one turn" },
59
69
  });
60
70
  }
61
71
 
@@ -129,11 +129,15 @@ export class ReportPanel extends OverlayPanel {
129
129
  return this.#body.render(Math.max(1, width - 4)).length + TEXT_CHROME_ROWS;
130
130
  }
131
131
 
132
- /** The sheet body (the overlay is the frame, and its body the scroller): the report, then Close (Esc). */
132
+ /**
133
+ * The sheet body (the overlay is the frame, and its body the scroller): the
134
+ * report as `zeta.report.body`, which the terminal sets on the reading
135
+ * measure, then Close (Esc) as the sheet's footer.
136
+ */
133
137
  override describe(): NativeNode {
134
138
  const scroll = this.#scroll;
135
139
  if (this.#native && this.#native.scroll === scroll) return this.#native.node;
136
- const body: NativeNode = { ...col([this.#body]), key: "body", scroll };
140
+ const body: NativeNode = { ...col([this.#body], { role: "zeta.report.body" }), key: "body", scroll };
137
141
  const node = col([body, actionBar([null, actionButton("Close", "close", { keys: "escape" })])], { gap: "md" });
138
142
  this.#native = { scroll, node };
139
143
  return node;