@agi-fans/dsh-tui 0.20.0 → 0.22.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 (319) hide show
  1. package/lib/CHANGELOG.md +249 -3
  2. package/lib/chrome/box.d.ts +47 -16
  3. package/lib/chrome/box.js +172 -130
  4. package/lib/chrome/box.js.map +1 -1
  5. package/lib/chrome/code-highlight.d.ts +15 -0
  6. package/lib/chrome/code-highlight.js +60 -4
  7. package/lib/chrome/code-highlight.js.map +1 -1
  8. package/lib/chrome/command-output.d.ts +29 -2
  9. package/lib/chrome/command-output.js +100 -4
  10. package/lib/chrome/command-output.js.map +1 -1
  11. package/lib/chrome/main-screen-renderer.d.ts +18 -0
  12. package/lib/chrome/main-screen-renderer.js +58 -5
  13. package/lib/chrome/main-screen-renderer.js.map +1 -1
  14. package/lib/chrome/markdown.js +30 -10
  15. package/lib/chrome/markdown.js.map +1 -1
  16. package/lib/chrome/relative-time.d.ts +17 -0
  17. package/lib/chrome/relative-time.js +36 -0
  18. package/lib/chrome/relative-time.js.map +1 -1
  19. package/lib/chrome/renderer.d.ts +9 -0
  20. package/lib/chrome/renderer.js +1 -1
  21. package/lib/chrome/renderer.js.map +1 -1
  22. package/lib/chrome/secret-mask.d.ts +35 -0
  23. package/lib/chrome/secret-mask.js +180 -0
  24. package/lib/chrome/secret-mask.js.map +1 -0
  25. package/lib/chrome/status-config.d.ts +161 -51
  26. package/lib/chrome/status-config.js +237 -126
  27. package/lib/chrome/status-config.js.map +1 -1
  28. package/lib/chrome/status-line.d.ts +3 -2
  29. package/lib/chrome/status-line.js +105 -63
  30. package/lib/chrome/status-line.js.map +1 -1
  31. package/lib/chrome/terminal-colors.d.ts +85 -0
  32. package/lib/chrome/terminal-colors.js +230 -0
  33. package/lib/chrome/terminal-colors.js.map +1 -0
  34. package/lib/chrome/terminal-images.d.ts +9 -1
  35. package/lib/chrome/terminal-images.js +27 -4
  36. package/lib/chrome/terminal-images.js.map +1 -1
  37. package/lib/chrome/theme.d.ts +32 -9
  38. package/lib/chrome/theme.js +616 -520
  39. package/lib/chrome/theme.js.map +1 -1
  40. package/lib/chrome/tool-args.d.ts +1 -1
  41. package/lib/chrome/tool-args.js +3 -1
  42. package/lib/chrome/tool-args.js.map +1 -1
  43. package/lib/chrome/tool-renderers.d.ts +2 -0
  44. package/lib/chrome/tool-renderers.js +69 -3
  45. package/lib/chrome/tool-renderers.js.map +1 -1
  46. package/lib/chrome/tools-list.d.ts +1 -4
  47. package/lib/chrome/tools-list.js +1 -41
  48. package/lib/chrome/tools-list.js.map +1 -1
  49. package/lib/chrome/welcome-tips.d.ts +20 -7
  50. package/lib/chrome/welcome-tips.js +22 -41
  51. package/lib/chrome/welcome-tips.js.map +1 -1
  52. package/lib/commands/auth-ids.d.ts +25 -0
  53. package/lib/commands/auth-ids.js +27 -0
  54. package/lib/commands/auth-ids.js.map +1 -0
  55. package/lib/commands/auth-input.d.ts +34 -0
  56. package/lib/commands/auth-input.js +96 -0
  57. package/lib/commands/auth-input.js.map +1 -0
  58. package/lib/commands/auth-providers.d.ts +39 -0
  59. package/lib/commands/auth-providers.js +125 -0
  60. package/lib/commands/auth-providers.js.map +1 -0
  61. package/lib/commands/auth.d.ts +16 -22
  62. package/lib/commands/auth.js +346 -299
  63. package/lib/commands/auth.js.map +1 -1
  64. package/lib/commands/btw.d.ts +27 -0
  65. package/lib/commands/btw.js +59 -0
  66. package/lib/commands/btw.js.map +1 -0
  67. package/lib/commands/diff.js +30 -5
  68. package/lib/commands/diff.js.map +1 -1
  69. package/lib/commands/export.d.ts +1 -1
  70. package/lib/commands/export.js +5 -3
  71. package/lib/commands/export.js.map +1 -1
  72. package/lib/commands/files.d.ts +17 -1
  73. package/lib/commands/files.js +69 -61
  74. package/lib/commands/files.js.map +1 -1
  75. package/lib/commands/integrations.js +18 -6
  76. package/lib/commands/integrations.js.map +1 -1
  77. package/lib/commands/jobs.d.ts +19 -4
  78. package/lib/commands/jobs.js +50 -7
  79. package/lib/commands/jobs.js.map +1 -1
  80. package/lib/commands/model.js +109 -76
  81. package/lib/commands/model.js.map +1 -1
  82. package/lib/commands/permission.js +46 -13
  83. package/lib/commands/permission.js.map +1 -1
  84. package/lib/commands/plugins.js +196 -42
  85. package/lib/commands/plugins.js.map +1 -1
  86. package/lib/commands/schedule.d.ts +29 -0
  87. package/lib/commands/schedule.js +228 -0
  88. package/lib/commands/schedule.js.map +1 -0
  89. package/lib/commands/session-configuration.d.ts +1 -1
  90. package/lib/commands/session-configuration.js +29 -28
  91. package/lib/commands/session-configuration.js.map +1 -1
  92. package/lib/commands/session.js +75 -80
  93. package/lib/commands/session.js.map +1 -1
  94. package/lib/commands/terminal.d.ts +24 -4
  95. package/lib/commands/terminal.js +38 -94
  96. package/lib/commands/terminal.js.map +1 -1
  97. package/lib/commands/trajectory.js +4 -1
  98. package/lib/commands/trajectory.js.map +1 -1
  99. package/lib/definition.d.ts +150 -11
  100. package/lib/index.d.ts +1 -0
  101. package/lib/index.js +1 -0
  102. package/lib/index.js.map +1 -1
  103. package/lib/input/actions.d.ts +51 -0
  104. package/lib/input/actions.js +120 -0
  105. package/lib/input/actions.js.map +1 -0
  106. package/lib/input/editor-discovery.js +2 -2
  107. package/lib/input/editor-discovery.js.map +1 -1
  108. package/lib/input/editor.d.ts +0 -10
  109. package/lib/input/editor.js +11 -9
  110. package/lib/input/editor.js.map +1 -1
  111. package/lib/input/image-paste.d.ts +19 -1
  112. package/lib/input/image-paste.js +82 -1
  113. package/lib/input/image-paste.js.map +1 -1
  114. package/lib/input/keybindings-config.d.ts +15 -4
  115. package/lib/input/keybindings-config.js +57 -35
  116. package/lib/input/keybindings-config.js.map +1 -1
  117. package/lib/input/keys.d.ts +18 -0
  118. package/lib/input/keys.js +48 -3
  119. package/lib/input/keys.js.map +1 -1
  120. package/lib/input/wheel-scroll.d.ts +46 -0
  121. package/lib/input/wheel-scroll.js +69 -0
  122. package/lib/input/wheel-scroll.js.map +1 -0
  123. package/lib/prompt-intents.d.ts +182 -0
  124. package/lib/prompt-intents.js +127 -0
  125. package/lib/prompt-intents.js.map +1 -0
  126. package/lib/runtime/agent-profile.d.ts +5 -0
  127. package/lib/runtime/agent-profile.js +12 -0
  128. package/lib/runtime/agent-profile.js.map +1 -1
  129. package/lib/runtime/approval-rules.d.ts +59 -0
  130. package/lib/runtime/approval-rules.js +179 -0
  131. package/lib/runtime/approval-rules.js.map +1 -0
  132. package/lib/runtime/btw-policy.d.ts +7 -0
  133. package/lib/runtime/btw-policy.js +18 -0
  134. package/lib/runtime/btw-policy.js.map +1 -0
  135. package/lib/runtime/btw.d.ts +47 -0
  136. package/lib/runtime/btw.js +200 -0
  137. package/lib/runtime/btw.js.map +1 -0
  138. package/lib/runtime/cancellable.d.ts +75 -0
  139. package/lib/runtime/cancellable.js +76 -0
  140. package/lib/runtime/cancellable.js.map +1 -0
  141. package/lib/runtime/composer-images.js +5 -4
  142. package/lib/runtime/composer-images.js.map +1 -1
  143. package/lib/runtime/composer-shell.d.ts +30 -0
  144. package/lib/runtime/composer-shell.js +150 -0
  145. package/lib/runtime/composer-shell.js.map +1 -0
  146. package/lib/runtime/file-recovery-local.d.ts +73 -0
  147. package/lib/runtime/file-recovery-local.js +351 -0
  148. package/lib/runtime/file-recovery-local.js.map +1 -0
  149. package/lib/runtime/file-recovery-recording.d.ts +23 -0
  150. package/lib/runtime/file-recovery-recording.js +131 -0
  151. package/lib/runtime/file-recovery-recording.js.map +1 -0
  152. package/lib/runtime/file-recovery-review.d.ts +3 -0
  153. package/lib/runtime/file-recovery-review.js +130 -0
  154. package/lib/runtime/file-recovery-review.js.map +1 -0
  155. package/lib/runtime/file-recovery-store.d.ts +83 -0
  156. package/lib/runtime/file-recovery-store.js +329 -0
  157. package/lib/runtime/file-recovery-store.js.map +1 -0
  158. package/lib/runtime/file-recovery-worker.d.ts +22 -0
  159. package/lib/runtime/file-recovery-worker.js +153 -0
  160. package/lib/runtime/file-recovery-worker.js.map +1 -0
  161. package/lib/runtime/file-review.d.ts +6 -3
  162. package/lib/runtime/file-review.js +44 -7
  163. package/lib/runtime/file-review.js.map +1 -1
  164. package/lib/runtime/herdr-agent.d.ts +4 -13
  165. package/lib/runtime/herdr-agent.js +19 -63
  166. package/lib/runtime/herdr-agent.js.map +1 -1
  167. package/lib/runtime/plain-tui.d.ts +2 -0
  168. package/lib/runtime/plain-tui.js +7 -1
  169. package/lib/runtime/plain-tui.js.map +1 -1
  170. package/lib/runtime/plugin-settings.d.ts +25 -2
  171. package/lib/runtime/plugin-settings.js +329 -31
  172. package/lib/runtime/plugin-settings.js.map +1 -1
  173. package/lib/runtime/provider-local.d.ts +81 -24
  174. package/lib/runtime/provider-local.js +1807 -608
  175. package/lib/runtime/provider-local.js.map +1 -1
  176. package/lib/runtime/runner.js +52 -12
  177. package/lib/runtime/runner.js.map +1 -1
  178. package/lib/runtime/session-archive.d.ts +6 -0
  179. package/lib/runtime/session-archive.js +129 -3
  180. package/lib/runtime/session-archive.js.map +1 -1
  181. package/lib/runtime/session-runtime.js +1 -1
  182. package/lib/runtime/session-runtime.js.map +1 -1
  183. package/lib/runtime/settings-preferences.d.ts +5 -0
  184. package/lib/runtime/settings-preferences.js +34 -0
  185. package/lib/runtime/settings-preferences.js.map +1 -0
  186. package/lib/runtime/startup-notices.js +2 -0
  187. package/lib/runtime/startup-notices.js.map +1 -1
  188. package/lib/runtime/terminal-modes.d.ts +47 -0
  189. package/lib/runtime/terminal-modes.js +86 -0
  190. package/lib/runtime/terminal-modes.js.map +1 -0
  191. package/lib/runtime/terminal-notifications.d.ts +3 -5
  192. package/lib/runtime/terminal-notifications.js +4 -8
  193. package/lib/runtime/terminal-notifications.js.map +1 -1
  194. package/lib/runtime/terminal-probe.d.ts +36 -0
  195. package/lib/runtime/terminal-probe.js +37 -0
  196. package/lib/runtime/terminal-probe.js.map +1 -0
  197. package/lib/runtime/terminal-session.d.ts +57 -0
  198. package/lib/runtime/terminal-session.js +164 -0
  199. package/lib/runtime/terminal-session.js.map +1 -0
  200. package/lib/runtime/terminal-state.d.ts +29 -0
  201. package/lib/runtime/terminal-state.js +62 -0
  202. package/lib/runtime/terminal-state.js.map +1 -0
  203. package/lib/runtime/tool-presentation.js +50 -14
  204. package/lib/runtime/tool-presentation.js.map +1 -1
  205. package/lib/session/external-agents.d.ts +5 -0
  206. package/lib/session/external-agents.js +27 -0
  207. package/lib/session/external-agents.js.map +1 -0
  208. package/lib/session/feature-toggles.d.ts +1 -1
  209. package/lib/session/feature-toggles.js +1 -1
  210. package/lib/session/interaction-adapter.d.ts +1 -6
  211. package/lib/session/interaction-adapter.js +95 -18
  212. package/lib/session/interaction-adapter.js.map +1 -1
  213. package/lib/session/message-queue.js +1 -1
  214. package/lib/session/message-queue.js.map +1 -1
  215. package/lib/session/model-readiness.d.ts +16 -0
  216. package/lib/session/model-readiness.js +43 -0
  217. package/lib/session/model-readiness.js.map +1 -0
  218. package/lib/session/package-metadata.d.ts +8 -0
  219. package/lib/session/package-metadata.js +8 -0
  220. package/lib/session/package-metadata.js.map +1 -1
  221. package/lib/session/project-context.d.ts +5 -0
  222. package/lib/session/project-context.js +36 -0
  223. package/lib/session/project-context.js.map +1 -1
  224. package/lib/session/session-controller.d.ts +10 -13
  225. package/lib/session/session-controller.js +183 -98
  226. package/lib/session/session-controller.js.map +1 -1
  227. package/lib/session/session-library.js +2 -0
  228. package/lib/session/session-library.js.map +1 -1
  229. package/lib/session/session-tree.d.ts +3 -0
  230. package/lib/session/session-tree.js +3 -1
  231. package/lib/session/session-tree.js.map +1 -1
  232. package/lib/session/subagent-roster.d.ts +2 -1
  233. package/lib/session/subagent-roster.js +24 -3
  234. package/lib/session/subagent-roster.js.map +1 -1
  235. package/lib/session/subagent-tracker.d.ts +6 -2
  236. package/lib/session/subagent-tracker.js +49 -2
  237. package/lib/session/subagent-tracker.js.map +1 -1
  238. package/lib/session/tui-settings.d.ts +47 -13
  239. package/lib/session/tui-settings.js +76 -34
  240. package/lib/session/tui-settings.js.map +1 -1
  241. package/lib/session/working-directory.d.ts +4 -0
  242. package/lib/session/working-directory.js +4 -0
  243. package/lib/session/working-directory.js.map +1 -0
  244. package/lib/session/worktrees.d.ts +13 -0
  245. package/lib/session/worktrees.js +49 -0
  246. package/lib/session/worktrees.js.map +1 -0
  247. package/lib/views/approval-rule.d.ts +3 -0
  248. package/lib/views/approval-rule.js +4 -0
  249. package/lib/views/approval-rule.js.map +1 -0
  250. package/lib/views/at-complete.d.ts +8 -0
  251. package/lib/views/at-complete.js +64 -2
  252. package/lib/views/at-complete.js.map +1 -1
  253. package/lib/views/autocomplete.d.ts +22 -2
  254. package/lib/views/autocomplete.js +70 -40
  255. package/lib/views/autocomplete.js.map +1 -1
  256. package/lib/views/copy-targets.d.ts +13 -14
  257. package/lib/views/copy-targets.js +42 -54
  258. package/lib/views/copy-targets.js.map +1 -1
  259. package/lib/views/document-reader.js +18 -5
  260. package/lib/views/document-reader.js.map +1 -1
  261. package/lib/views/event-views.d.ts +1 -1
  262. package/lib/views/event-views.js +60 -6
  263. package/lib/views/event-views.js.map +1 -1
  264. package/lib/views/file-actions.d.ts +12 -0
  265. package/lib/views/file-actions.js +72 -0
  266. package/lib/views/file-actions.js.map +1 -0
  267. package/lib/views/history-search.js +15 -21
  268. package/lib/views/history-search.js.map +1 -1
  269. package/lib/views/hotkeys.d.ts +8 -1
  270. package/lib/views/hotkeys.js +53 -26
  271. package/lib/views/hotkeys.js.map +1 -1
  272. package/lib/views/list-navigation.d.ts +62 -0
  273. package/lib/views/list-navigation.js +73 -0
  274. package/lib/views/list-navigation.js.map +1 -0
  275. package/lib/views/list-view.d.ts +99 -0
  276. package/lib/views/list-view.js +165 -0
  277. package/lib/views/list-view.js.map +1 -0
  278. package/lib/views/prompt-selector.d.ts +34 -4
  279. package/lib/views/prompt-selector.js +270 -134
  280. package/lib/views/prompt-selector.js.map +1 -1
  281. package/lib/views/session-files.d.ts +1 -2
  282. package/lib/views/session-files.js +1 -11
  283. package/lib/views/session-files.js.map +1 -1
  284. package/lib/views/settings-list.d.ts +51 -10
  285. package/lib/views/settings-list.js +267 -315
  286. package/lib/views/settings-list.js.map +1 -1
  287. package/lib/views/trajectory-timeline.d.ts +72 -0
  288. package/lib/views/trajectory-timeline.js +133 -0
  289. package/lib/views/trajectory-timeline.js.map +1 -0
  290. package/lib/views/trajectory.d.ts +15 -4
  291. package/lib/views/trajectory.js +557 -123
  292. package/lib/views/trajectory.js.map +1 -1
  293. package/lib/views/transcript-export.js +9 -0
  294. package/lib/views/transcript-export.js.map +1 -1
  295. package/lib/views/transcript-render.d.ts +97 -25
  296. package/lib/views/transcript-render.js +542 -170
  297. package/lib/views/transcript-render.js.map +1 -1
  298. package/lib/views/transcript-search.d.ts +39 -6
  299. package/lib/views/transcript-search.js +46 -11
  300. package/lib/views/transcript-search.js.map +1 -1
  301. package/lib/views/transcript-types.d.ts +63 -5
  302. package/lib/views/transcript-types.js +23 -11
  303. package/lib/views/transcript-types.js.map +1 -1
  304. package/package.json +95 -65
  305. package/lib/commands/loop.d.ts +0 -52
  306. package/lib/commands/loop.js +0 -307
  307. package/lib/commands/loop.js.map +0 -1
  308. package/lib/commands/steer.d.ts +0 -5
  309. package/lib/commands/steer.js +0 -24
  310. package/lib/commands/steer.js.map +0 -1
  311. package/lib/runtime/terminal-reader.d.ts +0 -11
  312. package/lib/runtime/terminal-reader.js +0 -87
  313. package/lib/runtime/terminal-reader.js.map +0 -1
  314. package/lib/views/agent-hub.d.ts +0 -32
  315. package/lib/views/agent-hub.js +0 -206
  316. package/lib/views/agent-hub.js.map +0 -1
  317. package/lib/views/copy-selector.d.ts +0 -51
  318. package/lib/views/copy-selector.js +0 -139
  319. package/lib/views/copy-selector.js.map +0 -1
@@ -3,16 +3,20 @@
3
3
  * Pure — the provider owns live application of the selected values.
4
4
  * @module @agi-fans/dsh-tui
5
5
  */
6
+ import { TERMINAL_TITLE_MODES, TERMINAL_STATE_MODES } from "../runtime/terminal-state.js";
6
7
  import { STARTUP_CHANGELOG_MODES } from "../session/release-notes.js";
7
- import { STATUS_COLOR_TOKENS, STATUS_CONTEXT_STYLES, STATUS_GROUP_IDS, STATUS_LABEL_STYLES, STATUS_META_IDS, isStatusItemId, isStatusMetaId, itemSide, resolveStatusBarConfig, } from "../chrome/status-config.js";
8
+ import { STATUS_DENSITIES, STATUS_ITEM_IDS, isStatusItemId, resolveStatusBarConfig, statusBarDocument, statusItemVisible, toggleStatusItem, } from "../chrome/status-config.js";
8
9
  import { renderStatusFooter } from "../chrome/status-line.js";
9
- import { BOX, SYMBOL, THEME_NAMES, isThemeName } from "../chrome/theme.js";
10
- import { padToWidth, truncateToWidth, visibleWidth, wrapText } from "../chrome/width.js";
11
- import { MOTION_MODES } from "../session/tui-settings.js";
10
+ import { BOX, THEME_NAMES, isThemeName } from "../chrome/theme.js";
11
+ import { padToWidth, truncateToWidth, visibleWidth, wrapTextStable } from "../chrome/width.js";
12
+ import { MOTION_MODES, NOTIFICATION_MODES, resolveNotificationMode, } from "../session/tui-settings.js";
12
13
  import { MATH_MODES } from "../chrome/math.js";
13
14
  import { MERMAID_MODES } from "../chrome/mermaid.js";
14
- import { formatOverlayHint } from "./hotkey-format.js";
15
+ import { listNavigation } from "./list-navigation.js";
16
+ import { LIST_CURSOR_CELLS, renderListRowWithValue } from "./list-view.js";
15
17
  import { FEATURE_TOGGLES } from "../session/feature-toggles.js";
18
+ import { rankSearchResults } from "../input/fuzzy-search.js";
19
+ import { moveGraphemeLeft } from "../chrome/grapheme.js";
16
20
  const COLOR_VALUES = ['on', 'off'];
17
21
  const STARTUP_CHANGELOG_VALUES = [...STARTUP_CHANGELOG_MODES];
18
22
  const AGENT_LANGUAGE_VALUES = ['Auto', 'Simplified Chinese', 'English'];
@@ -22,46 +26,30 @@ const AGENT_LANGUAGE_LABELS = {
22
26
  en: 'English',
23
27
  };
24
28
  const AGENT_LANGUAGE_IDS = Object.fromEntries(Object.entries(AGENT_LANGUAGE_LABELS).map(([id, label]) => [label, id]));
25
- const STATUS_ITEM_CONTROLS = 'Left/Right sets color; Space shows or hides; Enter then arrows move it.';
29
+ const STATUS_ITEM_CONTROLS = 'Space shows or hides it.';
26
30
  const STATUS_ITEM_COPY = {
27
- model: { label: 'Model', description: `Model name on the left of the first footer line. ${STATUS_ITEM_CONTROLS}`, sample: 'deepseek' },
31
+ model: { label: 'Model', description: `Model name on the first footer line. ${STATUS_ITEM_CONTROLS}`, sample: 'deepseek' },
28
32
  effort: { label: 'Effort', description: `Reasoning effort on the first footer line. ${STATUS_ITEM_CONTROLS}`, sample: 'max' },
29
33
  path: { label: 'Path', description: `Workspace path on the right of the first footer line. ${STATUS_ITEM_CONTROLS}`, sample: '~/project' },
30
- git: { label: 'Git', description: `Git branch on the right of the first footer line. A dirty worktree stays warning while color is default. ${STATUS_ITEM_CONTROLS}`, sample: 'main *1' },
31
- session: { label: 'Session', description: `Folded session title on the first footer line. Off by default; the terminal window title shows it regardless. ${STATUS_ITEM_CONTROLS}`, sample: 'Fix the parser' },
32
- context: { label: 'Context', description: `Context occupancy; choose its representation in Context style. ${STATUS_ITEM_CONTROLS}`, sample: 'Ctx 1.6%' },
33
- cache: { label: 'Cache', description: `Prompt-cache hit rate. Cache-hit percentages stay on the success color. ${STATUS_ITEM_CONTROLS}`, sample: 'Cache 99%' },
34
- tokens: { label: 'Tokens', description: `Input and output token counts. ${STATUS_ITEM_CONTROLS}`, sample: '5.9M in' },
35
- speed: { label: 'Latency', description: `First-token latency and decode rate. ${STATUS_ITEM_CONTROLS}`, sample: 'TTFT 1.2s' },
36
- durations: { label: 'Time', description: `LLM and tool duration. ${STATUS_ITEM_CONTROLS}`, sample: 'LLM 16m51s' },
37
- counts: { label: 'Activity', description: `Turn and step counts. ${STATUS_ITEM_CONTROLS}`, sample: '3 turns' },
34
+ git: { label: 'Git', description: `Git branch on the first footer line. A dirty worktree stays warning. ${STATUS_ITEM_CONTROLS}`, sample: 'main *1' },
35
+ session: { label: 'Session', description: `Folded session title on the first footer line. Off at Standard and Minimal; the terminal window title shows it regardless. ${STATUS_ITEM_CONTROLS}`, sample: 'Fix the parser' },
36
+ context: { label: 'Context', description: `Context occupancy, on the left of the second line. Its representation follows the density. ${STATUS_ITEM_CONTROLS}`, sample: 'Ctx 1.6%' },
37
+ cache: { label: 'Cache', description: `Prompt-cache hit rate, beside occupancy on the left. Cache-hit percentages stay on the success color. ${STATUS_ITEM_CONTROLS}`, sample: 'Cache 99%' },
38
+ tokens: { label: 'Tokens', description: `Input and output token counts, on the right with the other cost figures. ${STATUS_ITEM_CONTROLS}`, sample: '5.9M in' },
39
+ speed: { label: 'Latency', description: `First-token latency and decode rate, on the right. ${STATUS_ITEM_CONTROLS}`, sample: 'TTFT 1.2s' },
40
+ durations: { label: 'Time', description: `LLM and tool duration, on the right. ${STATUS_ITEM_CONTROLS}`, sample: 'LLM 16m51s' },
41
+ counts: { label: 'Activity', description: `Turn and step counts, on the right. ${STATUS_ITEM_CONTROLS}`, sample: '3 turns' },
38
42
  };
39
- function itemSwatch(id, token) {
40
- if (token !== 'default')
41
- return token === 'label' ? 'customMessageLabel' : token;
42
- if (id === 'effort')
43
- return 'customMessageLabel';
44
- if (id === 'git')
45
- return 'warning';
46
- if (id === 'path')
47
- return 'muted';
48
- return 'text';
49
- }
50
- function statusItemVisible(config, id) {
51
- return isStatusMetaId(id) ? (config.meta ?? STATUS_META_IDS).includes(id) : config.groups.includes(id);
52
- }
53
43
  function statusItemRow(config, id) {
54
44
  const visible = statusItemVisible(config, id);
55
- const color = config.colors?.[id] ?? 'default';
56
45
  const copy = STATUS_ITEM_COPY[id];
57
46
  return {
58
47
  id: `statusItem:${id}`,
59
- label: (itemSide(config, id) === 'right' ? '→ ' : '← ') + copy.label,
48
+ label: copy.label,
60
49
  description: copy.description,
61
50
  sample: visible ? copy.sample : 'hidden',
62
- value: color,
63
- values: STATUS_COLOR_TOKENS,
64
- swatch: itemSwatch(id, color),
51
+ value: visible ? 'shown' : 'hidden',
52
+ values: ['shown', 'hidden'],
65
53
  hidden: !visible,
66
54
  };
67
55
  }
@@ -102,6 +90,13 @@ function generalSettingItems(prefs, editors = [{ id: 'auto', label: 'Auto' }]) {
102
90
  value: prefs.mermaidMode ?? 'auto',
103
91
  values: MERMAID_MODES,
104
92
  },
93
+ {
94
+ id: 'hideThinking',
95
+ label: 'Thinking in turns',
96
+ description: 'Shown keeps reasoning visible while a turn runs; Hidden collapses it to one line. Ctrl+T flips this for the session.',
97
+ value: prefs.hideThinking === true ? 'hidden' : 'shown',
98
+ values: ['shown', 'hidden'],
99
+ },
105
100
  {
106
101
  id: 'editor',
107
102
  label: 'Editor',
@@ -131,11 +126,10 @@ function generalSettingItems(prefs, editors = [{ id: 'auto', label: 'Auto' }]) {
131
126
  values: COLOR_VALUES,
132
127
  },
133
128
  {
134
- id: 'terminalProgress',
135
- label: 'Terminal activity',
136
- description: 'Busy/idle status in supported terminal tabs and taskbars',
137
- value: prefs.terminalProgress === true ? 'on' : 'off',
138
- values: COLOR_VALUES,
129
+ id: 'terminalTitle', label: 'Terminal title', description: 'Status and session, session only, status and workspace, or Off. Changes the terminal tab title.', value: prefs.terminalTitle ?? 'status', values: TERMINAL_TITLE_MODES,
130
+ },
131
+ {
132
+ id: 'terminalState', label: 'Terminal status', description: 'Auto reports the root Agent lifecycle once the terminal answers an OSC 7501 capability query; On reports regardless; Off clears reports.', value: prefs.terminalState ?? 'auto', values: TERMINAL_STATE_MODES,
139
133
  },
140
134
  {
141
135
  id: 'checkUpdates',
@@ -154,23 +148,9 @@ function generalSettingItems(prefs, editors = [{ id: 'auto', label: 'Auto' }]) {
154
148
  {
155
149
  id: 'notifications',
156
150
  label: 'Notifications',
157
- description: 'Notify when a turn finishes or input is required',
158
- value: prefs.notifications ?? 'off',
159
- values: ['off', 'long-running', 'always'],
160
- },
161
- {
162
- id: 'notificationFocus',
163
- label: 'Notify when',
164
- description: 'Only notify while away from the terminal, or regardless of focus. Terminals without focus reports use the existing notification policy.',
165
- value: prefs.notificationFocus ?? 'unfocused',
166
- values: ['unfocused', 'always'],
167
- },
168
- {
169
- id: 'notificationThreshold',
170
- label: 'Long turn',
171
- description: 'Minimum duration for long-running notifications',
172
- value: prefs.notificationThreshold ?? '30s',
173
- values: ['15s', '30s', '1m', '2m'],
151
+ description: 'Notify when a long turn finishes or input is required. Away also waits until the terminal loses focus; Always reports regardless of focus.',
152
+ value: resolveNotificationMode(prefs.notifications, prefs.notificationFocus),
153
+ values: NOTIFICATION_MODES,
174
154
  },
175
155
  ];
176
156
  }
@@ -191,28 +171,21 @@ function statusSettingItems(prefs) {
191
171
  {
192
172
  id: 'statusEnabled',
193
173
  label: 'Telemetry',
194
- description: 'Show session metrics on the second footer line',
174
+ description: 'Show session metrics below the first footer line',
195
175
  value: statusBar.enabled ? 'on' : 'off',
196
176
  values: COLOR_VALUES,
197
177
  },
198
178
  {
199
- id: 'statusLabels',
200
- label: 'Context label',
201
- description: 'Use Ctx or Context before the occupancy value',
202
- value: statusBar.labels,
203
- values: STATUS_LABEL_STYLES,
204
- },
205
- {
206
- id: 'statusContextStyle',
207
- label: 'Context style',
208
- description: 'Percentage, a ten-cell bar, used/window tokens, or percentage with tokens',
209
- value: statusBar.contextStyle,
210
- values: STATUS_CONTEXT_STYLES,
179
+ id: 'statusDensity',
180
+ label: 'Density',
181
+ description: 'Minimal shows occupancy and cache; Standard adds latency; Verbose adds tokens on the same line and gives durations and activity a third, with long labels and the detailed context form',
182
+ value: statusBar.density,
183
+ values: STATUS_DENSITIES,
211
184
  },
212
- ...statusBar.metaOrder.filter(id => itemSide(statusBar, id) === 'left').map(id => statusItemRow(statusBar, id)),
213
- ...statusBar.metaOrder.filter(id => itemSide(statusBar, id) === 'right').map(id => statusItemRow(statusBar, id)),
214
- ...statusBar.order.filter(id => itemSide(statusBar, id) === 'left').map(id => statusItemRow(statusBar, id)),
215
- ...statusBar.order.filter(id => itemSide(statusBar, id) === 'right').map(id => statusItemRow(statusBar, id)),
185
+ // Each part has one fixed place, so the rows below are a list of what
186
+ // appears rather than a layout to assemble: Space shows or hides, and
187
+ // nothing has to be dragged into a column.
188
+ ...STATUS_ITEM_IDS.map(id => statusItemRow(statusBar, id)),
216
189
  ];
217
190
  }
218
191
  /**
@@ -230,68 +203,10 @@ function featureSettingItems(features) {
230
203
  }));
231
204
  }
232
205
  /** Product-owned rows shown in `/settings` (OMP settings-list cycle widgets). */
233
- export function tuiSettingItems(prefs, agent, features, plugins, editors) {
234
- return [
235
- ...generalSettingItems(prefs, editors),
236
- ...agentSettingItems(agent),
237
- ...(features === undefined ? [] : featureSettingItems(features)),
238
- ...statusSettingItems(prefs),
239
- ...(plugins ?? []).map(plugin => ({ ...plugin, id: `plugin:${plugin.id}`, value: 'Edit', values: [] })),
240
- ];
241
- }
242
- function showInOrder(visible, order, id) {
243
- const next = visible.filter(item => item !== id);
244
- const position = order.indexOf(id);
245
- const insert = next.findIndex(item => order.indexOf(item) > position);
246
- next.splice(insert < 0 ? next.length : insert, 0, id);
247
- return next;
248
- }
249
- function updateStatusGroup(config, id, value) {
250
- const groups = config.groups.filter(group => group !== id);
251
- if (value === 'hidden' || value === 'off')
252
- return { ...config, groups };
253
- if (value === 'shown')
254
- return { ...config, groups: showInOrder(groups, config.order ?? STATUS_GROUP_IDS, id) };
255
- const position = Number.parseInt(value, 10);
256
- if (!Number.isInteger(position) || position < 1 || position > STATUS_GROUP_IDS.length)
257
- return config;
258
- groups.splice(Math.min(position - 1, groups.length), 0, id);
259
- return { ...config, groups };
260
- }
261
- function toggleStatusItem(config, id) {
262
- if (isStatusMetaId(id)) {
263
- const meta = config.meta ?? [...STATUS_META_IDS];
264
- const hidden = !meta.includes(id);
265
- return {
266
- ...config,
267
- meta: hidden ? showInOrder(meta.filter(item => item !== id), config.metaOrder ?? STATUS_META_IDS, id) : meta.filter(item => item !== id),
268
- };
269
- }
270
- return updateStatusGroup(config, id, statusItemVisible(config, id) ? 'hidden' : 'shown');
271
- }
272
- function reorderVisible(visible, order, moving, direction) {
273
- const from = visible.indexOf(moving);
274
- if (from < 0)
275
- return { visible: [...visible], order: [...order] };
276
- const to = Math.max(0, Math.min(visible.length - 1, from + direction));
277
- const nextVisible = [...visible];
278
- if (to !== from) {
279
- nextVisible.splice(from, 1);
280
- nextVisible.splice(to, 0, moving);
281
- }
282
- const visibleSet = new Set(nextVisible);
283
- const nextOrder = [];
284
- let visibleIndex = 0;
285
- for (const item of order) {
286
- if (visibleSet.has(item))
287
- nextOrder.push(nextVisible[visibleIndex++]);
288
- else
289
- nextOrder.push(item);
290
- }
291
- for (const item of nextVisible)
292
- if (!nextOrder.includes(item))
293
- nextOrder.push(item);
294
- return { visible: nextVisible, order: nextOrder };
206
+ export function tuiSettingItems(prefs, agent, features, plugins, editors, actions) {
207
+ return settingGroups({ prefs, ...(agent === undefined ? {} : { agent }), ...(features === undefined ? {} : { features }),
208
+ ...(plugins === undefined ? {} : { plugins }), ...(editors === undefined ? {} : { editors }),
209
+ ...(actions === undefined ? {} : { actions }) }).flatMap(group => group.items);
295
210
  }
296
211
  /** Apply one cycled value back onto the prefs record. */
297
212
  export function applySettingValue(prefs, id, value) {
@@ -305,6 +220,8 @@ export function applySettingValue(prefs, id, value) {
305
220
  return { ...prefs, mathMode: value };
306
221
  if (id === 'mermaidMode' && MERMAID_MODES.includes(value))
307
222
  return { ...prefs, mermaidMode: value };
223
+ if (id === 'hideThinking')
224
+ return { ...prefs, hideThinking: value === 'hidden' };
308
225
  if (id === 'pasteProtection')
309
226
  return { ...prefs, pasteProtection: value === 'on' };
310
227
  if (id === 'copyOnSelect')
@@ -312,54 +229,60 @@ export function applySettingValue(prefs, id, value) {
312
229
  if (id === 'mouseInteraction' && ['auto', 'tui', 'native'].includes(value)) {
313
230
  return { ...prefs, mouseInteraction: value };
314
231
  }
315
- if (id === 'terminalProgress')
316
- return { ...prefs, terminalProgress: value === 'on' };
232
+ if (id === 'terminalTitle' && TERMINAL_TITLE_MODES.includes(value))
233
+ return { ...prefs, terminalTitle: value };
234
+ if (id === 'terminalState' && TERMINAL_STATE_MODES.includes(value))
235
+ return { ...prefs, terminalState: value };
317
236
  if (id === 'checkUpdates')
318
237
  return { ...prefs, checkUpdates: value === 'on' };
319
238
  if (id === 'startupChangelog' && STARTUP_CHANGELOG_MODES.includes(value)) {
320
239
  return { ...prefs, startupChangelog: value };
321
240
  }
322
- if (id === 'notifications' && ['off', 'long-running', 'always'].includes(value)) {
241
+ if (id === 'notifications' && NOTIFICATION_MODES.includes(value)) {
323
242
  return { ...prefs, notifications: value };
324
243
  }
325
- if (id === 'notificationFocus' && ['always', 'unfocused'].includes(value))
326
- return { ...prefs, notificationFocus: value };
327
- if (id === 'notificationThreshold' && ['15s', '30s', '1m', '2m'].includes(value)) {
328
- return { ...prefs, notificationThreshold: value };
244
+ /**
245
+ * The status line is one document: a density, plus the parts hidden on top of
246
+ * it. Row identity decides which field moves — there is no per-item colour or
247
+ * column left to write, and `enabled` survives only as "telemetry off".
248
+ */
249
+ const statusBar = storedStatusBar(prefs);
250
+ if (id === 'statusDensity' && STATUS_DENSITIES.includes(value)) {
251
+ return { ...prefs, statusBar: { ...statusBar, density: value } };
329
252
  }
330
- const statusBar = resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset);
331
253
  if (id === 'statusEnabled')
332
254
  return { ...prefs, statusBar: { ...statusBar, enabled: value === 'on' } };
333
- if (id === 'statusLabels' && STATUS_LABEL_STYLES.includes(value)) {
334
- return { ...prefs, statusBar: { ...statusBar, labels: value } };
335
- }
336
- if (id === 'statusContextStyle' && STATUS_CONTEXT_STYLES.includes(value)) {
337
- return { ...prefs, statusBar: { ...statusBar, contextStyle: value } };
338
- }
339
255
  if (id.startsWith('statusItem:')) {
340
256
  const item = id.slice('statusItem:'.length);
341
- if (isStatusItemId(item) && STATUS_COLOR_TOKENS.includes(value)) {
342
- return { ...prefs, statusBar: { ...statusBar, colors: { ...statusBar.colors, [item]: value } } };
343
- }
344
- if (isStatusItemId(item) && (value === 'shown' || value === 'hidden' || value === 'off')) {
345
- const wantVisible = value === 'shown';
346
- if (statusItemVisible(statusBar, item) === wantVisible)
347
- return prefs;
348
- return { ...prefs, statusBar: toggleStatusItem(statusBar, item) };
349
- }
350
- if (STATUS_GROUP_IDS.includes(item)) {
351
- return { ...prefs, statusBar: updateStatusGroup(statusBar, item, value) };
352
- }
257
+ if (!isStatusItemId(item))
258
+ return prefs;
259
+ if (value !== 'shown' && value !== 'hidden' && value !== 'off')
260
+ return prefs;
261
+ const resolved = resolveStatusBarConfig(statusBar);
262
+ if (statusItemVisible(resolved, item) === (value === 'shown'))
263
+ return prefs;
264
+ return { ...prefs, statusBar: toggleStatusItem(statusBar, item) };
353
265
  }
354
266
  return prefs;
355
267
  }
268
+ /**
269
+ * The document as it stands, ready for one edit.
270
+ *
271
+ * The narrowing is read from the *stored* document, not reconstructed from what
272
+ * the footer is showing. Reconstructing it turned every part the density happens
273
+ * to omit into an explicit hiding: pressing Space on one row then silently made
274
+ * a different density show less, which is the drift this replaced.
275
+ */
276
+ function storedStatusBar(prefs) {
277
+ return statusBarDocument(resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset));
278
+ }
356
279
  function adjacentValue(current, values, direction) {
357
280
  const index = values.indexOf(current);
358
281
  return values[(index + direction + values.length) % values.length] ?? values[0] ?? current;
359
282
  }
360
283
  /** Open the overlay on the current prefs, optionally focused on one row. */
361
- export function createSettings(prefs, focusId, agent, features, plugins, editors) {
362
- const items = tuiSettingItems(prefs, agent, features, plugins, editors);
284
+ export function createSettings(prefs, focusId, agent, features, plugins, editors, actions) {
285
+ const items = tuiSettingItems(prefs, agent, features, plugins, editors, actions);
363
286
  const focused = focusId === undefined ? 0 : items.findIndex((item) => item.id === focusId);
364
287
  return {
365
288
  prefs,
@@ -368,93 +291,73 @@ export function createSettings(prefs, focusId, agent, features, plugins, editors
368
291
  ...(features === undefined ? {} : { features }),
369
292
  ...(plugins === undefined ? {} : { plugins }),
370
293
  ...(editors === undefined ? {} : { editors }),
294
+ ...(actions === undefined ? {} : { actions }),
371
295
  };
372
296
  }
373
297
  function selectedStatusItem(state) {
374
- const id = tuiSettingItems(state.prefs, state.agent, state.features, state.plugins, state.editors)[state.selected]?.id;
298
+ const id = tuiSettingItems(state.prefs, state.agent, state.features, state.plugins, state.editors, state.actions)[state.selected]?.id;
375
299
  if (id?.startsWith('statusItem:') !== true)
376
300
  return undefined;
377
301
  const item = id.slice('statusItem:'.length);
378
302
  return isStatusItemId(item) ? item : undefined;
379
303
  }
380
- function startMoving(state, item) {
381
- const config = resolveStatusBarConfig(state.prefs.statusBar, state.prefs.statusPreset);
382
- const prefs = statusItemVisible(config, item)
383
- ? state.prefs
384
- : { ...state.prefs, statusBar: toggleStatusItem(config, item) };
385
- const selected = tuiSettingItems(prefs, state.agent, state.features, state.plugins, state.editors).findIndex(row => row.id === `statusItem:${item}`);
386
- return { ...state, prefs, selected: Math.max(0, selected), moving: item };
387
- }
388
- function assignItemSide(config, id, side) {
389
- if (itemSide(config, id) === side)
390
- return config;
391
- const next = { ...config, sides: { ...config.sides, [id]: side } };
392
- if (isStatusMetaId(id)) {
393
- const order = (next.metaOrder ?? STATUS_META_IDS).filter(item => item !== id);
394
- let last = -1;
395
- for (let index = 0; index < order.length; index += 1) {
396
- if (itemSide(next, order[index]) === side)
397
- last = index;
398
- }
399
- order.splice(last + 1, 0, id);
400
- return { ...next, metaOrder: order };
401
- }
402
- const order = (next.order ?? STATUS_GROUP_IDS).filter(item => item !== id);
403
- let last = -1;
404
- for (let index = 0; index < order.length; index += 1) {
405
- if (itemSide(next, order[index]) === side)
406
- last = index;
407
- }
408
- order.splice(last + 1, 0, id);
409
- return { ...next, order };
410
- }
411
- function moveStatusItem(state, direction) {
412
- const moving = state.moving;
413
- if (moving === undefined)
414
- return state;
415
- const config = resolveStatusBarConfig(state.prefs.statusBar, state.prefs.statusPreset);
416
- const side = itemSide(config, moving);
417
- if (isStatusMetaId(moving)) {
418
- const reordered = reorderVisible(config.meta.filter(id => itemSide(config, id) === side), config.metaOrder, moving, direction);
419
- const prefs = { ...state.prefs, statusBar: { ...config, meta: reordered.visible.concat(config.meta.filter(id => itemSide(config, id) !== side)), metaOrder: reordered.order } };
420
- const selected = tuiSettingItems(prefs, state.agent, state.features, state.plugins, state.editors).findIndex(row => row.id === `statusItem:${moving}`);
421
- return { ...state, prefs, selected, moving };
422
- }
423
- const reordered = reorderVisible(config.groups.filter(id => itemSide(config, id) === side), config.order, moving, direction);
424
- const prefs = { ...state.prefs, statusBar: { ...config, groups: reordered.visible.concat(config.groups.filter(id => itemSide(config, id) !== side)), order: reordered.order } };
425
- const selected = tuiSettingItems(prefs, state.agent, state.features, state.plugins, state.editors).findIndex(row => row.id === `statusItem:${moving}`);
426
- return { ...state, prefs, selected, moving };
427
- }
428
- function moveStatusItemSide(state, side) {
429
- const moving = state.moving;
430
- if (moving === undefined)
431
- return state;
432
- const config = resolveStatusBarConfig(state.prefs.statusBar, state.prefs.statusPreset);
433
- const prefs = { ...state.prefs, statusBar: assignItemSide(config, moving, side) };
434
- const selected = tuiSettingItems(prefs, state.agent, state.features, state.plugins, state.editors).findIndex(row => row.id === `statusItem:${moving}`);
435
- return { ...state, prefs, selected, moving };
304
+ function settingGroups(state) {
305
+ const owned = (section) => (state.plugins ?? [])
306
+ .filter(entry => (entry.section ?? 'advanced') === section)
307
+ .map(entry => ({ ...entry, id: `plugin:${entry.id}`, value: entry.inline?.value ?? entry.summary ?? 'Configure',
308
+ values: entry.inline?.choices.map(choice => choice.label) ?? [], resettable: (entry.fields?.length ?? 0) > 0 }));
309
+ return [
310
+ { id: 'general', label: 'Interface', items: [...generalSettingItems(state.prefs, state.editors).map(item => ({ ...item,
311
+ resettable: true, group: ['theme', 'colors', 'motion', 'mathMode', 'mermaidMode', 'hideThinking', 'terminalTitle', 'terminalState'].includes(item.id) ? 'Appearance'
312
+ : ['editor', 'mouseInteraction', 'pasteProtection', 'copyOnSelect'].includes(item.id) ? 'Input'
313
+ : ['checkUpdates', 'startupChangelog'].includes(item.id) ? 'Startup' : 'Notifications',
314
+ description: item.description,
315
+ })), ...(state.actions ?? []).filter(item => item.group === 'Input')].sort((a, b) => ['Appearance', 'Input', 'Notifications', 'Startup'].indexOf(a.group ?? '') - ['Appearance', 'Input', 'Notifications', 'Startup'].indexOf(b.group ?? '')) },
316
+ { id: 'agent', label: 'Agent', items: [...(state.actions ?? []).filter(item => item.group !== 'Input'), ...agentSettingItems(state.agent).map(item => ({ ...item, resettable: true, description: `${item.description}. Saved in this profile; applies from the next turn.` })), ...owned('agent')] },
317
+ { id: 'tools', label: 'Tools', items: [...(state.features === undefined ? [] : featureSettingItems(state.features)), ...owned('tools')] },
318
+ { id: 'status', label: 'Status line', items: [...statusSettingItems(state.prefs).map(item => ({ ...item, resettable: true })),
319
+ { id: 'statusReset', label: 'Restore layout', value: 'Restore', values: [], resettable: true, description: 'Restore the inherited status-line layout, colors and visibility. Confirmation shows what will be restored.' }] },
320
+ { id: 'integrations', label: 'Integrations', items: owned('integrations') },
321
+ { id: 'advanced', label: 'Advanced', items: owned('advanced') },
322
+ ].filter(group => group.items.length > 0);
436
323
  }
437
324
  function settingSections(state) {
438
- const generalEnd = generalSettingItems(state.prefs, state.editors).length;
439
- const agentEnd = generalEnd + agentSettingItems(state.agent).length;
440
- const featuresEnd = agentEnd + (state.features === undefined ? 0 : FEATURE_TOGGLES.length);
441
- const statusEnd = featuresEnd + statusSettingItems(state.prefs).length;
442
- return [
443
- { id: 'general', label: 'General', start: 0, end: generalEnd },
444
- ...(agentEnd === generalEnd ? [] : [{ id: 'agent', label: 'Agent', start: generalEnd, end: agentEnd }]),
445
- ...(featuresEnd === agentEnd
446
- ? []
447
- : [{ id: 'features', label: 'Features', start: agentEnd, end: featuresEnd }]),
448
- { id: 'status', label: 'Status line', start: featuresEnd, end: statusEnd },
449
- ...(!state.plugins?.length ? [] : [{ id: 'plugins', label: 'Plugins', start: statusEnd, end: statusEnd + state.plugins.length }]),
450
- ];
325
+ let start = 0;
326
+ return settingGroups(state).map(group => {
327
+ const section = { id: group.id, label: group.label, start, end: start + group.items.length };
328
+ start = section.end;
329
+ return section;
330
+ });
451
331
  }
452
332
  function selectedSection(state) {
453
333
  const sections = settingSections(state);
454
334
  return sections.find(section => state.selected >= section.start && state.selected < section.end)
455
335
  ?? sections[0];
456
336
  }
337
+ function settingsMatches(state) {
338
+ let index = 0;
339
+ const rows = settingGroups(state).flatMap(group => group.items.map(item => ({ item, index: index++, section: group.label })));
340
+ return rankSearchResults(rows, state.search ?? '', row => [row.item.label, row.item.description, row.section, row.item.group ?? '', ...row.item.keywords ?? []]);
341
+ }
342
+ function searchSettings(state, query) {
343
+ const next = { ...state, search: query };
344
+ const rows = settingsMatches(next);
345
+ return { ...next, selected: rows.find(row => row.index === state.selected)?.index ?? rows[0]?.index ?? state.selected };
346
+ }
347
+ /** Preserve selection and a cross-category search when live configuration refreshes. */
348
+ export function refreshSettings(state, next) {
349
+ const id = tuiSettingItems(state.prefs, state.agent, state.features, state.plugins, state.editors, state.actions)[state.selected]?.id;
350
+ const selected = tuiSettingItems(next.prefs, next.agent, next.features, next.plugins, next.editors, next.actions).findIndex(item => item.id === id);
351
+ const updated = { ...next, selected: selected < 0 ? next.selected : selected,
352
+ ...(state.searchOrigin === undefined ? {} : { searchOrigin: state.searchOrigin }) };
353
+ return state.search === undefined ? updated : searchSettings(updated, state.search);
354
+ }
457
355
  function moveSelected(state, next) {
356
+ if (state.search !== undefined) {
357
+ const rows = settingsMatches(state), current = rows.findIndex(row => row.index === state.selected);
358
+ const index = rows[Math.max(0, Math.min(rows.length - 1, current + next - state.selected))]?.index;
359
+ return index === undefined ? state : { ...state, selected: index };
360
+ }
458
361
  const section = selectedSection(state);
459
362
  const selected = Math.max(section.start, Math.min(next, section.end - 1));
460
363
  if (selected === state.selected)
@@ -466,15 +369,25 @@ function moveSection(state, direction) {
466
369
  const active = selectedSection(state);
467
370
  const current = sections.findIndex(section => section.id === active.id);
468
371
  const next = (current + direction + sections.length) % sections.length;
469
- return { ...state, selected: sections[next]?.start ?? 0 };
372
+ const { search: _, searchOrigin: __, ...browse } = state;
373
+ return { ...browse, selected: sections[next]?.start ?? 0 };
470
374
  }
471
375
  function cycleSelected(state, direction = 1) {
472
- const items = tuiSettingItems(state.prefs, state.agent, state.features, state.plugins, state.editors);
376
+ const items = tuiSettingItems(state.prefs, state.agent, state.features, state.plugins, state.editors, state.actions);
473
377
  const item = items[state.selected];
474
378
  if (item === undefined)
475
379
  return { kind: 'ignore' };
476
- if (item.id.startsWith('plugin:'))
380
+ if (item.action !== undefined)
477
381
  return { kind: 'ignore' };
382
+ if (item.id.startsWith('plugin:')) {
383
+ const entry = state.plugins?.find(entry => `plugin:${entry.id}` === item.id);
384
+ const choices = entry?.inline?.choices;
385
+ if (entry === undefined || choices === undefined || choices.length === 0)
386
+ return { kind: 'ignore' };
387
+ const current = choices.findIndex(choice => choice.label === item.value);
388
+ const next = current < 0 ? direction === 1 ? 0 : choices.length - 1 : (current + direction + choices.length) % choices.length;
389
+ return { kind: 'set-plugin', entry, value: choices[next].value };
390
+ }
478
391
  const value = adjacentValue(item.value, item.values, direction);
479
392
  if (item.id === 'editor') {
480
393
  const editor = state.editors?.find(choice => choice.label === value)?.id ?? 'auto';
@@ -502,25 +415,43 @@ function cycleSelected(state, direction = 1) {
502
415
  }
503
416
  /** Apply one decoded event to the overlay. */
504
417
  export function applySettingsEvent(state, event) {
505
- if ((event.type === 'key' && event.id === 'enter') || (event.type === 'text' && event.value === ' ')) {
506
- const selected = tuiSettingItems(state.prefs, state.agent, state.features, state.plugins, state.editors)[state.selected];
507
- if (selected?.id.startsWith('plugin:'))
508
- return { kind: 'open-plugin', id: selected.id.slice('plugin:'.length) };
509
- }
510
- if (state.moving !== undefined) {
511
- if (event.type !== 'key')
512
- return { kind: 'ignore' };
513
- if (event.id === 'up' || event.id === 'down') {
514
- return { kind: 'apply', domain: 'tui', state: moveStatusItem(state, event.id === 'up' ? -1 : 1) };
418
+ {
419
+ if (event.type === 'text' && (state.search !== undefined || event.value !== ' ')) {
420
+ const origin = tuiSettingItems(state.prefs, state.agent, state.features, state.plugins, state.editors, state.actions)[state.selected]?.id;
421
+ const searching = state.search === undefined ? { ...state, search: '', ...(origin === undefined ? {} : { searchOrigin: origin }) } : state;
422
+ return { kind: 'update', state: searchSettings(searching, (searching.search ?? '') + (event.value === '/' && state.search === undefined ? '' : event.value.replace(/[\r\n]/gu, ' '))) };
515
423
  }
516
- if (event.id === 'left' || event.id === 'right') {
517
- return { kind: 'apply', domain: 'tui', state: moveStatusItemSide(state, event.id === 'left' ? 'left' : 'right') };
424
+ if (state.search !== undefined && event.type === 'key') {
425
+ if (event.id === 'escape' || event.id === 'ctrl+c') {
426
+ if (state.search !== '')
427
+ return { kind: 'update', state: searchSettings(state, '') };
428
+ const { search: _, searchOrigin, ...browse } = state;
429
+ const index = tuiSettingItems(state.prefs, state.agent, state.features, state.plugins, state.editors, state.actions).findIndex(item => item.id === searchOrigin);
430
+ return { kind: 'update', state: { ...browse, selected: index < 0 ? state.selected : index } };
431
+ }
432
+ if (event.id === 'backspace' || event.id === 'ctrl+u')
433
+ return { kind: 'update', state: searchSettings(state, event.id === 'ctrl+u' ? '' : state.search.slice(0, moveGraphemeLeft(state.search, state.search.length))) };
434
+ if (event.id === 'home' || event.id === 'end') {
435
+ const rows = settingsMatches(state);
436
+ return { kind: 'update', state: { ...state, selected: (event.id === 'home' ? rows[0] : rows.at(-1))?.index ?? state.selected } };
437
+ }
438
+ if (settingsMatches(state).length === 0 && ['enter', 'left', 'right', 'ctrl+r'].includes(event.id))
439
+ return { kind: 'ignore' };
518
440
  }
519
- if (event.id === 'enter' || event.id === 'escape' || event.id === 'ctrl+c') {
520
- const { moving: _, ...rest } = state;
521
- return { kind: 'update', state: rest };
441
+ const selected = tuiSettingItems(state.prefs, state.agent, state.features, state.plugins, state.editors, state.actions)[state.selected];
442
+ if (event.type === 'key' && event.id === 'ctrl+r' && selected?.resettable) {
443
+ const entry = state.plugins?.find(entry => `plugin:${entry.id}` === selected.id);
444
+ return { kind: 'reset', id: selected.id, label: selected.label, ...(entry === undefined ? {} : { entry }) };
522
445
  }
523
- return { kind: 'ignore' };
446
+ if (event.type === 'key' && event.id === 'enter' && selected?.id === 'statusReset')
447
+ return { kind: 'reset', id: selected.id, label: selected.label };
448
+ if (selected?.action !== undefined && event.type === 'key' && event.id === 'enter')
449
+ return { kind: 'open-command', name: selected.action };
450
+ }
451
+ if ((event.type === 'key' && event.id === 'enter') || (event.type === 'text' && event.value === ' ')) {
452
+ const selected = tuiSettingItems(state.prefs, state.agent, state.features, state.plugins, state.editors, state.actions)[state.selected];
453
+ if (selected?.id.startsWith('plugin:') && (event.type === 'key' || selected.values.length === 0))
454
+ return { kind: 'open-plugin', id: selected.id.slice('plugin:'.length) };
524
455
  }
525
456
  if (event.type === 'text' && event.value === ' ') {
526
457
  const item = selectedStatusItem(state);
@@ -531,10 +462,7 @@ export function applySettingsEvent(state, event) {
531
462
  domain: 'tui',
532
463
  state: {
533
464
  ...state,
534
- prefs: {
535
- ...state.prefs,
536
- statusBar: toggleStatusItem(resolveStatusBarConfig(state.prefs.statusBar, state.prefs.statusPreset), item),
537
- },
465
+ prefs: { ...state.prefs, statusBar: toggleStatusItem(storedStatusBar(state.prefs), item) },
538
466
  },
539
467
  };
540
468
  }
@@ -542,33 +470,31 @@ export function applySettingsEvent(state, event) {
542
470
  return { kind: 'ignore' };
543
471
  switch (event.id) {
544
472
  case 'enter':
545
- {
546
- const item = selectedStatusItem(state);
547
- if (item !== undefined)
548
- return { kind: 'apply', domain: 'tui', state: startMoving(state, item) };
549
- }
550
473
  return cycleSelected(state);
551
474
  case 'right':
552
475
  return cycleSelected(state);
553
476
  case 'left':
554
477
  return cycleSelected(state, -1);
555
- case 'escape':
556
- case 'ctrl+c':
557
- return { kind: 'close' };
558
- case 'up':
559
- return { kind: 'update', state: moveSelected(state, state.selected - 1) };
560
- case 'down':
561
- return { kind: 'update', state: moveSelected(state, state.selected + 1) };
478
+ // This overlay keeps three keys that a plain list would spend on movement:
479
+ // `tab` switches section, and `home`/`end` jump to the edge of the current
480
+ // section rather than of the whole list, which is the only reading that makes
481
+ // sense when the rows above belong to another category.
562
482
  case 'tab':
563
483
  return { kind: 'update', state: moveSection(state, 1) };
564
484
  case 'shift+tab':
565
485
  return { kind: 'update', state: moveSection(state, -1) };
566
486
  case 'home':
567
- return { kind: 'update', state: moveSelected(state, 0) };
487
+ return { kind: 'update', state: moveSelected(state, selectedSection(state).start) };
568
488
  case 'end':
569
489
  return { kind: 'update', state: moveSelected(state, selectedSection(state).end - 1) };
570
- default:
490
+ default: {
491
+ const navigation = listNavigation(event);
492
+ if (navigation.kind === 'close')
493
+ return { kind: 'close' };
494
+ if (navigation.kind === 'move')
495
+ return { kind: 'update', state: moveSelected(state, state.selected + navigation.delta) };
571
496
  return { kind: 'ignore' };
497
+ }
572
498
  }
573
499
  }
574
500
  /** First overlay-local row that paints a setting item. */
@@ -578,14 +504,11 @@ export const SETTINGS_ITEM_ROW = 3;
578
504
  const HOTKEY_NAVIGATE = { keys: '↑↓', action: 'Navigate rows' };
579
505
  const HOTKEY_CHANGE = { keys: '←→', action: 'Change the selected value' };
580
506
  const HOTKEY_TOGGLE = { keys: 'Space', action: 'Toggle — show or hide a status item, or cycle other rows' };
581
- const HOTKEY_MOVE = { keys: 'Enter', action: 'Change — move a status item, edit a plugin, or cycle other rows' };
507
+ const HOTKEY_MOVE = { keys: 'Enter', action: 'Change — open configuration, or cycle other rows' };
582
508
  const HOTKEY_SECTION = { keys: 'Tab / Shift+Tab', action: 'Switch section' };
583
509
  const HOTKEY_EDGE = { keys: 'Home / End', action: 'Jump to the section edges' };
584
- const HOTKEY_CLOSE = { keys: 'Esc', action: 'Close the overlay' };
585
- const HOTKEY_CANCEL = { keys: 'Ctrl+C', action: 'Close the overlay, or place the grabbed item while moving' };
586
- const HOTKEY_ORDER = { keys: '↑↓', action: 'Order the grabbed item while moving' };
587
- const HOTKEY_COLUMN = { keys: '←→', action: 'Column — choose the footer side while moving' };
588
- const HOTKEY_PLACE = { keys: 'Enter / Esc', action: 'Place the grabbed item and stop moving' };
510
+ const HOTKEY_CLOSE = { keys: 'Esc', action: 'Clear search, return to browsing, or close the overlay' };
511
+ const HOTKEY_CANCEL = { keys: 'Ctrl+C', action: 'Close the overlay' };
589
512
  /** Keys the settings overlay accepts; `/help` and its bottom hint read this list. */
590
513
  export const SETTINGS_HOTKEYS = [
591
514
  HOTKEY_NAVIGATE,
@@ -594,17 +517,15 @@ export const SETTINGS_HOTKEYS = [
594
517
  HOTKEY_MOVE,
595
518
  HOTKEY_SECTION,
596
519
  HOTKEY_EDGE,
520
+ { keys: '/ or Text', action: 'Search across all categories' },
521
+ { keys: 'Backspace / Ctrl+U', action: 'Edit or clear the settings search' },
522
+ { keys: 'Ctrl+R', action: 'Restore inheritance for the selected setting' },
597
523
  HOTKEY_CLOSE,
598
524
  HOTKEY_CANCEL,
599
- HOTKEY_ORDER,
600
- HOTKEY_COLUMN,
601
- HOTKEY_PLACE,
602
525
  ];
603
526
  /** Bottom hint for the overlay; the grabbed-row state swaps in its own gestures. */
604
- function settingsHints(moving) {
605
- return moving
606
- ? formatOverlayHint([HOTKEY_ORDER, HOTKEY_COLUMN, HOTKEY_PLACE])
607
- : formatOverlayHint([HOTKEY_NAVIGATE, HOTKEY_CHANGE, HOTKEY_TOGGLE, HOTKEY_MOVE, HOTKEY_CLOSE]);
527
+ function settingsHints() {
528
+ return '↑↓ select · ←→ change · Tab sections · / search · Ctrl+R reset · Esc back';
608
529
  }
609
530
  function fit(text, width) {
610
531
  if (width <= 0)
@@ -616,7 +537,7 @@ function border(theme, text) {
616
537
  }
617
538
  function topBorder(theme, width) {
618
539
  const inner = Math.max(0, width - 2);
619
- const title = truncateToWidth(' Settings ', Math.max(0, inner - 2));
540
+ const title = truncateToWidth(' Settings · Profile ', Math.max(0, inner - 2));
620
541
  const fill = Math.max(0, inner - 1 - visibleWidth(title));
621
542
  return border(theme, BOX.topLeft + BOX.horizontal)
622
543
  + theme.bold(theme.fg('accent', title))
@@ -637,7 +558,9 @@ function renderSectionTabs(active, sections, theme, width) {
637
558
  ? theme.bold(theme.fg('accent', `● ${label}`))
638
559
  : theme.fg('muted', `○ ${label}`);
639
560
  const all = ' ' + sections.map(section => tab(section.id, section.label)).join(' ');
640
- return framedRow(theme, visibleWidth(all) <= width - 4 ? all : ' ' + tab(active, sections.find(section => section.id === active)?.label ?? active), width);
561
+ const current = sections.findIndex(section => section.id === active);
562
+ return framedRow(theme, visibleWidth(all) <= width - 4 ? all
563
+ : ` ‹ ${tab(active, sections[current]?.label ?? active)} · ${current + 1}/${sections.length} ›`, width);
641
564
  }
642
565
  const STATUS_PREVIEW_STATS = {
643
566
  turns: 3,
@@ -680,7 +603,7 @@ function renderStatusPreview(state, theme, width, rows) {
680
603
  }
681
604
  /** Fullscreen OMP-style settings frame with stable chrome and section tabs. */
682
605
  export function renderSettings(state, theme, width, height = 24) {
683
- const items = tuiSettingItems(state.prefs, state.agent, state.features, state.plugins, state.editors);
606
+ const items = tuiSettingItems(state.prefs, state.agent, state.features, state.plugins, state.editors, state.actions);
684
607
  const index = Math.max(0, Math.min(state.selected, Math.max(0, items.length - 1)));
685
608
  const viewState = index === state.selected ? state : { ...state, selected: index };
686
609
  const sections = settingSections(viewState);
@@ -697,39 +620,61 @@ export function renderSettings(state, theme, width, height = 24) {
697
620
  ? renderStatusPreview(state, theme, width, maxPreviewContent)
698
621
  : [];
699
622
  const previewRows = previewLines.length === 0 ? 0 : previewLines.length + 1;
700
- const viewportHeight = Math.max(1, height - 6 - descriptionRows - previewRows);
701
- const localSelection = index - sectionStart;
702
- const visibleCount = Math.min(viewportHeight, sectionItems.length);
703
- const start = Math.max(0, Math.min(localSelection - Math.floor(visibleCount / 2), sectionItems.length - visibleCount));
704
- const visibleItems = sectionItems.slice(start, start + visibleCount);
705
- const selected = items[index];
706
- const lines = [topBorder(theme, width), renderSectionTabs(active, sections, theme, width), divider(theme, width)];
623
+ const matches = state.search === undefined ? undefined : settingsMatches(state);
624
+ let previousGroup;
625
+ const rows = matches ?? sectionItems.flatMap((item, offset) => {
626
+ const heading = item.group !== undefined && item.group !== previousGroup ? [{ group: item.group }] : [];
627
+ previousGroup = item.group;
628
+ return [...heading, { item, index: sectionStart + offset }];
629
+ });
630
+ const viewportHeight = Math.max(1, height - 6 - descriptionRows - previewRows - (matches === undefined ? 0 : 1));
631
+ const localSelection = rows.findIndex(row => 'item' in row && row.index === index);
632
+ const visibleCount = Math.min(viewportHeight, rows.length);
633
+ const start = Math.max(0, Math.min(localSelection - Math.floor(visibleCount / 2), rows.length - visibleCount));
634
+ const visibleItems = rows.slice(start, start + visibleCount);
635
+ const selected = matches?.length === 0 ? undefined : items[index];
636
+ const lines = [topBorder(theme, width), renderSectionTabs(active, sections, theme, width)];
637
+ if (matches !== undefined)
638
+ lines.push(framedRow(theme, `Search: ${state.search || 'type to filter'} · ${matches.length} matches`, width));
639
+ lines.push(divider(theme, width));
707
640
  if (previewLines.length > 0) {
708
641
  lines.push(...previewLines, divider(theme, width));
709
642
  }
710
643
  let cursorRow = SETTINGS_ITEM_ROW;
711
644
  for (let row = 0; row < viewportHeight; row += 1) {
712
- const item = visibleItems[row];
713
- if (item === undefined) {
714
- lines.push(framedRow(theme, '', width));
645
+ const entry = visibleItems[row];
646
+ if (entry === undefined) {
647
+ lines.push(framedRow(theme, row === 0 && matches?.length === 0 ? theme.fg('muted', 'No matching settings') : '', width));
648
+ continue;
649
+ }
650
+ if ('group' in entry) {
651
+ lines.push(framedRow(theme, theme.fg('dim', ` ${entry.group}`), width));
715
652
  continue;
716
653
  }
717
- const itemIndex = sectionStart + start + row;
718
- const selectedRow = itemIndex === index;
654
+ const item = entry.item;
655
+ const selectedRow = entry.index === index;
719
656
  if (selectedRow)
720
657
  cursorRow = lines.length;
721
658
  const above = start > 0 && row === 0 ? theme.fg('dim', '↑ ') : '';
722
- const below = start + visibleCount < sectionItems.length && row === viewportHeight - 1 ? theme.fg('dim', '↓ ') : '';
723
- lines.push(framedRow(theme, renderSettingRow(item, selectedRow, theme, Math.max(1, width - 4), above || below, state.moving !== undefined && item.id === `statusItem:${state.moving}`), width));
659
+ const below = start + visibleCount < rows.length && row === viewportHeight - 1 ? theme.fg('dim', '↓ ') : '';
660
+ lines.push(framedRow(theme, renderSettingRow(entry.section === undefined ? item : { ...item, label: `${item.label} · ${entry.section}` }, selectedRow, theme, Math.max(1, width - 4), above || below, false), width));
724
661
  }
725
662
  lines.push(divider(theme, width));
726
- const description = selected?.description ?? '';
727
- const descriptionLines = wrapText(description, Math.max(1, width - 8)).slice(0, descriptionRows);
663
+ const description = selected === undefined ? '' : selected.description;
664
+ const descriptionLines = wrapTextStable(description, Math.max(1, width - 8)).slice(0, descriptionRows);
728
665
  for (let row = 0; row < descriptionRows; row += 1) {
729
666
  const text = descriptionLines[row] ?? '';
730
667
  lines.push(framedRow(theme, text === '' ? '' : ' ' + theme.fg('muted', text), width));
731
668
  }
732
- const hints = theme.fg('dim', active === 'plugins' ? '↑↓ navigate · enter edit · tab section · esc close' : settingsHints(state.moving !== undefined));
669
+ const fullHint = matches !== undefined ? 'Type filter · ↑↓ select · ←→ change · Enter edit · Tab sections · Esc clear/back'
670
+ : selected?.action !== undefined ? 'Enter open · Tab sections · / search · Esc back'
671
+ : selected?.id.startsWith('plugin:') === true ? `Enter configure${selected.values.length > 0 ? ' · ←→ change' : ''} · Tab sections · / search${selected.resettable ? ' · Ctrl+R reset' : ''} · Esc back`
672
+ : selected?.id.startsWith('statusItem:') === true
673
+ ? 'Space show/hide · / search · Esc back'
674
+ : selected?.resettable === false ? '←→ change · Tab sections · / search · Esc back' : settingsHints();
675
+ const hints = theme.fg('dim', width < 60
676
+ ? matches !== undefined ? 'Type filter · ↑↓ · Esc clear/back' : 'Tab sections · / search · Esc back'
677
+ : fullHint);
733
678
  lines.push(framedRow(theme, hints, width), bottomBorder(theme, width));
734
679
  return {
735
680
  lines,
@@ -737,18 +682,25 @@ export function renderSettings(state, theme, width, height = 24) {
737
682
  };
738
683
  }
739
684
  function renderSettingRow(item, selected, theme, width, edge = '', moving = false) {
740
- const cursor = selected ? theme.fg('accent', (moving ? '↕' : SYMBOL.cursor) + ' ') : ' ';
741
- const label = selected
742
- ? theme.bold(theme.fg('accent', item.label))
743
- : item.hidden === true ? theme.fg('dim', item.label) : item.label;
744
- const sample = item.sample === undefined
745
- ? ''
746
- : ' ' + theme.fg(item.hidden === true ? 'dim' : 'muted', item.sample);
685
+ // A colour value keeps its swatch tone only when the row is not selected;
686
+ // selection has to win, or the reader cannot tell which row the cursor is on.
747
687
  const rawValue = moving ? 'moving' : item.value;
748
- const value = selected || item.swatch === undefined
749
- ? theme.fg(selected ? 'accent' : 'muted', rawValue)
750
- : theme.fg(item.swatch, rawValue);
751
- const fill = Math.max(1, width - visibleWidth(cursor) - visibleWidth(label) - visibleWidth(sample) - visibleWidth(value) - visibleWidth(edge));
752
- return truncateToWidth(cursor + label + sample + ' '.repeat(fill) + value + edge, width);
688
+ const value = selected || item.swatch === undefined ? rawValue : theme.fg(item.swatch, rawValue);
689
+ const row = {
690
+ label: item.label,
691
+ ...(item.sample === undefined ? {} : { description: item.sample }),
692
+ secondary: item.hidden === true,
693
+ value,
694
+ };
695
+ const rendered = renderListRowWithValue(row, theme, width, visibleWidth(item.label), {
696
+ selected,
697
+ ...(moving ? { moving } : {}),
698
+ });
699
+ // An edge mark replaces the cursor cell, which the shared row already
700
+ // reserved, so the row cannot shift when the reader reaches either end.
701
+ if (edge === '')
702
+ return rendered;
703
+ const mark = theme.fg('dim', edge.trimStart() + ' ');
704
+ return truncateToWidth(mark + rendered.slice(LIST_CURSOR_CELLS), width);
753
705
  }
754
706
  //# sourceMappingURL=settings-list.js.map