@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
@@ -34,7 +34,16 @@ import { sanitizeDisplayWarnings } from "../render/render-utils";
34
34
  import { formatKeyHint } from "../app-keybindings";
35
35
  import { editorKey, editorKeys } from "../chrome/keybinding-hints";
36
36
  import { HookEditorComponent } from "./hook-editor";
37
- import { buildBrowserItems, ModelBrowser, type ModelBrowserSource, sortModelItems } from "./model-browser";
37
+ import {
38
+ buildBrowserItems,
39
+ MODEL_PICKER_COLUMNS,
40
+ ModelBrowser,
41
+ type ModelBrowserItem,
42
+ type ModelBrowserSource,
43
+ type ModelPickerCatalogue,
44
+ type ModelPickerOrder,
45
+ sortModelItems,
46
+ } from "./model-browser";
38
47
  import { bottomBorder, divider, dividerSplit, PanelRows, row, topBorder, topBorderSplit } from "../chrome/overlay-box";
39
48
  import { isLayoutMouseRoutable } from "../components/layout/geometry";
40
49
  import { SplitPane } from "../components/layout/split-pane";
@@ -42,7 +51,16 @@ import { Stack } from "../components/layout/stack";
42
51
  import type { TspPrefsProps, TspPrefsRow, TspSpan } from "@linxiraos/pi-wire";
43
52
  import { col, node, span, text } from "../native/describe";
44
53
  import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
45
- import { actionHint, hintsRow, type NativeHint, overlayCard } from "../native/overlay";
54
+ import { actionBar, actionButton, actionHint, hintsRow, type NativeHint, overlayCard } from "../native/overlay";
55
+ import {
56
+ CLOSE_ACTION,
57
+ pickerAction,
58
+ pickerEvent,
59
+ routeSelectListPicker,
60
+ type SelectPickerOptions,
61
+ selectListPicker,
62
+ } from "../native/picker";
63
+ import { getThinkingLevelMetadata, parseThinkingLevel } from "../thinking";
46
64
 
47
65
  export const ADVISOR_REVIEW_MODES = ["turn", "agent-end"] as const;
48
66
  export const ADVISOR_SYNC_BACKLOG_MODES = ["off", "1", "3", "5", "strict"] as const;
@@ -205,6 +223,40 @@ function formatAdvisorTools(tools: readonly string[] | undefined, emptyLabel: st
205
223
  /** Choice value for "no per-advisor override"; matches no {@link ADVISOR_SYNC_BACKLOG_MODES} entry. */
206
224
  const SYNC_BACKLOG_INHERIT = "inherit";
207
225
 
226
+ /** The tools list's terminal-only "Done" row; the picker applies with its own action instead. */
227
+ const TOOLS_DONE = "__done";
228
+
229
+ /**
230
+ * A picker editor as a component of its own: a component's root picker stays
231
+ * where it is described, the prefs page's editor slot, where a picker
232
+ * described inside the page would be hoisted into `layer` under the sheet.
233
+ */
234
+ class EditorSheet implements Component {
235
+ node: NativeNode | undefined;
236
+ readonly #route: (event: NativeUiEvent) => void;
237
+
238
+ constructor(route: (event: NativeUiEvent) => void) {
239
+ this.#route = route;
240
+ }
241
+
242
+ render(): readonly string[] {
243
+ return [];
244
+ }
245
+
246
+ describe(): NativeNode | null {
247
+ return this.node ?? null;
248
+ }
249
+
250
+ handleNativeEvent(event: NativeUiEvent): void {
251
+ this.#route(event);
252
+ }
253
+ }
254
+
255
+ /** Whether two memo dependency lists hold the same values by identity. */
256
+ function sameDeps(a: readonly unknown[], b: readonly unknown[]): boolean {
257
+ return a.length === b.length && a.every((value, i) => value === b[i]);
258
+ }
259
+
208
260
  /** One-line cadence label: mode plus interval. */
209
261
  function formatReviewCadence(advisor: AdvisorConfig): string {
210
262
  const reviewInterval = advisor.reviewInterval ?? 1;
@@ -297,7 +349,15 @@ export class AdvisorConfigOverlayComponent implements Component {
297
349
 
298
350
  /** The advisor the detail screen (and its editors) edits; -1 on the roster or the shared instructions. */
299
351
  #detailIndex = -1;
300
- #nativePrefs: { signature: string; editor: Component | undefined; node: NativeNode } | undefined;
352
+ #nativePrefs: { signature: string; editor: NativeNode | undefined; node: NativeNode } | undefined;
353
+ /** The editor over the prefs page, rebuilt only when one of `deps` changes identity. */
354
+ #nativeEditor: { deps: readonly unknown[]; node: NativeNode } | undefined;
355
+ /** The model screen's catalogue and the advisor's current model among it (picker `current`). */
356
+ #modelItems: ModelBrowserItem[] = [];
357
+ #modelCurrent: string | undefined;
358
+ /** Picker sheet options of the current list screen (thinking, review mode, sync backlog, tools). */
359
+ #sheetOptions: SelectPickerOptions | undefined;
360
+ readonly #editorSheet = new EditorSheet(event => this.#routeEditorPicker(event));
301
361
  #screen: Screen = "list";
302
362
  /** The interactive element for the current screen. */
303
363
  #active: Component = new SelectList([], 1, getSelectListTheme());
@@ -587,7 +647,7 @@ export class AdvisorConfigOverlayComponent implements Component {
587
647
  this.#screen === "sync-backlog" ||
588
648
  this.#screen === "tools" ||
589
649
  this.#screen === "instructions";
590
- const editor = overlay ? this.#active : undefined;
650
+ const editor = overlay ? this.#prefsEditor() : undefined;
591
651
  const focus =
592
652
  this.#screen === "detail" && this.#active instanceof SelectList
593
653
  ? (this.#active.getSelectedItem()?.value ?? null)
@@ -606,12 +666,137 @@ export class AdvisorConfigOverlayComponent implements Component {
606
666
  const signature = JSON.stringify(props);
607
667
  const prev = this.#nativePrefs;
608
668
  if (prev && prev.signature === signature && prev.editor === editor) return prev.node;
609
- const children: NativeChild[] = editor ? [col([editor], { role: "zeta.prefs.editor" })] : [];
669
+ let children: NativeChild[] = [];
670
+ if (editor?.k === "picker") {
671
+ this.#editorSheet.node = editor;
672
+ children = [this.#editorSheet];
673
+ } else if (editor) {
674
+ children = [editor];
675
+ }
610
676
  const root = node("prefs", props, children);
611
677
  this.#nativePrefs = { signature, editor, node: root };
612
678
  return root;
613
679
  }
614
680
 
681
+ /**
682
+ * The editor over the prefs page. Choices are picker sheets on `/model`'s
683
+ * anatomy (search, rows, the highlighted model's facts, one primary
684
+ * action); the instructions and review-interval editors sit in the page's
685
+ * editor card.
686
+ */
687
+ #prefsEditor(): NativeNode {
688
+ const active = this.#active;
689
+ const memo = this.#nativeEditor;
690
+ if (active instanceof ModelBrowser) {
691
+ const order = active.pickerOrder({ providers: false, rest: active.query.trim() ? "Matches" : "All" });
692
+ const catalogue = active.pickerItems(this.#modelItems);
693
+ const preview = active.pickerPreview("compact", this.#modelCurrent);
694
+ const deps = [active, order, catalogue, preview, active.query, active.cursor, active.pickerSelected];
695
+ if (memo && sameDeps(memo.deps, deps)) return memo.node;
696
+ const sheet = this.#modelSheet(active, order, catalogue, preview);
697
+ this.#nativeEditor = { deps, node: sheet };
698
+ return sheet;
699
+ }
700
+ const options = this.#sheetOptions;
701
+ if (active instanceof SelectList && options) {
702
+ const view = active.pickerView();
703
+ const deps = [active, view.items, view.selected, view.pending, view.query, view.cursor];
704
+ if (memo && sameDeps(memo.deps, deps)) return memo.node;
705
+ const props = selectListPicker(active, options);
706
+ const sheet = node(
707
+ "picker",
708
+ this.#screen === "tools"
709
+ ? {
710
+ ...props,
711
+ items: props.items?.filter(item => item.id !== TOOLS_DONE),
712
+ actions: [
713
+ pickerAction("confirm", "Toggle", "enter"),
714
+ pickerAction("apply", "Apply", "escape", { primary: true, end: true }),
715
+ ],
716
+ }
717
+ : props,
718
+ );
719
+ this.#nativeEditor = { deps, node: sheet };
720
+ return sheet;
721
+ }
722
+ if (memo && sameDeps(memo.deps, [active])) return memo.node;
723
+ const content = this.#screen === "review-interval" ? this.#intervalCard(active) : active;
724
+ const card = col([content], { role: "zeta.prefs.editor" });
725
+ this.#nativeEditor = { deps: [active], node: card };
726
+ return card;
727
+ }
728
+
729
+ #modelSheet(
730
+ browser: ModelBrowser,
731
+ order: ModelPickerOrder,
732
+ catalogue: ModelPickerCatalogue,
733
+ preview: readonly NativeChild[],
734
+ ): NativeNode {
735
+ const name = this.#doc.advisors[this.#detailIndex]?.name || "This advisor";
736
+ const fallback = this.#defaultModelLabel ?? "the advisor role default";
737
+ return node(
738
+ "picker",
739
+ {
740
+ title: "Advisor model",
741
+ subtitle: `${name} reviews with this model. Unset, it uses ${fallback}.`,
742
+ icon: "model",
743
+ noun: "models",
744
+ size: "md",
745
+ layout: "rows",
746
+ preview: "below",
747
+ query: browser.query,
748
+ cursor: browser.cursor,
749
+ placeholder: "Search models…",
750
+ columns: MODEL_PICKER_COLUMNS,
751
+ ...catalogue,
752
+ order: order.order,
753
+ ...(order.hits ? { hits: order.hits } : {}),
754
+ selected: browser.pickerSelected,
755
+ ...(this.#modelCurrent ? { current: [this.#modelCurrent] } : {}),
756
+ total: browser.baseCount,
757
+ empty: "No models available",
758
+ actions: [
759
+ pickerAction("use", "Use for advisor", "enter", { primary: true }),
760
+ browser.query.length > 0 ? { ...CLOSE_ACTION, label: "Clear search" } : CLOSE_ACTION,
761
+ ],
762
+ },
763
+ preview,
764
+ );
765
+ }
766
+
767
+ /** The review-interval field as a titled card: what the number means, the field, Cancel and Save. */
768
+ #intervalCard(input: Component): NativeNode {
769
+ const name = this.#doc.advisors[this.#detailIndex]?.name || "this advisor";
770
+ return overlayCard("zeta.overlay.advisor.interval", `Review interval · ${name}`, [
771
+ text([span("Review every Nth eligible update; skipped updates join the next review.", "muted")], {
772
+ wrap: "word",
773
+ }),
774
+ input,
775
+ actionBar([
776
+ null,
777
+ actionButton("Cancel", "field-cancel", { keys: "escape" }),
778
+ actionButton("Save", "field-save", { keys: "enter", tone: "accent" }),
779
+ ]),
780
+ ]);
781
+ }
782
+
783
+ /** A pointer event on the editor's picker sheet, on the path of the key it stands for. */
784
+ #routeEditorPicker(event: NativeUiEvent): void {
785
+ const picked = pickerEvent(event);
786
+ if (!picked) return;
787
+ const active = this.#active;
788
+ if (active instanceof ModelBrowser) {
789
+ if (picked.kind !== "action") active.routePickerItem(picked.item, picked.kind === "activate");
790
+ else if (picked.act === "close") active.handleCancel();
791
+ else if (picked.act === "use" && active.pickerSelected) active.routePickerItem(active.pickerSelected, true);
792
+ } else if (active instanceof SelectList) {
793
+ // The tools list applies on cancel, like its Esc.
794
+ if (picked.kind === "action" && picked.act === "apply") active.onCancel?.();
795
+ else routeSelectListPicker(active, event);
796
+ }
797
+ this.#cb.requestRender();
798
+ }
799
+
615
800
  /**
616
801
  * Native page events, each through the roster/detail selections the keys
617
802
  * make: a page opens that advisor (or the shared instructions), a field
@@ -619,6 +804,14 @@ export class AdvisorConfigOverlayComponent implements Component {
619
804
  * or the page cancels like Esc.
620
805
  */
621
806
  handleNativeEvent(event: NativeUiEvent): void {
807
+ // The review-interval card's buttons, on its Enter and Esc paths.
808
+ if (event.type === "action" && (event.act === "field-save" || event.act === "field-cancel")) {
809
+ const input = this.#active;
810
+ if (!(input instanceof Input)) return;
811
+ if (event.act === "field-save") input.onSubmit?.(input.getValue());
812
+ else input.onEscape?.();
813
+ return;
814
+ }
622
815
  if (event.key !== "") return;
623
816
  const index = this.#pageIndex();
624
817
  switch (event.type) {
@@ -979,9 +1172,11 @@ export class AdvisorConfigOverlayComponent implements Component {
979
1172
 
980
1173
  // ───────────────────────────── screens ───────────────────────────
981
1174
 
982
- #setScreen(screen: Screen, active: Component, footerHint: string): void {
1175
+ /** `sheet`: how a list screen describes as a picker over the prefs page. */
1176
+ #setScreen(screen: Screen, active: Component, footerHint: string, sheet?: SelectPickerOptions): void {
983
1177
  this.#screen = screen;
984
1178
  this.#active = active;
1179
+ this.#sheetOptions = sheet;
985
1180
  this.#footerHint = footerHint;
986
1181
  this.#previewScroll = 0;
987
1182
  this.#cb.requestRender();
@@ -1155,6 +1350,8 @@ export class AdvisorConfigOverlayComponent implements Component {
1155
1350
  }
1156
1351
 
1157
1352
  #onDetailSelect(index: number, field: string): void {
1353
+ // Editors opened straight from the prefs page edit this advisor too.
1354
+ this.#detailIndex = index;
1158
1355
  switch (field) {
1159
1356
  case "toggleEnabled": {
1160
1357
  const a = this.#doc.advisors[index];
@@ -1232,11 +1429,19 @@ export class AdvisorConfigOverlayComponent implements Component {
1232
1429
  }
1233
1430
  const items = buildBrowserItems(models);
1234
1431
  sortModelItems(items, { mruOrder });
1432
+ // The configured model, whatever thinking suffix it carries, is the picker's current row.
1433
+ const model = this.#doc.advisors[index]?.model?.trim();
1434
+ const current = model
1435
+ ? items.find(item => model === item.selector || model.startsWith(`${item.selector}:`))?.selector
1436
+ : undefined;
1437
+ this.#modelItems = items;
1438
+ this.#modelCurrent = current;
1235
1439
 
1236
1440
  const picker = new ModelBrowser(this.#deps.browserSource, {});
1237
1441
  picker.setMruOrder(mruOrder);
1238
1442
  picker.setPerfStats(this.#deps.browserSource.modelPerf);
1239
1443
  picker.setItems(items);
1444
+ if (current) picker.selectSelector(current);
1240
1445
  picker.onActivate = item => {
1241
1446
  const efforts = getSupportedEfforts(item.model);
1242
1447
  if (efforts.length === 0) {
@@ -1259,6 +1464,11 @@ export class AdvisorConfigOverlayComponent implements Component {
1259
1464
  const items: SelectItem[] = [{ value: "", label: "(model default thinking)" }];
1260
1465
  for (const effort of efforts) items.push({ value: effort, label: effort });
1261
1466
  const list = new SelectList(items, Math.max(1, items.length), getSelectListTheme());
1467
+ // The level the advisor already runs this model at, when it is this model.
1468
+ const model = this.#doc.advisors[index]?.model?.trim();
1469
+ const current =
1470
+ model === selector ? "" : model?.startsWith(`${selector}:`) ? model.slice(selector.length + 1) : undefined;
1471
+ if (current !== undefined) list.setSelectedValue(current);
1262
1472
  list.onSelect = item => {
1263
1473
  // Values are supported efforts or the empty model-default choice.
1264
1474
  this.#doc.advisors[index].model = item.value ? `${selector}:${item.value}` : selector;
@@ -1270,6 +1480,23 @@ export class AdvisorConfigOverlayComponent implements Component {
1270
1480
  "thinking",
1271
1481
  list,
1272
1482
  `Thinking effort for ${selector} · ${editorKey("tui.select.confirm")} / click pick · ${editorKey("tui.select.cancel")} back`,
1483
+ {
1484
+ title: "Thinking level",
1485
+ subtitle: `How hard ${this.#doc.advisors[index]?.name || "the advisor"} reasons on ${selector}.`,
1486
+ icon: "thinking",
1487
+ noun: "levels",
1488
+ current: current === undefined ? [] : [current],
1489
+ confirm: "Use level",
1490
+ decorate: item => {
1491
+ if (!item.value) return { label: "Model default", detail: "No level; the provider decides." };
1492
+ const level = parseThinkingLevel(item.value);
1493
+ const dot = `thinking${item.value.charAt(0).toUpperCase()}${item.value.slice(1)}`;
1494
+ return {
1495
+ detail: level ? getThinkingLevelMetadata(level).description : undefined,
1496
+ chips: [{ text: "", dot }],
1497
+ };
1498
+ },
1499
+ },
1273
1500
  );
1274
1501
  }
1275
1502
  #showReviewModePicker(index: number): void {
@@ -1299,6 +1526,14 @@ export class AdvisorConfigOverlayComponent implements Component {
1299
1526
  "review-mode",
1300
1527
  list,
1301
1528
  `${editorKey("tui.select.confirm")} / click choose review mode · ${editorKey("tui.select.cancel")} back`,
1529
+ {
1530
+ title: "Review mode",
1531
+ subtitle: `When ${advisor.name || "the advisor"} reviews the session.`,
1532
+ icon: "review",
1533
+ noun: "modes",
1534
+ current: [current],
1535
+ decorate: item => ({ label: item.value }),
1536
+ },
1302
1537
  );
1303
1538
  }
1304
1539
 
@@ -1361,6 +1596,14 @@ export class AdvisorConfigOverlayComponent implements Component {
1361
1596
  "sync-backlog",
1362
1597
  list,
1363
1598
  `${editorKey("tui.select.confirm")} / click choose catch-up policy · ${editorKey("tui.select.cancel")} back`,
1599
+ {
1600
+ title: "Sync backlog",
1601
+ subtitle: `How long the session waits for ${advisor.name || "the advisor"}'s pending reviews.`,
1602
+ icon: "time",
1603
+ noun: "policies",
1604
+ current: [current ?? SYNC_BACKLOG_INHERIT],
1605
+ decorate: item => ({ label: item.value }),
1606
+ },
1364
1607
  );
1365
1608
  }
1366
1609
 
@@ -1370,7 +1613,7 @@ export class AdvisorConfigOverlayComponent implements Component {
1370
1613
  value: name,
1371
1614
  label: `${selected.has(name) ? "[x]" : "[ ]"} ${name}`,
1372
1615
  }));
1373
- items.push({ value: "__done", label: "Done" });
1616
+ items.push({ value: TOOLS_DONE, label: "Done" });
1374
1617
  const list = new SelectList(items, Math.max(1, items.length), getSelectListTheme());
1375
1618
  list.setSelectedIndex(cursor);
1376
1619
  let cursorIndex = cursor;
@@ -1378,7 +1621,7 @@ export class AdvisorConfigOverlayComponent implements Component {
1378
1621
  cursorIndex = items.findIndex(i => i.value === item.value);
1379
1622
  };
1380
1623
  list.onSelect = item => {
1381
- if (item.value === "__done") {
1624
+ if (item.value === TOOLS_DONE) {
1382
1625
  this.#doc.advisors[index].tools = commitTools(selected, all, this.#deps.defaultToolNames);
1383
1626
  this.#dirty = true;
1384
1627
  this.#showDetail(index);
@@ -1397,6 +1640,14 @@ export class AdvisorConfigOverlayComponent implements Component {
1397
1640
  "tools",
1398
1641
  list,
1399
1642
  `${editorKey("tui.select.confirm")} / click toggle · select Done or ${editorKey("tui.select.cancel")} to apply (empty = no tools; read/grep/glob = default)`,
1643
+ {
1644
+ title: "Tools",
1645
+ subtitle: `What ${this.#doc.advisors[index]?.name || "the advisor"} may call. None grants no tools; read, grep and glob are the default.`,
1646
+ icon: "tools",
1647
+ noun: "tools",
1648
+ current: [...selected],
1649
+ decorate: item => ({ label: item.value }),
1650
+ },
1400
1651
  );
1401
1652
  }
1402
1653
 
@@ -1,6 +1,9 @@
1
1
  import type { AgentMetricsSummary, AgentRecordLike, AgentStatus } from "./agent-hub-types";
2
2
  import type { ObservableSession } from "./session-observer-registry";
3
3
  export type AgentMetrics = AgentMetricsSummary;
4
+ export interface AgentCostSnapshot {
5
+ cost: number | undefined;
6
+ }
4
7
  export interface AggregateMetrics extends AgentMetrics {
5
8
  reportedAgents: number;
6
9
  /** Rows whose duration is an observer-measured active runtime. */
@@ -17,14 +20,16 @@ export declare const STATUS_ORDER: Record<AgentStatus, number>;
17
20
  export declare function progressMetrics(observed: ObservableSession | undefined): AgentMetrics | undefined;
18
21
  /** Live session used to cache a row's direct assistant usage, including with observer progress. */
19
22
  export declare function hubFallbackStatsSession<TRecord extends AgentRecordLike>(ref: TRecord, observed: ObservableSession | undefined): NonNullable<TRecord["session"]> | undefined;
20
- /**
21
- * Preserve progress/history for non-cost metrics, overlaying cumulative direct-session cost.
22
- * Persisted `history.metrics.cost` is a leaf-scoped snapshot, so it never overrides live
23
- * progress; only the cumulative direct-cost sources do.
24
- */
23
+ /** Full Agent Hub row metrics; see {@link projectRowMetrics} for source precedence. */
25
24
  export declare function hubRowMetrics<TRecord extends AgentRecordLike>(ref: TRecord, observed: ObservableSession | undefined, sessionMetrics: WeakMap<object, {
26
25
  metrics: AgentMetrics | undefined;
27
26
  }>): AgentMetrics | undefined;
27
+ /** Cost-only projection for frequent status updates; full roster metrics keep their own cache. */
28
+ export declare function aggregateCost<TRecord extends AgentRecordLike>(args: {
29
+ rows: readonly TRecord[];
30
+ observedById: ReadonlyMap<string, ObservableSession>;
31
+ sessionCosts: WeakMap<object, AgentCostSnapshot>;
32
+ }): number;
28
33
  export declare function aggregateMetrics<TRecord extends AgentRecordLike>(args: {
29
34
  rows: readonly TRecord[];
30
35
  observedById: ReadonlyMap<string, ObservableSession>;
@@ -4,6 +4,10 @@ import type { ObservableSession } from "./session-observer-registry";
4
4
 
5
5
  export type AgentMetrics = AgentMetricsSummary;
6
6
 
7
+ export interface AgentCostSnapshot {
8
+ cost: number | undefined;
9
+ }
10
+
7
11
  export interface AggregateMetrics extends AgentMetrics {
8
12
  reportedAgents: number;
9
13
  /** Rows whose duration is an observer-measured active runtime. */
@@ -203,15 +207,28 @@ export function hubFallbackStatsSession<TRecord extends AgentRecordLike>(
203
207
  return session && typeof session.getSessionStats === "function" ? session : undefined;
204
208
  }
205
209
 
206
- function withDirectCost(metrics: AgentMetrics | undefined, cost: number | undefined): AgentMetrics | undefined {
210
+ /**
211
+ * Shared row policy for the Agent Hub and the status-line cost total.
212
+ * Non-cost metrics come from live progress, then persisted history, then the retained
213
+ * snapshot (parked rows only), then the live session. Cost always overlays the cumulative
214
+ * direct-session bill: live, then `history.directCost` (written at park), then retained.
215
+ * Persisted `history.metrics.cost` is a leaf-scoped snapshot, so it never wins.
216
+ */
217
+ function projectRowMetrics<TMetrics extends AgentCostSnapshot>(
218
+ ref: AgentRecordLike,
219
+ observed: ObservableSession | undefined,
220
+ live: TMetrics | undefined,
221
+ retained: TMetrics | undefined,
222
+ hasLiveSession: boolean,
223
+ ): TMetrics | AgentMetrics | undefined {
224
+ const cost = live?.cost ?? ref.history?.directCost ?? retained?.cost;
225
+ const metrics = observed?.progress
226
+ ? progressMetrics(observed)
227
+ : (ref.history?.metrics ?? (!hasLiveSession ? retained : undefined) ?? live);
207
228
  return metrics && cost !== undefined ? { ...metrics, cost } : metrics;
208
229
  }
209
230
 
210
- /**
211
- * Preserve progress/history for non-cost metrics, overlaying cumulative direct-session cost.
212
- * Persisted `history.metrics.cost` is a leaf-scoped snapshot, so it never overrides live
213
- * progress; only the cumulative direct-cost sources do.
214
- */
231
+ /** Full Agent Hub row metrics; see {@link projectRowMetrics} for source precedence. */
215
232
  export function hubRowMetrics<TRecord extends AgentRecordLike>(
216
233
  ref: TRecord,
217
234
  observed: ObservableSession | undefined,
@@ -220,12 +237,46 @@ export function hubRowMetrics<TRecord extends AgentRecordLike>(
220
237
  const session = hubFallbackStatsSession(ref, observed);
221
238
  const liveEntry = session ? sessionMetrics.get(session) : undefined;
222
239
  const retainedEntry = sessionMetrics.get(ref);
223
- const historyMetrics = ref.history?.metrics;
224
- const cost = liveEntry?.metrics?.cost ?? ref.history?.directCost ?? retainedEntry?.metrics?.cost;
225
- if (observed?.progress) return withDirectCost(progressMetrics(observed), cost);
226
- if (historyMetrics) return withDirectCost(historyMetrics, cost);
227
- if (!session && retainedEntry?.metrics) return withDirectCost(retainedEntry.metrics, cost);
228
- return withDirectCost(liveEntry?.metrics, cost);
240
+ return projectRowMetrics(ref, observed, liveEntry?.metrics, retainedEntry?.metrics, session !== undefined);
241
+ }
242
+
243
+ /** Cost-only projection for frequent status updates; full roster metrics keep their own cache. */
244
+ export function aggregateCost<TRecord extends AgentRecordLike>(args: {
245
+ rows: readonly TRecord[];
246
+ observedById: ReadonlyMap<string, ObservableSession>;
247
+ sessionCosts: WeakMap<object, AgentCostSnapshot>;
248
+ }): number {
249
+ let total = 0;
250
+ const countedSessions = new Set<NonNullable<AgentRecordLike["session"]>>();
251
+ for (const ref of args.rows) {
252
+ const observed = args.observedById.get(ref.id);
253
+ const session = hubFallbackStatsSession(ref, observed);
254
+ let cached = session ? args.sessionCosts.get(session) : undefined;
255
+ if (session) {
256
+ const directCost = readSessionDirectCost(session);
257
+ if (directCost !== undefined) {
258
+ cached = { cost: directCost };
259
+ args.sessionCosts.set(session, cached);
260
+ } else if (!cached || !fallbackReadCurrent(cached, session)) {
261
+ const stamp = fallbackReadStamp(session);
262
+ cached = { cost: readSessionMetrics(session, undefined)?.cost };
263
+ if (stamp) fallbackReadStamps.set(cached, stamp);
264
+ args.sessionCosts.set(session, cached);
265
+ }
266
+ if (cached?.cost !== undefined) args.sessionCosts.set(ref, cached);
267
+ }
268
+ const cost = projectRowMetrics(
269
+ ref,
270
+ observed,
271
+ cached?.cost !== undefined ? cached : undefined,
272
+ args.sessionCosts.get(ref),
273
+ session !== undefined,
274
+ )?.cost;
275
+ if (cost === undefined || (session && countedSessions.has(session))) continue;
276
+ if (session) countedSessions.add(session);
277
+ total += finiteMetric(cost);
278
+ }
279
+ return total;
229
280
  }
230
281
 
231
282
  export function aggregateMetrics<TRecord extends AgentRecordLike>(args: {
@@ -1211,7 +1211,7 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
1211
1211
  [span("Finished, parked, and killed subagents remain with the session that created them.", "dim")],
1212
1212
  { wrap: "word" },
1213
1213
  ),
1214
- text([span("Resume that session with omp-dev --continue, or spawn a task here.", "dim")], {
1214
+ text([span("Resume that session with zetacode --continue, or spawn a task here.", "dim")], {
1215
1215
  wrap: "word",
1216
1216
  }),
1217
1217
  ],
@@ -1857,7 +1857,7 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
1857
1857
  const emptyState = [
1858
1858
  `${theme.fg("muted", theme.status.shadowed)} ${theme.bold("No agents in this session")}`,
1859
1859
  theme.fg("dim", "Finished, parked, and killed subagents remain with the session that created them."),
1860
- theme.fg("dim", "Resume that session with omp-dev --continue, or spawn a task here."),
1860
+ theme.fg("dim", "Resume that session with zetacode --continue, or spawn a task here."),
1861
1861
  ];
1862
1862
  for (const line of emptyState.slice(0, budget)) {
1863
1863
  lines.push(line);