@linxiraos/pi-tui 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/CHANGELOG.md +245 -224
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.txt +55 -5
  4. package/dist/types/app-keybindings.d.ts +18 -8
  5. package/dist/types/apps/git/diff-pane.d.ts +5 -0
  6. package/dist/types/apps/git/git-tui.d.ts +16 -2
  7. package/dist/types/apps/git/state.d.ts +11 -0
  8. package/dist/types/autocomplete.d.ts +2 -0
  9. package/dist/types/charts/chart-svg.d.ts +7 -13
  10. package/dist/types/chat/advisor-message.d.ts +4 -1
  11. package/dist/types/chat/assistant-message.d.ts +9 -4
  12. package/dist/types/chat/figure-markdown.d.ts +7 -2
  13. package/dist/types/chat/messages.d.ts +3 -1
  14. package/dist/types/chat/read-tool-group.d.ts +2 -2
  15. package/dist/types/chat/table-chart.d.ts +11 -15
  16. package/dist/types/chat/tool-execution.d.ts +32 -20
  17. package/dist/types/chat/user-message.d.ts +3 -2
  18. package/dist/types/chrome/countdown-timer.d.ts +3 -2
  19. package/dist/types/chrome/diff.d.ts +2 -3
  20. package/dist/types/chrome/status-notice.d.ts +2 -0
  21. package/dist/types/chrome/transcript-container.d.ts +2 -0
  22. package/dist/types/components/image.d.ts +2 -0
  23. package/dist/types/components/markdown.d.ts +49 -3
  24. package/dist/types/components/text.d.ts +2 -0
  25. package/dist/types/debug-server.d.ts +2 -0
  26. package/dist/types/keybinding-matchers.d.ts +6 -0
  27. package/dist/types/multiplexers/tmux.d.ts +1 -0
  28. package/dist/types/multiplexers/types.d.ts +6 -0
  29. package/dist/types/native/backend.d.ts +5 -4
  30. package/dist/types/native/blobs.d.ts +24 -12
  31. package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
  32. package/dist/types/overlays/annotation-overlay.d.ts +19 -0
  33. package/dist/types/overlays/approval-dialog.d.ts +79 -0
  34. package/dist/types/overlays/ask-dialog.d.ts +11 -6
  35. package/dist/types/overlays/btw-history-panel.d.ts +2 -6
  36. package/dist/types/overlays/hook-editor.d.ts +7 -2
  37. package/dist/types/overlays/hook-input.d.ts +12 -4
  38. package/dist/types/overlays/job-tree.d.ts +50 -0
  39. package/dist/types/overlays/jobs-panel.d.ts +25 -11
  40. package/dist/types/overlays/login-dialog.d.ts +8 -3
  41. package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
  42. package/dist/types/overlays/model-browser.d.ts +6 -1
  43. package/dist/types/overlays/model-picker.d.ts +2 -0
  44. package/dist/types/overlays/pause-screen.d.ts +5 -0
  45. package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
  46. package/dist/types/overlays/report-panel.d.ts +5 -1
  47. package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
  48. package/dist/types/overlays/session-account-selector.d.ts +4 -0
  49. package/dist/types/overlays/session-info-overlay.d.ts +7 -3
  50. package/dist/types/overlays/session-selector.d.ts +3 -1
  51. package/dist/types/overlays/usage-dashboard.d.ts +6 -6
  52. package/dist/types/prompt/composer.d.ts +23 -20
  53. package/dist/types/prompt/custom-editor.d.ts +20 -10
  54. package/dist/types/prompt/image-references.d.ts +22 -0
  55. package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
  56. package/dist/types/prompt/macos-spelling.d.ts +2 -1
  57. package/dist/types/prompt/prose-gate.d.ts +3 -0
  58. package/dist/types/prompt/queued-messages.d.ts +1 -1
  59. package/dist/types/prompt/typing-word.d.ts +25 -0
  60. package/dist/types/prompt/welcome.d.ts +26 -6
  61. package/dist/types/render/hanging-wrap.d.ts +7 -0
  62. package/dist/types/render/hyperlink.d.ts +2 -0
  63. package/dist/types/render/render-utils.d.ts +9 -1
  64. package/dist/types/render/utils.d.ts +1 -1
  65. package/dist/types/setup/scenes/splash.d.ts +6 -5
  66. package/dist/types/status-line/context-usage.d.ts +4 -1
  67. package/dist/types/status-line/host.d.ts +6 -4
  68. package/dist/types/status-line/startup.d.ts +6 -6
  69. package/dist/types/status-line/types.d.ts +3 -0
  70. package/dist/types/stdin-buffer.d.ts +7 -0
  71. package/dist/types/terminal-capabilities.d.ts +16 -1
  72. package/dist/types/terminal-multiplexer.d.ts +7 -0
  73. package/dist/types/terminal.d.ts +8 -4
  74. package/dist/types/theme/symbols.d.ts +1 -1
  75. package/dist/types/theme/theme.d.ts +11 -0
  76. package/dist/types/thinking.d.ts +5 -0
  77. package/dist/types/tmux.d.ts +39 -4
  78. package/dist/types/tools/ast-edit.d.ts +2 -2
  79. package/dist/types/tools/ast-grep.d.ts +2 -2
  80. package/dist/types/tools/bash.d.ts +6 -1
  81. package/dist/types/tools/edit.d.ts +2 -0
  82. package/dist/types/tools/eval.d.ts +53 -4
  83. package/dist/types/tools/find.d.ts +14 -4
  84. package/dist/types/tools/glob.d.ts +3 -3
  85. package/dist/types/tools/grep.d.ts +2 -2
  86. package/dist/types/tools/grouped-file-output.d.ts +9 -4
  87. package/dist/types/tools/native-view.d.ts +1 -1
  88. package/dist/types/tools/read.d.ts +32 -10
  89. package/dist/types/tools/renderer.d.ts +2 -0
  90. package/dist/types/tools/task.d.ts +31 -0
  91. package/dist/types/tools/web-search-types.d.ts +1 -0
  92. package/dist/types/tools/write.d.ts +2 -0
  93. package/dist/types/tui.d.ts +20 -3
  94. package/package.json +10 -10
  95. package/src/app-keybindings.d.ts +18 -8
  96. package/src/app-keybindings.ts +20 -8
  97. package/src/apps/autoresearch-dashboard.ts +444 -27
  98. package/src/apps/debug/raw-sse.ts +2 -2
  99. package/src/apps/git/diff-pane.ts +39 -41
  100. package/src/apps/git/git-tui.ts +80 -14
  101. package/src/apps/git/sidebar.ts +10 -1
  102. package/src/apps/git/state.ts +12 -0
  103. package/src/autocomplete.d.ts +2 -0
  104. package/src/autocomplete.ts +21 -19
  105. package/src/bracketed-paste.ts +9 -2
  106. package/src/charts/chart-svg.d.ts +7 -13
  107. package/src/charts/chart-svg.ts +19 -22
  108. package/src/charts/table-data.ts +17 -6
  109. package/src/chat/advisor-message.d.ts +4 -1
  110. package/src/chat/advisor-message.ts +77 -24
  111. package/src/chat/assistant-message.d.ts +9 -4
  112. package/src/chat/assistant-message.ts +127 -31
  113. package/src/chat/bash-execution.ts +7 -2
  114. package/src/chat/figure-markdown.d.ts +7 -2
  115. package/src/chat/figure-markdown.ts +10 -3
  116. package/src/chat/image-loading.ts +9 -6
  117. package/src/chat/messages.d.ts +3 -1
  118. package/src/chat/messages.ts +4 -1
  119. package/src/chat/read-tool-group.d.ts +2 -2
  120. package/src/chat/read-tool-group.ts +97 -42
  121. package/src/chat/svg-figure.ts +3 -1
  122. package/src/chat/svg-source.ts +6 -3
  123. package/src/chat/table-chart.d.ts +11 -15
  124. package/src/chat/table-chart.ts +24 -57
  125. package/src/chat/tool-execution.d.ts +32 -20
  126. package/src/chat/tool-execution.ts +103 -36
  127. package/src/chat/user-message.d.ts +3 -2
  128. package/src/chat/user-message.ts +54 -34
  129. package/src/chrome/countdown-timer.ts +3 -3
  130. package/src/chrome/diff.d.ts +2 -3
  131. package/src/chrome/diff.ts +4 -9
  132. package/src/chrome/status-notice.ts +5 -0
  133. package/src/chrome/transcript-container.d.ts +2 -0
  134. package/src/chrome/transcript-container.ts +212 -28
  135. package/src/components/editor.ts +12 -12
  136. package/src/components/image.d.ts +2 -0
  137. package/src/components/image.ts +54 -20
  138. package/src/components/markdown.d.ts +49 -3
  139. package/src/components/markdown.ts +482 -12
  140. package/src/components/settings-list.ts +2 -4
  141. package/src/components/text.d.ts +2 -0
  142. package/src/components/text.ts +11 -1
  143. package/src/debug-server.d.ts +2 -0
  144. package/src/debug-server.ts +51 -69
  145. package/src/hotkeys-markdown.ts +2 -0
  146. package/src/keybinding-matchers.d.ts +6 -0
  147. package/src/keybinding-matchers.ts +19 -0
  148. package/src/multiplexers/tmux.d.ts +1 -0
  149. package/src/multiplexers/tmux.ts +3 -0
  150. package/src/multiplexers/types.d.ts +6 -0
  151. package/src/multiplexers/types.ts +6 -0
  152. package/src/native/backend.d.ts +5 -4
  153. package/src/native/backend.ts +13 -5
  154. package/src/native/blobs.d.ts +24 -12
  155. package/src/native/blobs.ts +56 -7
  156. package/src/native/reconcile.ts +43 -21
  157. package/src/overlays/advisor-config.ts +259 -8
  158. package/src/overlays/agent-hub-projection.d.ts +10 -5
  159. package/src/overlays/agent-hub-projection.ts +63 -12
  160. package/src/overlays/annotation-overlay.ts +422 -168
  161. package/src/overlays/approval-dialog.ts +389 -0
  162. package/src/overlays/ask-dialog.ts +244 -105
  163. package/src/overlays/btw-history-panel.ts +208 -147
  164. package/src/overlays/extensions/extension-list.ts +4 -6
  165. package/src/overlays/hook-editor.ts +90 -32
  166. package/src/overlays/hook-input.ts +105 -19
  167. package/src/overlays/job-tree.ts +177 -0
  168. package/src/overlays/jobs-panel.ts +222 -142
  169. package/src/overlays/login-dialog.ts +201 -91
  170. package/src/overlays/mcp-add-wizard.ts +562 -279
  171. package/src/overlays/model-browser.ts +32 -7
  172. package/src/overlays/model-hub.ts +4 -10
  173. package/src/overlays/model-picker.ts +58 -21
  174. package/src/overlays/pause-screen.ts +81 -23
  175. package/src/overlays/plan-review-overlay.ts +16 -0
  176. package/src/overlays/plugin-selector.ts +17 -1
  177. package/src/overlays/queue-mode-selector.ts +10 -0
  178. package/src/overlays/report-panel.ts +6 -2
  179. package/src/overlays/reset-usage-selector.ts +130 -21
  180. package/src/overlays/session-account-selector.ts +36 -0
  181. package/src/overlays/session-info-overlay.ts +199 -76
  182. package/src/overlays/session-selector.ts +15 -10
  183. package/src/overlays/settings-selector.ts +3 -0
  184. package/src/overlays/show-images-selector.ts +1 -0
  185. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  186. package/src/overlays/theme-selector.ts +6 -1
  187. package/src/overlays/thinking-selector.ts +8 -4
  188. package/src/overlays/tree-selector.ts +117 -2
  189. package/src/overlays/usage-dashboard.ts +498 -144
  190. package/src/prompt/composer-cache.ts +4 -1
  191. package/src/prompt/composer.ts +200 -74
  192. package/src/prompt/custom-editor.ts +168 -65
  193. package/src/prompt/image-references.d.ts +22 -0
  194. package/src/prompt/image-references.ts +79 -2
  195. package/src/prompt/internal-url-autocomplete.ts +1 -0
  196. package/src/prompt/macos-spelling.ts +28 -5
  197. package/src/prompt/prose-gate.ts +3 -0
  198. package/src/prompt/queued-messages.ts +9 -2
  199. package/src/prompt/tips.txt +1 -2
  200. package/src/prompt/typing-word.ts +59 -0
  201. package/src/prompt/welcome.ts +97 -45
  202. package/src/render/code-cell.ts +18 -5
  203. package/src/render/hanging-wrap.d.ts +7 -0
  204. package/src/render/hanging-wrap.ts +58 -0
  205. package/src/render/hyperlink.d.ts +2 -0
  206. package/src/render/hyperlink.ts +5 -0
  207. package/src/render/render-utils.d.ts +9 -1
  208. package/src/render/render-utils.ts +16 -2
  209. package/src/render/utils.d.ts +1 -1
  210. package/src/render/utils.ts +1 -1
  211. package/src/render/width-aware-text.ts +6 -5
  212. package/src/setup/scenes/splash.ts +22 -8
  213. package/src/status-line/component.ts +79 -11
  214. package/src/status-line/context-usage.ts +176 -73
  215. package/src/status-line/footer.ts +18 -35
  216. package/src/status-line/host.ts +6 -1
  217. package/src/status-line/segments.ts +16 -5
  218. package/src/status-line/startup.ts +6 -6
  219. package/src/status-line/types.ts +1 -0
  220. package/src/stdin-buffer.d.ts +7 -0
  221. package/src/stdin-buffer.ts +300 -62
  222. package/src/terminal-capabilities.d.ts +16 -1
  223. package/src/terminal-capabilities.ts +66 -28
  224. package/src/terminal-multiplexer.d.ts +7 -0
  225. package/src/terminal-multiplexer.ts +14 -0
  226. package/src/terminal.d.ts +8 -4
  227. package/src/terminal.ts +190 -63
  228. package/src/theme/symbols.d.ts +1 -1
  229. package/src/theme/symbols.ts +1 -0
  230. package/src/theme/theme-class.ts +1 -0
  231. package/src/theme/theme.d.ts +11 -0
  232. package/src/theme/theme.ts +46 -14
  233. package/src/thinking.d.ts +5 -0
  234. package/src/thinking.ts +9 -1
  235. package/src/tmux.d.ts +39 -4
  236. package/src/tmux.ts +110 -16
  237. package/src/tools/ask.ts +20 -9
  238. package/src/tools/ast-edit.d.ts +2 -2
  239. package/src/tools/ast-edit.ts +30 -7
  240. package/src/tools/ast-grep.d.ts +2 -2
  241. package/src/tools/ast-grep.ts +39 -35
  242. package/src/tools/bash.d.ts +6 -1
  243. package/src/tools/bash.ts +15 -9
  244. package/src/tools/debug.ts +2 -1
  245. package/src/tools/default-renderer.ts +3 -2
  246. package/src/tools/edit.d.ts +2 -0
  247. package/src/tools/edit.ts +100 -21
  248. package/src/tools/eval.d.ts +53 -4
  249. package/src/tools/eval.ts +173 -137
  250. package/src/tools/find.d.ts +14 -4
  251. package/src/tools/find.ts +71 -18
  252. package/src/tools/glob.d.ts +3 -3
  253. package/src/tools/glob.ts +51 -17
  254. package/src/tools/grep.d.ts +2 -2
  255. package/src/tools/grep.ts +99 -22
  256. package/src/tools/grouped-file-output.d.ts +9 -4
  257. package/src/tools/grouped-file-output.ts +21 -6
  258. package/src/tools/lsp.ts +30 -18
  259. package/src/tools/mcp.ts +2 -2
  260. package/src/tools/native-view.d.ts +1 -1
  261. package/src/tools/native-view.ts +2 -2
  262. package/src/tools/read.d.ts +32 -10
  263. package/src/tools/read.ts +89 -43
  264. package/src/tools/renderer.d.ts +2 -0
  265. package/src/tools/renderer.ts +2 -0
  266. package/src/tools/task.d.ts +31 -0
  267. package/src/tools/task.ts +40 -7
  268. package/src/tools/todo.ts +3 -2
  269. package/src/tools/wait.ts +16 -30
  270. package/src/tools/web-search-types.d.ts +1 -0
  271. package/src/tools/web-search-types.ts +1 -0
  272. package/src/tools/web-search.ts +2 -1
  273. package/src/tools/write.d.ts +2 -0
  274. package/src/tools/write.ts +36 -12
  275. package/src/tools/yield.ts +2 -2
  276. package/src/tui.d.ts +20 -3
  277. package/src/tui.ts +160 -43
  278. package/src/utils.ts +5 -3
  279. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  280. package/src/tools/eval-format/code-mode-declarations.ts +0 -61
@@ -10,8 +10,10 @@ import { formatUsageResetWindow } from "./usage-display";
10
10
  import { formatKeyHint } from "../app-keybindings";
11
11
  import { editorKey, editorKeys } from "../chrome/keybinding-hints";
12
12
  import { node, span, text } from "../native/describe";
13
- import type { NativeNode, NativeUiEvent } from "../native/node";
13
+ import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node";
14
14
  import { actionHint, hintsRow, overlayCard } from "../native/overlay";
15
+ import { CLOSE_ACTION, dockedPicker, PICKER_KEY, pickerAction, pickerEvent } from "../native/picker";
16
+ import type { TspPickerItem, TspPickerProps, TspSpan } from "@linxiraos/pi-wire";
15
17
 
16
18
  const RESET_SELECTOR_MAX_VISIBLE = 10;
17
19
 
@@ -36,6 +38,37 @@ function accountCountLabel(account: ResetUsageAccount): string {
36
38
  return countLabel;
37
39
  }
38
40
 
41
+ /** The picker row of an account: provider mark, identity, its resets as a fact, unusable ones disabled with the reason. */
42
+ function accountPickerItem(account: ResetUsageAccount, id: string): TspPickerItem {
43
+ const redeemable = account.redeemableCount > 0;
44
+ const detail: TspSpan[] = [span(`${oneLine(account.providerLabel)} · #${account.target.credentialId}`)];
45
+ const reason = account.error ?? (redeemable ? undefined : (account.unavailableReason ?? "no resets usable now"));
46
+ if (reason) detail.push(span(` · ${oneLine(reason)}`, account.error ? "error" : "warning"));
47
+ let resets: TspSpan;
48
+ if (account.error) resets = span("unavailable", "error");
49
+ else if (!redeemable) resets = span(`${account.availableCount} saved`, "dim");
50
+ else if (account.redeemableCount === account.availableCount)
51
+ resets = span(`${account.redeemableCount} usable`, "success");
52
+ else resets = span(`${account.redeemableCount} of ${account.availableCount} usable`, "success");
53
+ const facts: Record<string, TspSpan[]> = { resets: [resets] };
54
+ const expiryMs = account.expiresAt ? Date.parse(account.expiresAt) : Number.NaN;
55
+ if (!Number.isNaN(expiryMs)) {
56
+ facts.expires = [
57
+ expiryMs > Date.now()
58
+ ? span(`expires in ${formatDuration(expiryMs - Date.now())}`, "dim")
59
+ : span("expired", "warning"),
60
+ ];
61
+ }
62
+ return {
63
+ id,
64
+ label: oneLine(account.label),
65
+ detail,
66
+ mark: { text: account.providerLabel, seed: account.provider },
67
+ facts,
68
+ ...(reason ? { disabled: oneLine(reason) } : {}),
69
+ };
70
+ }
71
+
39
72
  /** One account row with its redeemable rate-limit reset credits. */
40
73
  export interface ResetUsageAccount {
41
74
  label: string;
@@ -74,6 +107,8 @@ export class ResetUsageSelectorComponent extends OverlayPanel {
74
107
  #nativeItems: readonly NativeNode[] | undefined;
75
108
  #nativeHints: NativeNode | undefined;
76
109
  #nativeRoot: NativeNode | undefined;
110
+ #pickerRoot: NativeNode | undefined;
111
+ #pickerItems: readonly TspPickerItem[] | undefined;
77
112
 
78
113
  constructor(accounts: ResetUsageAccount[], onSelect: (account: ResetUsageAccount) => void, onCancel: () => void) {
79
114
  super("Spend a saved rate-limit reset", "zeta.overlay.reset-usage");
@@ -99,6 +134,7 @@ export class ResetUsageSelectorComponent extends OverlayPanel {
99
134
 
100
135
  #updateList(): void {
101
136
  this.#nativeRoot = undefined;
137
+ this.#pickerRoot = undefined;
102
138
  this.#listContainer.clear();
103
139
 
104
140
  const items = this.#menu.visibleItems;
@@ -165,21 +201,28 @@ export class ResetUsageSelectorComponent extends OverlayPanel {
165
201
  }
166
202
  }
167
203
 
168
- #confirmationMessage(account: ResetUsageAccount): string {
204
+ /** What a reset of `account` spends and covers, without key instructions. */
205
+ #confirmationNotes(account: ResetUsageAccount): { subject: string; notes: string[] } {
169
206
  const subject = account.credit?.title ? `“${account.credit.title}”` : "1 saved reset";
170
- const messages = [
171
- `Press ${formatKeyHint("enter")} again to spend ${subject} for ${account.label} (${account.providerLabel}).`,
172
- ];
207
+ const notes: string[] = [];
173
208
  if (account.credit?.program === "juniper_tide") {
174
- messages.push("This resets Claude's 5h session limit only; weekly limits stay unchanged.");
209
+ notes.push("This resets Claude's 5h session limit only; weekly limits stay unchanged.");
175
210
  } else if (account.credit?.clears?.length) {
176
- messages.push(`Covers ${account.credit.clears.map(formatUsageResetWindow).join(", ")}.`);
211
+ notes.push(`Covers ${account.credit.clears.map(formatUsageResetWindow).join(", ")}.`);
177
212
  }
178
213
  if (account.credit?.requiresLimit === false) {
179
- messages.push("Optional early use: this can be spent before the covered limit is reached.");
214
+ notes.push("Optional early use: this can be spent before the covered limit is reached.");
180
215
  }
181
- messages.push(`${editorKey("tui.select.cancel")} cancels.`);
182
- return messages.join(" ");
216
+ return { subject, notes };
217
+ }
218
+
219
+ #confirmationMessage(account: ResetUsageAccount): string {
220
+ const { subject, notes } = this.#confirmationNotes(account);
221
+ return [
222
+ `Press ${formatKeyHint("enter")} again to spend ${subject} for ${account.label} (${account.providerLabel}).`,
223
+ ...notes,
224
+ `${editorKey("tui.select.cancel")} cancels.`,
225
+ ].join(" ");
183
226
  }
184
227
 
185
228
  handleInput(keyData: string): void {
@@ -239,7 +282,8 @@ export class ResetUsageSelectorComponent extends OverlayPanel {
239
282
  }
240
283
  }
241
284
 
242
- override describe(): NativeNode {
285
+ override describe(cx: DescribeContext): NativeNode {
286
+ if (cx.supports("picker")) return this.#describePicker();
243
287
  if (this.#nativeRoot) return this.#nativeRoot;
244
288
  this.#nativeItems ??= this.#menu.visibleItems.map(account => {
245
289
  const redeemable = account.redeemableCount > 0;
@@ -282,18 +326,83 @@ export class ResetUsageSelectorComponent extends OverlayPanel {
282
326
  }
283
327
 
284
328
  /**
285
- * A click (or double-click) on an account highlights it and presses Enter:
286
- * moving to another account drops a pending confirmation, so spending a
287
- * reset still takes a second activation of the same account.
329
+ * The docked md picker: accounts as provider-marked cards with their resets
330
+ * as facts, the active account checked, unusable ones disabled with the
331
+ * reason; the first `Spend reset` turns the selected row into a confirm strip.
332
+ */
333
+ #describePicker(): NativeNode {
334
+ if (this.#pickerRoot) return this.#pickerRoot;
335
+ const items = (this.#pickerItems ??= this.#menu.visibleItems.map(account =>
336
+ accountPickerItem(account, this.#menu.adapter.getKey(account)),
337
+ ));
338
+ const pending = this.#menu.visibleItems.find(item => this.#menu.isPending(item));
339
+ // The strip asks the question over the row; what the reset covers reads in the subtitle meanwhile.
340
+ let confirm: TspPickerProps["confirm"] = null;
341
+ let subtitle: TspPickerProps["subtitle"] = "Spend a saved reset to refresh an account's rate limit now";
342
+ if (pending) {
343
+ const { subject, notes } = this.#confirmationNotes(pending);
344
+ confirm = { text: `Spend ${subject} on ${oneLine(pending.label)}?`, act: "confirm", label: "Spend" };
345
+ if (notes.length > 0) subtitle = oneLine(notes.join(" "));
346
+ }
347
+ if (this.#statusMessage) subtitle = [span(oneLine(this.#statusMessage), "warning")];
348
+ this.#pickerRoot = dockedPicker({
349
+ title: "Rate-limit resets",
350
+ subtitle,
351
+ icon: "refresh",
352
+ noun: "accounts",
353
+ size: "md",
354
+ layout: "cards",
355
+ preview: "none",
356
+ query: null,
357
+ columns: [
358
+ { id: "expires", priority: 1 },
359
+ { id: "resets", priority: 2 },
360
+ ],
361
+ items,
362
+ current: this.#menu.visibleItems
363
+ .filter(account => account.active)
364
+ .map(account => this.#menu.adapter.getKey(account)),
365
+ selected: this.#menu.selectedKey ?? null,
366
+ empty: "No provider accounts with saved resets",
367
+ actions: [pickerAction("confirm", "Spend reset", "enter", { primary: true }), CLOSE_ACTION],
368
+ confirm,
369
+ });
370
+ return this.#pickerRoot;
371
+ }
372
+
373
+ /** Point the menu at `key`; moving to another account drops a pending confirmation. */
374
+ #selectKey(key: string): boolean {
375
+ if (this.#menu.selectedKey === key) return true;
376
+ this.#menu.cancelConfirmation();
377
+ this.#menu.setSelectedKey(key);
378
+ if (this.#menu.selectedKey !== key) return false;
379
+ this.#statusMessage = undefined;
380
+ this.#updateList();
381
+ return true;
382
+ }
383
+
384
+ /**
385
+ * Picker: a row click selects, a double-click or `Spend reset` presses
386
+ * Enter (so spending still takes a second activation, on the confirm
387
+ * strip), the strip's `Keep` drops the confirmation and `Close` cancels.
388
+ * Card list: a click (or double-click) on an account highlights it and
389
+ * presses Enter.
288
390
  */
289
391
  handleNativeEvent(event: NativeUiEvent): void {
290
- if ((event.type !== "select" && event.type !== "activate") || event.key !== "list") return;
291
- if (this.#menu.selectedKey !== event.item) {
292
- this.#menu.cancelConfirmation();
293
- this.#menu.setSelectedKey(event.item);
294
- if (this.#menu.selectedKey !== event.item) return;
295
- this.#statusMessage = undefined;
392
+ const ev = pickerEvent(event, PICKER_KEY);
393
+ if (ev?.kind === "action") {
394
+ if (ev.act === "confirm") this.#activateSelection();
395
+ else if (ev.act === "cancel" && this.#menu.cancelConfirmation()) {
396
+ this.#statusMessage = undefined;
397
+ this.#updateList();
398
+ } else if (ev.act === "close" || ev.act === "cancel") this.#onCancelCallback();
399
+ return;
296
400
  }
297
- this.#activateSelection();
401
+ if (ev) {
402
+ if (this.#selectKey(ev.item) && ev.kind === "activate") this.#activateSelection();
403
+ return;
404
+ }
405
+ if ((event.type !== "select" && event.type !== "activate") || event.key !== "list") return;
406
+ if (this.#selectKey(event.item)) this.#activateSelection();
298
407
  }
299
408
  }
@@ -2,6 +2,8 @@ import { type SelectItem, SelectList, type SgrMouseEvent } from "../index";
2
2
  import { getSelectListTheme } from "../theme/theme";
3
3
  import { OverlayPanel } from "../chrome/overlay-box";
4
4
  import { routeSelectListMouseWithTopBorder } from "../chrome/select-list-mouse-routing";
5
+ import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node";
6
+ import { SelectListSheet } from "../native/picker";
5
7
 
6
8
  const ACCOUNT_SELECTOR_MAX_VISIBLE = 10;
7
9
 
@@ -19,9 +21,18 @@ export interface SessionPinAccount {
19
21
  label: string;
20
22
  }
21
23
 
24
+ /** What tells two accounts of one provider apart besides their label: the organization, project or host. */
25
+ function accountDetail(account: SessionPinAccount): string | undefined {
26
+ const parts = [account.orgName ?? account.orgId, account.projectId, account.enterpriseUrl].filter(
27
+ (part): part is string => typeof part === "string" && part.trim().length > 0 && !account.label.includes(part),
28
+ );
29
+ return parts.length > 0 ? parts.join(" · ") : undefined;
30
+ }
31
+
22
32
  /** Account picker opened by `/session pin` for the current model provider. */
23
33
  export class SessionAccountSelectorComponent extends OverlayPanel {
24
34
  #selectList: SelectList;
35
+ #sheet: SelectListSheet;
25
36
 
26
37
  constructor(
27
38
  providerName: string,
@@ -54,6 +65,31 @@ export class SessionAccountSelectorComponent extends OverlayPanel {
54
65
  };
55
66
  this.#selectList.onCancel = onCancel;
56
67
  this.addChild(this.#selectList);
68
+ // Natively the active account carries the picker's current check, so the
69
+ // row's detail is what tells accounts apart instead of "active for this session".
70
+ this.#sheet = new SelectListSheet(this.#selectList, {
71
+ title: `${providerName} account`,
72
+ subtitle: "Pin the account this session sends its requests with",
73
+ icon: "key-round",
74
+ noun: "accounts",
75
+ current: accounts.filter(account => account.active).map(account => String(account.credentialId)),
76
+ confirm: "Pin",
77
+ decorate: item => {
78
+ const account = accountsByValue.get(item.value);
79
+ if (!account) return undefined;
80
+ const detail = accountDetail(account);
81
+ return { mark: { text: account.email ?? account.label, seed: account.label }, detail: detail ?? "" };
82
+ },
83
+ });
84
+ }
85
+
86
+ override describe(cx: DescribeContext): NativeNode | null {
87
+ return cx.supports("picker") ? this.#sheet.describe() : super.describe(cx);
88
+ }
89
+
90
+ /** Picker pointer events drive the list exactly as its keys do. */
91
+ handleNativeEvent(event: NativeUiEvent): void {
92
+ this.#sheet.handle(event);
57
93
  }
58
94
 
59
95
  /** Forward keyboard navigation and cancellation when the wrapper owns focus. */
@@ -4,9 +4,9 @@ import { matchesSelectCancel } from "../keybinding-matchers";
4
4
  import { OverlayPanel, PanelDivider, PanelRows } from "../chrome/overlay-box";
5
5
  import { formatKeyHints } from "../app-keybindings";
6
6
  import { editorKey } from "../chrome/keybinding-hints";
7
- import type { TspProps, TspSpan } from "@linxiraos/pi-wire";
7
+ import type { TspSpan } from "@linxiraos/pi-wire";
8
8
  import { formatNumber } from "@linxiraos/pi-utils";
9
- import { col, node, row, span, text } from "../native/describe";
9
+ import { col, node, span, text } from "../native/describe";
10
10
  import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
11
11
  import { actionBar, actionButton } from "../native/overlay";
12
12
  import { plainText, styledSpans } from "../native/spans";
@@ -57,29 +57,50 @@ function joined(spans: readonly TspSpan[]): string {
57
57
  return spans.map(part => part.t).join("");
58
58
  }
59
59
 
60
+ /** A figure the stats grid can right-align: `182,440`, `0.4127`, `$1.20`, `21%`. */
61
+ const NUMERIC = /^[$€£]?\d[\d,]*(?:\.\d+)?%?$/;
62
+
63
+ /** A report key or heading in sentence case: `Tool Calls` → `Tool calls`; acronyms (`MCP`, `ACU`) stay. */
64
+ function sentenceCase(label: string): string {
65
+ let first = true;
66
+ return label.replace(/[A-Za-z]+/g, word => {
67
+ if (first) {
68
+ first = false;
69
+ return word;
70
+ }
71
+ return /^[A-Z][a-z]+$/.test(word) ? word.toLowerCase() : word;
72
+ });
73
+ }
74
+
60
75
  /**
61
- * A value with a copy button after it: a click anywhere on the line copies its
62
- * text (the terminal-local `copy` action takes the leaf text; the icon has none).
76
+ * One line of the identity block: a sans label, then the value. With `copy`
77
+ * (its tooltip) the value and a copy glyph after it are one target: a click
78
+ * copies the value (the terminal-local `copy` action takes the leaf text; the
79
+ * icon has none).
63
80
  */
64
- function copyLine(
65
- spans: TspSpan[],
66
- title: string,
67
- key: string,
68
- props: Omit<TspProps<"text">, "text" | "spans"> = {},
69
- ): NativeNode {
81
+ function identityField(label: string, value: TspSpan[], key: string, copy?: string): NativeNode {
82
+ const shown = text(value, { truncate: "middle", grow: 1, shrink: 1 });
70
83
  return node(
71
84
  "row",
72
- {
73
- gap: "sm",
74
- align: "center",
75
- grow: 1,
76
- role: "zeta.info.copy",
77
- actions: { click: "copy" },
78
- title,
79
- },
85
+ { gap: "md", align: "center", role: "zeta.info.field" },
80
86
  [
81
- text(spans, { ...props, grow: 1, shrink: 1 }),
82
- node("icon", { name: "copy", role: "zeta.info.copy.ic", aria: title }),
87
+ text([span(label)], { role: "zeta.info.label", wrap: "none", shrink: 0 }),
88
+ copy
89
+ ? node(
90
+ "row",
91
+ {
92
+ gap: "sm",
93
+ align: "center",
94
+ grow: 1,
95
+ shrink: 1,
96
+ role: "zeta.info.copy",
97
+ actions: { click: "copy" },
98
+ title: copy,
99
+ },
100
+ [shown, node("icon", { name: "copy", role: "zeta.info.copy.ic", aria: copy })],
101
+ "value",
102
+ )
103
+ : shown,
83
104
  ],
84
105
  key,
85
106
  );
@@ -165,83 +186,185 @@ export class SessionInfoOverlay implements Component {
165
186
  }
166
187
 
167
188
  /**
168
- * The sheet body (the overlay is the frame): the session id in mono
169
- * (click copies), each report heading as a section of key/value rows, a
170
- * context meter after the leading block, and a Close button (Esc).
189
+ * The sheet body (the overlay is the frame), in three tiers: who the session
190
+ * is (the id and file as copyable mono fields), how full its context window
191
+ * is (a meter under a right-aligned figure), then the report's sections as
192
+ * headed groups. Groups of plain counts (Messages, Tokens, Cost) share a
193
+ * two-column stats grid with right-aligned figures and their Total set off
194
+ * last; the rest (Provider, servers) are key/value rows. A Close button (Esc)
195
+ * ends it.
171
196
  */
172
197
  describe(cx: DescribeContext): NativeNode {
173
198
  const meter = cx.supports("meter");
174
199
  if (this.#nativeNode?.meter === meter) return this.#nativeNode.node;
200
+ // The report prints the append-only context mode after the message counts;
201
+ // it is a provider setting, so it joins the Provider group when there is one.
202
+ const parsed = parseKvReport(this.#infoSource);
203
+ const hasProvider = parsed.some(section => section.head === "Provider");
204
+ const moved: KvReportEntry[] = [];
205
+ const sections = parsed.map(section => ({
206
+ head: section.head,
207
+ entries: section.entries.filter(entry => {
208
+ if (!hasProvider || section.head === "Provider" || !("k" in entry) || entry.k !== "Append-Only")
209
+ return true;
210
+ moved.push(entry);
211
+ return false;
212
+ }),
213
+ }));
214
+ sections.find(section => section.head === "Provider")?.entries.push(...moved);
175
215
  const body: NativeChild[] = [];
176
- for (const [index, section] of parseKvReport(this.#infoSource).entries()) {
177
- const children: NativeChild[] = [];
178
- let items: { k: string; v: TspSpan[] }[] = [];
179
- const flush = (): void => {
180
- if (items.length === 0) return;
181
- children.push(node("kv", { items, layout: "grid", role: "zeta.info.kv" }));
182
- items = [];
183
- };
184
- for (const entry of section.entries) {
185
- if (!("k" in entry)) {
186
- flush();
187
- children.push(text(entry.line, { wrap: "word" }));
188
- } else if (section.head === undefined && entry.k === "ID") {
189
- // The id leads the sheet, once, as a copyable mono line.
190
- body.push(
191
- copyLine([span(joined(entry.v), "mono muted")], "Copy session ID", "id", { truncate: "middle" }),
192
- );
193
- } else if (section.head === undefined && entry.k === "File") {
194
- flush();
195
- children.push(
196
- row(
197
- [
198
- text([span("File", "muted")], { wrap: "none", shrink: 0 }),
199
- copyLine([span(joined(entry.v), "mono")], "Copy file path", "path", { wrap: "char" }),
200
- ],
201
- {
202
- gap: "md",
203
- align: "start",
204
- role: "zeta.info.file",
205
- },
206
- ),
207
- );
208
- } else {
209
- items.push({ k: entry.k, v: entry.v });
216
+ const stats: { head: string; key: string; items: { k: string; v: TspSpan[] }[] }[] = [];
217
+ const flushStats = (): void => {
218
+ if (stats.length > 0) body.push(this.#describeStats(stats.splice(0)));
219
+ };
220
+ for (const [index, { head, entries }] of sections.entries()) {
221
+ if (head === undefined) {
222
+ // The session's identity: its id, then its file (copyable when it is one).
223
+ const identity: NativeChild[] = [];
224
+ const rest: NativeChild[] = [];
225
+ for (const entry of entries) {
226
+ if (!("k" in entry)) rest.push(text(entry.line, { wrap: "word", role: "zeta.info.note" }));
227
+ else if (entry.k === "ID")
228
+ identity.unshift(identityField("ID", [span(joined(entry.v), "mono")], "id", "Copy session ID"));
229
+ else if (entry.k === "File") {
230
+ const path = joined(entry.v);
231
+ identity.push(
232
+ path.includes("/")
233
+ ? identityField("File", [span(path, "path")], "file", `Copy ${path}`)
234
+ : identityField("File", [span(path, "muted")], "file"),
235
+ );
236
+ } else rest.push(node("kv", { items: [{ k: entry.k, v: entry.v }], role: "zeta.info.kv" }));
210
237
  }
211
- }
212
- flush();
213
- if (section.head === undefined) {
214
- body.push(node("col", { gap: "sm" }, children, `s${index}`));
238
+ if (identity.length > 0) body.push(node("col", { role: "zeta.info.identity" }, identity, "identity"));
239
+ body.push(...rest);
215
240
  const context = this.#context;
216
241
  if (context && context.contextWindow > 0) body.push(this.#describeContext(context, meter));
217
- } else {
218
- body.push(node("section", { head: section.head }, children, `s${index}`));
242
+ continue;
243
+ }
244
+ const pairs = entries.filter((entry): entry is { k: string; v: TspSpan[] } => "k" in entry);
245
+ if (
246
+ pairs.length > 0 &&
247
+ pairs.length === entries.length &&
248
+ pairs.every(p => NUMERIC.test(joined(p.v).trim()))
249
+ ) {
250
+ const cost = head === "Cost";
251
+ stats.push({
252
+ head,
253
+ key: `s${index}`,
254
+ items: pairs.map(p => ({
255
+ k: sentenceCase(p.k),
256
+ v: [span(cost && p.k === "Total" ? `$${joined(p.v).trim()}` : joined(p.v).trim(), "mono")],
257
+ })),
258
+ });
259
+ continue;
219
260
  }
261
+ flushStats();
262
+ body.push(
263
+ node(
264
+ "section",
265
+ { head: sentenceCase(head), role: "zeta.info.group" },
266
+ this.#describeEntries(entries, head === "Provider"),
267
+ `s${index}`,
268
+ ),
269
+ );
220
270
  }
271
+ flushStats();
221
272
  body.push(actionBar([null, actionButton("Close", "close", { keys: "escape" })]));
222
273
  const described = col(body, { gap: "md" });
223
274
  this.#nativeNode = { meter, node: described };
224
275
  return described;
225
276
  }
226
277
 
278
+ /**
279
+ * Key/value rows, free lines as notes between them. The provider's values are
280
+ * machine strings (`mono`); elsewhere (LSP, MCP servers) the keys are names
281
+ * (role `zeta.info.names`, keys in mono).
282
+ */
283
+ #describeEntries(entries: readonly KvReportEntry[], mono: boolean): NativeChild[] {
284
+ const children: NativeChild[] = [];
285
+ let items: { k: string; v: TspSpan[] }[] = [];
286
+ const flush = (): void => {
287
+ if (items.length === 0) return;
288
+ children.push(node("kv", { items, layout: "grid", role: mono ? "zeta.info.kv" : "zeta.info.names" }));
289
+ items = [];
290
+ };
291
+ for (const entry of entries) {
292
+ if (!("k" in entry)) {
293
+ flush();
294
+ children.push(text(entry.line, { wrap: "word", role: "zeta.info.note" }));
295
+ continue;
296
+ }
297
+ const v = mono ? entry.v.map(part => ({ ...part, s: part.s ? `${part.s} mono` : "mono" })) : entry.v;
298
+ items.push({ k: mono ? sentenceCase(entry.k) : entry.k, v });
299
+ }
300
+ flush();
301
+ return children;
302
+ }
303
+
304
+ /**
305
+ * Count groups as a two-column grid that stacks when narrow: each group goes
306
+ * to the shorter column (report order holds when stacked), its Total last
307
+ * over a hairline.
308
+ */
309
+ #describeStats(groups: readonly { head: string; key: string; items: { k: string; v: TspSpan[] }[] }[]): NativeNode {
310
+ const columns: { lines: number; children: NativeChild[] }[] = [
311
+ { lines: 0, children: [] },
312
+ { lines: 0, children: [] },
313
+ ];
314
+ for (const g of groups) {
315
+ const total = g.items.length > 1 ? g.items.find(item => item.k === "Total") : undefined;
316
+ const items = total ? g.items.filter(item => item !== total) : g.items;
317
+ const children: NativeChild[] = [node("kv", { items, layout: "grid", role: "zeta.info.nums" })];
318
+ if (total) children.push(node("kv", { items: [total], layout: "grid", role: "zeta.info.total" }));
319
+ // Ties go right, so the stacked order (left, then right) stays the report's.
320
+ const column = columns[0].lines === 0 || columns[0].lines < columns[1].lines ? columns[0] : columns[1];
321
+ column.lines += g.items.length + 2;
322
+ column.children.push(
323
+ node("section", { head: sentenceCase(g.head), role: "zeta.info.group" }, children, g.key),
324
+ );
325
+ }
326
+ return node(
327
+ "row",
328
+ { align: "start", wrap: true, role: "zeta.info.stats" },
329
+ columns
330
+ .filter(column => column.children.length > 0)
331
+ .map((column, i) => node("col", { role: "zeta.info.column" }, column.children, `c${i}`)),
332
+ "stats",
333
+ );
334
+ }
335
+
336
+ /** The context window: a head row (label, `41K / 200K`, the share) over a full-width meter. */
227
337
  #describeContext(context: ContextUsage, meter: boolean): NativeNode {
228
338
  const value = Math.min(1, Math.max(0, context.percent / 100));
229
- const figure = `${formatNumber(context.tokens)} / ${formatNumber(context.contextWindow).toLowerCase()} · ${Math.round(context.percent)}%`;
339
+ const title = `${context.tokens.toLocaleString("en-US")} of ${context.contextWindow.toLocaleString("en-US")} tokens`;
230
340
  return node(
231
- "row",
232
- { gap: "sm", align: "center" },
341
+ "col",
342
+ { role: "zeta.info.context", title },
233
343
  [
234
- text([span("Context", "muted")]),
344
+ node(
345
+ "row",
346
+ { align: "baseline", role: "zeta.info.contextHead" },
347
+ [
348
+ text([span("Context window")], { grow: 1, shrink: 1, truncate: "end" }),
349
+ text(
350
+ [
351
+ span(`${formatNumber(context.tokens)} / ${formatNumber(context.contextWindow)}`, "mono muted"),
352
+ span(" "),
353
+ span(`${Math.round(context.percent)}%`, "mono strong"),
354
+ ],
355
+ { wrap: "none", shrink: 0 },
356
+ ),
357
+ ],
358
+ "head",
359
+ ),
235
360
  meter
236
- ? node("meter", {
237
- value,
238
- style: "bar",
239
- size: "md",
240
- thresholds: { warn: 0.7, bad: 0.9 },
241
- grow: 1,
242
- })
243
- : node("progress", { value, grow: 1 }),
244
- text([span(figure, "mono")]),
361
+ ? node(
362
+ "meter",
363
+ { value, style: "bar", size: "md", thresholds: { warn: 0.7, bad: 0.9 }, title },
364
+ undefined,
365
+ "meter",
366
+ )
367
+ : node("progress", { value }, undefined, "meter"),
245
368
  ],
246
369
  "context",
247
370
  );