micro-models-agent 2.10.0 → 2.12.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 (359) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/assets/app.js +22 -4
  3. package/dist/assets/index.html +29 -0
  4. package/dist/assets/js/dom.js +2 -1
  5. package/dist/assets/js/feed.js +38 -26
  6. package/dist/assets/js/format.js +1 -1
  7. package/dist/assets/js/rail.js +114 -0
  8. package/dist/assets/js/settings-controls.js +2 -2
  9. package/dist/assets/js/settings.js +246 -51
  10. package/dist/assets/js/store.js +3 -0
  11. package/dist/assets/js/tabs.js +104 -2
  12. package/dist/assets/styles.css +215 -66
  13. package/dist/cli/cache-line.js +1 -0
  14. package/dist/cli/command-suggest.js +5 -4
  15. package/dist/cli/commands.js +231 -122
  16. package/dist/cli/completer.js +132 -31
  17. package/dist/cli/index.js +1 -0
  18. package/dist/cli/json-payload.js +4 -4
  19. package/dist/cli/main.js +166 -61
  20. package/dist/cli/onboarding-commands.js +126 -0
  21. package/dist/cli/plugin-commands.js +2 -1
  22. package/dist/cli/relaunch.js +16 -13
  23. package/dist/cli/repl-commands-lsp.js +75 -0
  24. package/dist/cli/repl-commands-skills.js +105 -0
  25. package/dist/cli/repl-commands.js +589 -266
  26. package/dist/cli/repl.js +436 -177
  27. package/dist/cli/run-result.js +5 -6
  28. package/dist/cli/security-commands.js +37 -28
  29. package/dist/cli/setup-order.js +4 -55
  30. package/dist/cli/setup-prompt.js +3 -2
  31. package/dist/cli/setup.js +27 -107
  32. package/dist/cli/web-command.js +98 -0
  33. package/dist/config/budget.js +5 -5
  34. package/dist/config/config.js +103 -96
  35. package/dist/config/defaults.js +38 -8
  36. package/dist/config/domains.js +24 -27
  37. package/dist/config/env.js +46 -0
  38. package/dist/config/experts.js +2 -0
  39. package/dist/config/index.js +1 -0
  40. package/dist/config/paths.js +56 -0
  41. package/dist/config/redact.js +44 -0
  42. package/dist/config/registry-extra.js +334 -0
  43. package/dist/config/registry.js +584 -0
  44. package/dist/config/security.js +35 -36
  45. package/dist/config/timeouts.js +74 -0
  46. package/dist/config/utils.js +16 -15
  47. package/dist/core/agent/artifact-progress.js +59 -0
  48. package/dist/core/agent/artifact-resolve.js +79 -0
  49. package/dist/core/agent/audit-gate.js +7 -3
  50. package/dist/core/agent/compaction.js +2 -2
  51. package/dist/core/agent/constants.js +33 -4
  52. package/dist/core/agent/context-log.js +454 -0
  53. package/dist/core/agent/context-renderer.js +5 -0
  54. package/dist/core/agent/critic-gate.js +132 -0
  55. package/dist/core/agent/hallucination-gate.js +148 -38
  56. package/dist/core/agent/loop-state.js +15 -0
  57. package/dist/core/agent/prefix-monitor.js +1 -1
  58. package/dist/core/agent/reasoning-resolver.js +10 -1
  59. package/dist/core/agent/token-tracker.js +7 -4
  60. package/dist/core/agent/tool-batch.js +93 -18
  61. package/dist/core/agent/tool-images.js +18 -0
  62. package/dist/core/agent/tool-output.js +65 -11
  63. package/dist/core/agent/tool-pairing.js +60 -0
  64. package/dist/core/agent-moe.js +25 -24
  65. package/dist/core/agent.js +429 -108
  66. package/dist/core/bootstrap/agent-phase.js +66 -0
  67. package/dist/core/bootstrap/config-phase.js +86 -0
  68. package/dist/core/bootstrap/modules-phase.js +269 -0
  69. package/dist/core/bootstrap/provider-phase.js +183 -0
  70. package/dist/core/bootstrap/session-phase.js +204 -0
  71. package/dist/core/bootstrap/state.js +94 -0
  72. package/dist/core/bootstrap/workspace-phase.js +115 -0
  73. package/dist/core/bootstrap.js +92 -660
  74. package/dist/core/crash-handler.js +17 -7
  75. package/dist/core/environment.js +37 -12
  76. package/dist/core/host-plan.js +43 -0
  77. package/dist/core/host.js +23 -0
  78. package/dist/core/index.js +1 -0
  79. package/dist/core/prompt-builder.js +17 -1
  80. package/dist/core/prompt-overflow.js +42 -43
  81. package/dist/core/session-logger.js +122 -30
  82. package/dist/core/system-prompt.js +117 -0
  83. package/dist/core/version.js +6 -4
  84. package/dist/core/web-host.js +34 -0
  85. package/dist/core/workspace.js +19 -15
  86. package/dist/i18n/en.json +106 -21
  87. package/dist/i18n/index.js +37 -3
  88. package/dist/i18n/ru.json +109 -21
  89. package/dist/index.js +7 -3
  90. package/dist/llm/image-utils.js +55 -64
  91. package/dist/llm/llm-errors.js +9 -0
  92. package/dist/llm/model-loader.js +10 -32
  93. package/dist/llm/models-dev.js +121 -0
  94. package/dist/llm/openai-compat.js +146 -73
  95. package/dist/llm/orchestrator.js +33 -11
  96. package/dist/llm/provider-budget.js +2 -2
  97. package/dist/llm/provider-context.js +233 -0
  98. package/dist/llm/provider.js +1 -1
  99. package/dist/llm/response.js +5 -0
  100. package/dist/llm/token-counter.js +27 -11
  101. package/dist/logger/app-logger.js +48 -22
  102. package/dist/logger/file-log.js +28 -19
  103. package/dist/logger/index.js +1 -0
  104. package/dist/main.js +24285 -22447
  105. package/dist/migration/backup.js +14 -3
  106. package/dist/migration/detect.js +12 -3
  107. package/dist/migration/index.js +1 -0
  108. package/dist/modules/artifacts/store.js +9 -8
  109. package/dist/modules/browser/actions.js +6 -0
  110. package/dist/modules/browser/bridge-client.js +14 -12
  111. package/dist/modules/browser/bridge-path.js +4 -4
  112. package/dist/modules/browser/console-buffer.js +37 -0
  113. package/dist/modules/browser/cookie-store.js +5 -0
  114. package/dist/modules/browser/driver-types.js +1 -0
  115. package/dist/modules/browser/driver.js +19 -11
  116. package/dist/modules/browser/index.js +3 -1
  117. package/dist/modules/browser/module.js +2 -1
  118. package/dist/modules/browser/session.js +10 -30
  119. package/dist/modules/browser/snapshot.js +19 -13
  120. package/dist/modules/browser/types.js +1 -0
  121. package/dist/modules/certification/cli.js +28 -20
  122. package/dist/modules/certification/fact-checker.js +9 -1
  123. package/dist/modules/certification/loader.js +9 -2
  124. package/dist/modules/certification/manifest.js +83 -21
  125. package/dist/modules/certification/runner.js +23 -13
  126. package/dist/modules/certification/scenarios.js +20 -20
  127. package/dist/modules/certification/syntax-scenarios.js +1 -2
  128. package/dist/modules/context/chunk-query.js +11 -9
  129. package/dist/modules/context/fact-extractor.js +37 -39
  130. package/dist/modules/context/index.js +1 -0
  131. package/dist/modules/context/manager.js +106 -80
  132. package/dist/modules/critic/client.js +176 -0
  133. package/dist/modules/critic/evidence.js +81 -0
  134. package/dist/modules/critic/provider.js +45 -0
  135. package/dist/modules/critic/tools.js +78 -0
  136. package/dist/modules/critic/types.js +1 -0
  137. package/dist/modules/execution/audit-runners.js +28 -34
  138. package/dist/modules/execution/auditor.js +53 -49
  139. package/dist/modules/execution/execution-plugin.js +142 -102
  140. package/dist/modules/execution/index.js +1 -1
  141. package/dist/modules/execution/input-from.js +37 -13
  142. package/dist/modules/execution/module.js +368 -191
  143. package/dist/modules/execution/moe-executor.js +53 -26
  144. package/dist/modules/execution/plan-actions.js +320 -202
  145. package/dist/modules/execution/plan-checks.js +35 -0
  146. package/dist/modules/execution/plan-coverage.js +36 -33
  147. package/dist/modules/execution/plan-steps.js +11 -3
  148. package/dist/modules/execution/plan-store.js +83 -17
  149. package/dist/modules/execution/plan-tool.js +89 -35
  150. package/dist/modules/execution/plan-validator.js +30 -10
  151. package/dist/modules/execution/planner.js +22 -13
  152. package/dist/modules/execution/stuck-detector.js +196 -93
  153. package/dist/modules/execution/tracker.js +22 -8
  154. package/dist/modules/execution/verifier.js +69 -48
  155. package/dist/modules/execution/windows-commands.js +12 -12
  156. package/dist/modules/hallucination/artifact-claim.js +93 -0
  157. package/dist/modules/hallucination/confidence.js +13 -11
  158. package/dist/modules/hallucination/consistency.js +10 -2
  159. package/dist/modules/hallucination/detector.js +10 -5
  160. package/dist/modules/hallucination/factual.js +43 -26
  161. package/dist/modules/hallucination/index.js +1 -0
  162. package/dist/modules/hallucination/js-identifiers.js +44 -45
  163. package/dist/modules/hallucination/llm-judge.js +17 -12
  164. package/dist/modules/hallucination/success-claim.js +59 -0
  165. package/dist/modules/indexer/cache.js +52 -4
  166. package/dist/modules/indexer/index.js +1 -0
  167. package/dist/modules/indexer/map-command.js +2 -4
  168. package/dist/modules/indexer/map-select.js +16 -16
  169. package/dist/modules/indexer/module.js +47 -12
  170. package/dist/modules/indexer/project-profile.js +8 -7
  171. package/dist/modules/indexer/symbols.js +15 -15
  172. package/dist/modules/indexer/walker.js +159 -29
  173. package/dist/modules/lsp/check-tool.js +7 -5
  174. package/dist/modules/lsp/client.js +39 -39
  175. package/dist/modules/lsp/command.js +17 -24
  176. package/dist/modules/lsp/config.js +12 -11
  177. package/dist/modules/lsp/index.js +1 -0
  178. package/dist/modules/lsp/module.js +48 -46
  179. package/dist/modules/lsp/probe.js +13 -14
  180. package/dist/modules/lsp/project-root.js +14 -17
  181. package/dist/modules/lsp/startup-check.js +18 -16
  182. package/dist/modules/mcp/client.js +59 -62
  183. package/dist/modules/mcp/index.js +1 -0
  184. package/dist/modules/mcp/module.js +66 -37
  185. package/dist/modules/mcp/registry.js +5 -0
  186. package/dist/modules/memory/index.js +1 -0
  187. package/dist/modules/memory/module.js +12 -8
  188. package/dist/modules/memory/search.js +4 -1
  189. package/dist/modules/memory/store.js +40 -2
  190. package/dist/modules/onboarding/init.js +47 -0
  191. package/dist/modules/onboarding/meet.js +30 -0
  192. package/dist/modules/onboarding/mode.js +30 -0
  193. package/dist/modules/onboarding/prompts.js +43 -0
  194. package/dist/modules/pipelines/engine.js +10 -0
  195. package/dist/modules/pipelines/index.js +1 -0
  196. package/dist/modules/pipelines/parser.js +4 -1
  197. package/dist/modules/pipelines/template.js +4 -0
  198. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  199. package/dist/modules/plugins/builtin/notify.js +2 -0
  200. package/dist/modules/plugins/index.js +1 -0
  201. package/dist/modules/plugins/loader.js +18 -3
  202. package/dist/modules/plugins/manager.js +37 -17
  203. package/dist/modules/pricing/index.js +10 -11
  204. package/dist/modules/pricing/prices.js +12 -12
  205. package/dist/modules/processes/index.js +1 -0
  206. package/dist/modules/processes/kill-tree.js +27 -31
  207. package/dist/modules/processes/registry.js +44 -25
  208. package/dist/modules/processes/runner.js +4 -4
  209. package/dist/modules/providers/capabilities.js +28 -0
  210. package/dist/modules/providers/create.js +3 -4
  211. package/dist/modules/providers/factory.js +7 -7
  212. package/dist/modules/providers/fallback.js +12 -12
  213. package/dist/modules/providers/health.js +4 -4
  214. package/dist/modules/providers/manager.js +50 -19
  215. package/dist/modules/providers/naming.js +15 -0
  216. package/dist/modules/providers/preflight.js +71 -0
  217. package/dist/modules/providers/presets.js +39 -8
  218. package/dist/modules/providers/registry.js +7 -2
  219. package/dist/modules/reasoning/policy.js +8 -6
  220. package/dist/modules/reasoning/probe.js +30 -25
  221. package/dist/modules/security/audit-log.js +19 -36
  222. package/dist/modules/security/audit-notifier.js +35 -75
  223. package/dist/modules/security/command-validator.js +99 -51
  224. package/dist/modules/security/content-scanner.js +8 -8
  225. package/dist/modules/security/data-sanitizer.js +9 -16
  226. package/dist/modules/security/encryption.js +32 -52
  227. package/dist/modules/security/index.js +2 -1
  228. package/dist/modules/security/network-validator.js +120 -43
  229. package/dist/modules/security/path-validator.js +61 -46
  230. package/dist/modules/security/protected-paths.js +73 -0
  231. package/dist/modules/security/rate-limiter.js +17 -49
  232. package/dist/modules/security/security-policies.js +25 -57
  233. package/dist/modules/security/session-encryption.js +32 -71
  234. package/dist/modules/security/session-isolation.js +15 -28
  235. package/dist/modules/session/auto-name.js +63 -0
  236. package/dist/modules/session/export.js +98 -0
  237. package/dist/modules/session/index.js +1 -0
  238. package/dist/modules/session/manager.js +79 -28
  239. package/dist/modules/session/module.js +4 -3
  240. package/dist/modules/session/store.js +57 -20
  241. package/dist/modules/setup/answers.js +35 -0
  242. package/dist/modules/setup/apply.js +79 -0
  243. package/dist/modules/setup/discovery.js +72 -0
  244. package/dist/modules/skills/index.js +1 -0
  245. package/dist/modules/skills/loader.js +9 -1
  246. package/dist/modules/skills/module.js +11 -0
  247. package/dist/modules/updater/changelog-reader.js +15 -16
  248. package/dist/modules/updater/checker.js +9 -4
  249. package/dist/modules/updater/dev-detect.js +7 -6
  250. package/dist/modules/updater/index.js +1 -0
  251. package/dist/modules/updater/module.js +17 -15
  252. package/dist/modules/user-profile/compressor.js +2 -0
  253. package/dist/modules/user-profile/index.js +1 -0
  254. package/dist/modules/user-profile/profile.js +10 -1
  255. package/dist/modules/webui/answer-api.js +29 -0
  256. package/dist/modules/webui/assets.js +50 -0
  257. package/dist/modules/webui/body.js +21 -0
  258. package/dist/modules/webui/commands-settings.js +209 -0
  259. package/dist/modules/webui/commands-shared.js +60 -0
  260. package/dist/modules/webui/commands.js +174 -0
  261. package/dist/modules/webui/config-api.js +134 -0
  262. package/dist/modules/webui/file-api.js +202 -0
  263. package/dist/modules/webui/forward.js +159 -0
  264. package/dist/modules/webui/history-api.js +112 -0
  265. package/dist/modules/webui/host-state.js +60 -0
  266. package/dist/modules/webui/i18n-api.js +9 -0
  267. package/dist/modules/webui/limits.js +6 -0
  268. package/dist/modules/webui/log-forward.js +20 -0
  269. package/dist/modules/webui/memory-commands.js +64 -0
  270. package/dist/modules/webui/model-cache.js +18 -0
  271. package/dist/modules/webui/module.js +148 -0
  272. package/dist/modules/webui/persist.js +24 -0
  273. package/dist/modules/webui/prompt-io.js +97 -0
  274. package/dist/modules/webui/providers-api.js +337 -0
  275. package/dist/modules/webui/providers-view.js +56 -0
  276. package/dist/modules/webui/reasoning-view.js +15 -0
  277. package/dist/modules/webui/run-agent.js +81 -0
  278. package/dist/modules/webui/server.js +284 -0
  279. package/dist/modules/webui/sessions-api.js +178 -0
  280. package/dist/modules/webui/settings-api.js +561 -0
  281. package/dist/modules/webui/setup-api.js +201 -0
  282. package/dist/modules/webui/state.js +74 -0
  283. package/dist/modules/webui/util.js +261 -0
  284. package/dist/output/bus.js +7 -2
  285. package/dist/output/channel.js +80 -28
  286. package/dist/output/format.js +2 -1
  287. package/dist/output/index.js +1 -0
  288. package/dist/output/json-sink.js +5 -3
  289. package/dist/output/machine.js +4 -3
  290. package/dist/output/session-sink.js +4 -4
  291. package/dist/tools/approve.js +4 -0
  292. package/dist/tools/attach-image.js +5 -4
  293. package/dist/tools/auto-fixer.js +26 -44
  294. package/dist/tools/auto-tags.js +83 -0
  295. package/dist/tools/bash.js +193 -116
  296. package/dist/tools/browser.js +76 -31
  297. package/dist/tools/chunk-query.js +14 -6
  298. package/dist/tools/create-dir.js +9 -5
  299. package/dist/tools/delete-file.js +9 -5
  300. package/dist/tools/download-file.js +32 -7
  301. package/dist/tools/edit-file.js +52 -14
  302. package/dist/tools/enable-tools.js +6 -6
  303. package/dist/tools/executor.js +132 -27
  304. package/dist/tools/file-info.js +3 -1
  305. package/dist/tools/filter-tools.js +3 -3
  306. package/dist/tools/glob-tool.js +4 -1
  307. package/dist/tools/grep-tool.js +13 -10
  308. package/dist/tools/hidden-tools-block.js +6 -6
  309. package/dist/tools/index.js +16 -8
  310. package/dist/tools/list-dir.js +4 -2
  311. package/dist/tools/load-skill.js +7 -0
  312. package/dist/tools/mcp-call.js +15 -4
  313. package/dist/tools/meet-done.js +14 -0
  314. package/dist/tools/move-file.js +14 -7
  315. package/dist/tools/path-utils.js +29 -29
  316. package/dist/tools/pipeline-run.js +29 -7
  317. package/dist/tools/preview.js +1 -1
  318. package/dist/tools/process-ids.js +31 -0
  319. package/dist/tools/process-kill.js +9 -16
  320. package/dist/tools/process-list.js +1 -0
  321. package/dist/tools/process-log.js +11 -16
  322. package/dist/tools/process-wait.js +63 -0
  323. package/dist/tools/prompt-io.js +4 -3
  324. package/dist/tools/question.js +172 -25
  325. package/dist/tools/read-file.js +55 -34
  326. package/dist/tools/recall.js +14 -8
  327. package/dist/tools/registry.js +6 -5
  328. package/dist/tools/remember.js +8 -4
  329. package/dist/tools/scope-request.js +11 -6
  330. package/dist/tools/search-history.js +5 -2
  331. package/dist/tools/session-info.js +2 -0
  332. package/dist/tools/set-thinking.js +8 -4
  333. package/dist/tools/subagent.js +59 -39
  334. package/dist/tools/syntax-validator.js +292 -90
  335. package/dist/tools/user-input.js +22 -18
  336. package/dist/tools/web-browse.js +9 -3
  337. package/dist/tools/web-fetch.js +12 -23
  338. package/dist/tools/web-search.js +7 -5
  339. package/dist/tools/write-file.js +45 -15
  340. package/dist/ui/action-log.js +13 -0
  341. package/dist/ui/box.js +4 -3
  342. package/dist/ui/colors.js +2 -0
  343. package/dist/ui/diff.js +6 -5
  344. package/dist/ui/index.js +1 -0
  345. package/dist/ui/line-editor.js +230 -122
  346. package/dist/ui/line-math.js +10 -2
  347. package/dist/ui/md-formatter.js +57 -37
  348. package/dist/ui/output.js +8 -3
  349. package/dist/ui/plan-view.js +21 -10
  350. package/dist/ui/renderer.js +393 -67
  351. package/dist/ui/spinner.js +16 -5
  352. package/dist/ui/table.js +15 -17
  353. package/dist/ui/theme.js +143 -0
  354. package/dist/utils/child-run.js +20 -0
  355. package/dist/utils/error.js +1 -1
  356. package/dist/utils/index.js +2 -0
  357. package/dist/utils/retry.js +2 -2
  358. package/dist/utils/truncate.js +1 -1
  359. package/package.json +1 -1
@@ -12,6 +12,7 @@ import { contextLabel } from "./format.js";
12
12
  import { closeAllMenus, setSessOpen } from "./tabs.js";
13
13
  import { setIndicator, closeConfirm, confirmOverlay, trapTab } from "./dialog.js";
14
14
  import { controlFor, PANEL_SECTIONS, keyToPanel } from "./settings-controls.js";
15
+ import { setLayout } from "./rail.js";
15
16
 
16
17
  function setSelButton(btn, label, value) {
17
18
  btn.textContent = label + ":";
@@ -131,6 +132,52 @@ export function renderSelectors() {
131
132
  setTimeout(() => input.focus(), 0);
132
133
  });
133
134
  ctxMenu.appendChild(custom);
135
+
136
+ renderReasoningSelector();
137
+
138
+ // Keep the pending paint in sync: boot (config === null) and an in-flight
139
+ // switch both show the mark; a mid-switch re-read must not clear it.
140
+ paintSelectorPending();
141
+ }
142
+
143
+ const REASONING_LEVELS_FULL = ["auto", "default", "none", "low", "medium", "high", "max"];
144
+ // Backends without granularity (prompt-tag, template-kwarg) collapse
145
+ // medium/max into low/high — show the collapsed set, no duplicates.
146
+ const REASONING_LEVELS_COARSE = ["auto", "default", "none", "low", "high"];
147
+
148
+ // 4th selector row: reasoning effort filtered to what the active
149
+ // provider+model actually supports (`GET /api/models` → `reasoning`).
150
+ // Unknown (boot) or `strategy: "none"` → hidden, never a wrong option list.
151
+ // Probe failed → the row stays with an honest `off` mark and a shortcut to
152
+ // Settings → Reasoning instead of dead levels.
153
+ function renderReasoningSelector() {
154
+ const wrap = document.querySelector('[data-sel="reasoning"]');
155
+ const btn = document.getElementById("selReasoningBtn");
156
+ const menu = document.getElementById("selReasoningMenu");
157
+ if (!wrap || !btn || !menu) return;
158
+ const info = S.state.reasoning;
159
+ if (!info || info.strategy === "none") {
160
+ wrap.hidden = true;
161
+ return;
162
+ }
163
+ wrap.hidden = false;
164
+ if (info.supported === false) {
165
+ setSelButton(btn, t("web.selectors.reasoning"), t("web.selectors.reasoning_off"));
166
+ menu.textContent = "";
167
+ menu.appendChild(el("div", "mi dim", t("reasoning.probe_failed")));
168
+ menu.appendChild(el("div", "sep"));
169
+ menu.appendChild(
170
+ menuAction(t("web.settings.reasoning"), "settings", () => openSettingsAt("reasoning")),
171
+ );
172
+ return;
173
+ }
174
+ const mode = (S.state.config && getPath(S.state.config, "reasoning.mode")) || info.mode || "auto";
175
+ setSelButton(btn, t("web.selectors.reasoning"), String(mode));
176
+ menu.textContent = "";
177
+ const levels = info.strategy === "openai-effort" ? REASONING_LEVELS_FULL : REASONING_LEVELS_COARSE;
178
+ levels.forEach((lvl) => {
179
+ menu.appendChild(menuItem(lvl, lvl === mode ? t("web.selectors.current") : "", lvl === mode, () => postConfig("reasoning.mode", lvl, false)));
180
+ });
134
181
  }
135
182
 
136
183
  document.querySelectorAll("[data-sel]").forEach((sel) => {
@@ -152,30 +199,92 @@ document.querySelectorAll("[data-sel]").forEach((sel) => {
152
199
  });
153
200
  });
154
201
 
202
+ // Selector keys (`provider/model/context/reasoning`) share the switch treatment:
203
+ // pending paint + single-flight + parallel re-reads. Settings-panel writes
204
+ // bypass it (plain POST + save toast).
205
+ const SELECTOR_KEYS = ["provider.active", "model", "contextWindow", "reasoning.mode"];
206
+
207
+ // Monotonic id for selector switches: a slow A→B→A pair must not let the
208
+ // stale reply repaint over the fresh one.
209
+ let switchSeq = 0;
210
+ // Key of the in-flight selector switch, if any. While set, every selector
211
+ // button carries the pending mark and all selector menus are inert (CSS gates
212
+ // on `.selectors[data-pending]`), so a double-click is answered with a toast
213
+ // instead of a second concurrent switch.
214
+ let pendingKey = null;
215
+
216
+ // Paint the pending mark (`⋯` via CSS `::after`, no label rewrites) and the
217
+ // `aria-busy` flag. Boot (`config === null`) paints the same mark, so the
218
+ // skeleton state is visible while `boot()` loads config/models/providers.
219
+ function paintSelectorPending() {
220
+ const show = pendingKey !== null || !S.state.config;
221
+ const wrap = document.querySelector(".selectors");
222
+ if (wrap) {
223
+ if (show) wrap.setAttribute("data-pending", pendingKey || "boot");
224
+ else wrap.removeAttribute("data-pending");
225
+ }
226
+ ["selProviderBtn", "selModelBtn", "selContextBtn", "selReasoningBtn"].forEach((id) => {
227
+ const btn = document.getElementById(id);
228
+ if (!btn) return;
229
+ btn.classList.toggle("pending", show);
230
+ btn.setAttribute("aria-busy", show ? "true" : "false");
231
+ });
232
+ }
233
+
155
234
  async function postConfig(key, value, refreshSettings) {
156
- const r = await api("/api/config", { method: "POST", body: { key, value } });
157
- if (r.ok && r.data && r.data.ok) {
235
+ const isSelector = SELECTOR_KEYS.indexOf(key) !== -1;
236
+ if (isSelector && pendingKey !== null) {
237
+ setIndicator(t("web.selectors.switch_busy"), true);
238
+ return false;
239
+ }
240
+ // Non-selector writes must not touch the sequence: otherwise a settings
241
+ // write landing mid-switch would orphan `pendingKey` (stale-guard skips the
242
+ // only `finally` that clears it).
243
+ let seq = 0;
244
+ if (isSelector) {
245
+ seq = ++switchSeq;
246
+ pendingKey = key;
247
+ closeAllMenus();
248
+ paintSelectorPending();
249
+ }
250
+ try {
251
+ const r = await api("/api/config", { method: "POST", body: { key, value } });
252
+ if (!r.ok || !r.data || !r.data.ok) {
253
+ const err = (r.data && (r.data.error || r.data.reason)) || r.status || t("web.settings.save_failed");
254
+ setIndicator(String(err), true);
255
+ return false;
256
+ }
158
257
  const persisted = r.data.persisted !== false;
159
258
  setIndicator(persisted ? t("web.settings.saved") : (r.data.warning || "not persisted"), !persisted);
160
259
  if (S.state.config && r.data.value !== undefined) setPath(S.state.config, key, r.data.value);
260
+ if (key === "ui.activityRail") setLayout(String(r.data.value));
261
+ if (key === "ui.fontSize") applyFontSize(String(r.data.value));
161
262
  if (key === "locale") {
162
263
  await loadI18n();
163
264
  if (hooks.applyStaticI18n) hooks.applyStaticI18n();
164
265
  renderSettings();
165
266
  }
166
267
  // A provider switch restores the per-provider model; a model change
167
- // updates that provider's remembered model. Re-read both server-side.
268
+ // updates that provider's remembered model. Both are independent GETs,
269
+ // so re-read them in parallel. Context/reasoning switches only need the
270
+ // config back (no extra model round-trip).
168
271
  if (key === "provider.active" || key === "model") {
272
+ await Promise.all([loadConfig(), loadModels()]);
273
+ } else if (isSelector) {
169
274
  await loadConfig();
170
- await loadModels();
171
275
  }
276
+ // Stale reply (a newer switch started while this one was in flight):
277
+ // leave the paint and the final render to the newer operation.
278
+ if (isSelector && seq !== switchSeq) return true;
172
279
  renderSelectors();
173
280
  if (refreshSettings) renderSettings();
174
281
  return true;
282
+ } finally {
283
+ if (isSelector && seq === switchSeq) {
284
+ pendingKey = null;
285
+ paintSelectorPending();
286
+ }
175
287
  }
176
- const err = (r.data && (r.data.error || r.data.reason)) || r.status || t("web.settings.save_failed");
177
- setIndicator(String(err), true);
178
- return false;
179
288
  }
180
289
 
181
290
  export async function loadConfig() {
@@ -184,6 +293,7 @@ export async function loadConfig() {
184
293
  S.state.config = r.data.config || {};
185
294
  S.state.registry = Array.isArray(r.data.registry) ? r.data.registry : [];
186
295
  S.state.webToken = typeof r.data.webToken === "string" ? r.data.webToken : null;
296
+ applyFontSize();
187
297
  renderSelectors();
188
298
  renderSettings();
189
299
  }
@@ -192,6 +302,9 @@ export async function loadModels() {
192
302
  const r = await api("/api/models");
193
303
  if (!r.ok || !r.data) return;
194
304
  S.state.models = Array.isArray(r.data.models) ? r.data.models : [];
305
+ // Reasoning support rides along with the model list (one round-trip):
306
+ // the 4th selector row renders from this, never from a separate GET.
307
+ if (r.data.reasoning && typeof r.data.reasoning === "object") S.state.reasoning = r.data.reasoning;
195
308
  renderSelectors();
196
309
  }
197
310
 
@@ -377,16 +490,77 @@ function providerSelect(options, value) {
377
490
  const none = el("option", "", "—");
378
491
  none.value = "";
379
492
  sel.appendChild(none);
493
+ const seen = new Set([""]);
380
494
  options.forEach((v) => {
495
+ seen.add(v);
381
496
  const o = el("option", "", v);
382
497
  o.value = v;
383
498
  if (v === value) o.selected = true;
384
499
  sel.appendChild(o);
385
500
  });
501
+ // Keep a stored-but-unknown value selectable (e.g. a legacy provider type
502
+ // from hand-edited config) instead of silently resetting it to "—".
503
+ if (value != null && value !== "" && !seen.has(value)) {
504
+ const o = el("option", "", value);
505
+ o.value = value;
506
+ o.selected = true;
507
+ sel.appendChild(o);
508
+ }
386
509
  if (value == null) sel.value = "";
387
510
  return sel;
388
511
  }
389
512
 
513
+ // Base-URL field: a dropdown of well-known URLs plus a "custom URL" option
514
+ // revealing a free-text input, so corporate proxies stay enterable.
515
+ // Exposes a `.value` get/set compatible with plain inputs, plus the raw
516
+ // `sel`/`custom` nodes for event wiring.
517
+ function providerBaseField(value) {
518
+ const node = el("div", "pform-base");
519
+ const sel = el("select");
520
+ PROVIDER_BASE_URLS.forEach(([label, url]) => {
521
+ const o = el("option", "", label + " — " + url);
522
+ o.value = url;
523
+ sel.appendChild(o);
524
+ });
525
+ const customOpt = el("option", "", t("web.settings.provider_baseurl_custom"));
526
+ customOpt.value = BASE_URL_CUSTOM;
527
+ sel.appendChild(customOpt);
528
+ const custom = providerInput("", "text", "https://…");
529
+ const known = new Set(PROVIDER_BASE_URLS.map(([, url]) => url));
530
+ const sync = () => {
531
+ custom.style.display = sel.value === BASE_URL_CUSTOM ? "" : "none";
532
+ };
533
+ const field = {
534
+ node,
535
+ sel,
536
+ custom,
537
+ get value() {
538
+ return sel.value === BASE_URL_CUSTOM ? custom.value : sel.value;
539
+ },
540
+ set value(v) {
541
+ if (v && known.has(v)) {
542
+ sel.value = v;
543
+ } else {
544
+ sel.value = BASE_URL_CUSTOM;
545
+ custom.value = v || "";
546
+ }
547
+ sync();
548
+ },
549
+ addEventListener(ev, fn) {
550
+ sel.addEventListener(ev, fn);
551
+ custom.addEventListener(ev, fn);
552
+ },
553
+ setAttribute(k, v) {
554
+ sel.setAttribute(k, v);
555
+ },
556
+ };
557
+ field.value = value || "";
558
+ sel.addEventListener("change", sync);
559
+ node.appendChild(sel);
560
+ node.appendChild(custom);
561
+ return field;
562
+ }
563
+
390
564
  function providerCheck(value) {
391
565
  const input = el("input");
392
566
  input.type = "checkbox";
@@ -422,34 +596,29 @@ function chip(label, cls, onClick) {
422
596
  return b;
423
597
  }
424
598
 
425
- // Suggestions for the provider form: known provider types and common base URLs.
426
- function providerHints() {
427
- const frag = document.createDocumentFragment();
428
- const types = el("datalist");
429
- types.id = "providerTypes";
430
- ["openai-compat", "lmstudio", "openrouter", "openai", "anthropic", "opencode-zen", "opencode-go"].forEach((v) => {
431
- const o = el("option");
432
- o.value = v;
433
- types.appendChild(o);
434
- });
435
- const urls = el("datalist");
436
- urls.id = "providerBaseUrls";
437
- [
438
- ["LM Studio", "http://localhost:1234/v1"],
439
- ["Ollama", "http://localhost:11434/v1"],
440
- ["OpenRouter", "https://openrouter.ai/api/v1"],
441
- ["OpenAI", "https://api.openai.com/v1"],
442
- ["Anthropic", "https://api.anthropic.com"],
443
- ].forEach(([label, value]) => {
444
- const o = el("option");
445
- o.value = value;
446
- o.label = label;
447
- urls.appendChild(o);
448
- });
449
- frag.appendChild(types);
450
- frag.appendChild(urls);
451
- return frag;
452
- }
599
+ // Known provider types (mirrors BUILTIN_PROVIDERS in src/modules/providers/presets.ts)
600
+ // and well-known local base URLs. Both render as real `<select>` dropdowns —
601
+ // `datalist` hints were replaced because they don't read as a list and behave
602
+ // inconsistently across browsers.
603
+ const PROVIDER_TYPES = [
604
+ "openai-compat",
605
+ "lmstudio",
606
+ "openrouter",
607
+ "openai",
608
+ "anthropic",
609
+ "opencode-zen",
610
+ "opencode-go",
611
+ ];
612
+
613
+ const PROVIDER_BASE_URLS = [
614
+ ["LM Studio", "http://localhost:1234/v1"],
615
+ ["Ollama", "http://localhost:11434/v1"],
616
+ ["OpenRouter", "https://openrouter.ai/api/v1"],
617
+ ["OpenAI", "https://api.openai.com/v1"],
618
+ ["Anthropic", "https://api.anthropic.com"],
619
+ ];
620
+
621
+ const BASE_URL_CUSTOM = "__custom__";
453
622
 
454
623
  // One-click preset row: selecting a builtin fills type/label/base URL, so
455
624
  // hosted providers need no URL knowledge. `custom` leaves the fields free.
@@ -488,6 +657,11 @@ function wirePresetSync(presets, presetSel, mode, f) {
488
657
  const sel = presets.find((x) => x.id === presetSel.value);
489
658
  if (!sel || f.type.value.trim() !== sel.type) presetSel.value = "";
490
659
  });
660
+ // The type control is a `<select>`: some browsers fire only `change`.
661
+ f.type.addEventListener("change", () => {
662
+ const sel = presets.find((x) => x.id === presetSel.value);
663
+ if (!sel || f.type.value.trim() !== sel.type) presetSel.value = "";
664
+ });
491
665
  }
492
666
 
493
667
  // Optional tuning fields stay collapsed so the form reads as "type + URL".
@@ -509,11 +683,16 @@ function providerAdvancedFields(editing) {
509
683
  retryBase: providerInput(retry.baseDelay, "number", "1000"),
510
684
  retryMaxD: providerInput(retry.maxDelay, "number", "30000"),
511
685
  };
686
+ // Flags keep the raw config key in `title` so it stays searchable, but the
687
+ // visible label is localized like every other field of the form.
512
688
  f.cacheFlags = CACHE_FLAG_KEYS.map((key) => {
513
689
  const box = providerCheck(cache[key]);
690
+ const label = t("web.settings.provider_flag." + key);
691
+ box.setAttribute("aria-label", label);
514
692
  const wrap = el("label", "pform-check");
693
+ wrap.title = key;
515
694
  wrap.appendChild(box);
516
- wrap.appendChild(el("span", "", key));
695
+ wrap.appendChild(el("span", "", label));
517
696
  return { key, box, node: wrap };
518
697
  });
519
698
  advGrid.appendChild(pfield("web.settings.provider_context", f.ctx));
@@ -524,19 +703,23 @@ function providerAdvancedFields(editing) {
524
703
  advGrid.appendChild(pfield("web.settings.provider_cache_mechanism", f.cacheMech));
525
704
  advGrid.appendChild(pfield("web.settings.provider_cache_report", f.cacheReport));
526
705
  adv.appendChild(advGrid);
706
+
707
+ // Retry numbers reuse the field grid (label above input) — a bare label+input
708
+ // pair collapses into an unreadable inline blob without the grid styles.
527
709
  const retryGroup = el("div", "pform-checks");
528
710
  retryGroup.appendChild(el("span", "pform-checks-title", t("web.settings.provider_retry")));
529
- [["maxRetries", f.retryMax], ["baseDelay", f.retryBase], ["maxDelay", f.retryMaxD]].forEach(([key, input]) => {
530
- const wrap = el("label", "pform-check");
531
- wrap.appendChild(el("span", "", key));
532
- input.setAttribute("aria-label", key);
533
- wrap.appendChild(input);
534
- retryGroup.appendChild(wrap);
535
- });
711
+ const retryGrid = el("div", "pform-grid");
712
+ retryGrid.appendChild(pfield("web.settings.provider_retry_max", f.retryMax));
713
+ retryGrid.appendChild(pfield("web.settings.provider_retry_base", f.retryBase));
714
+ retryGrid.appendChild(pfield("web.settings.provider_retry_maxdelay", f.retryMaxD));
715
+ retryGroup.appendChild(retryGrid);
536
716
  adv.appendChild(retryGroup);
717
+
537
718
  const flagGroup = el("div", "pform-checks");
538
719
  flagGroup.appendChild(el("span", "pform-checks-title", t("web.settings.provider_cache_flags")));
539
- f.cacheFlags.forEach(({ node }) => flagGroup.appendChild(node));
720
+ const flagGrid = el("div", "pform-checks-grid");
721
+ f.cacheFlags.forEach(({ node }) => flagGrid.appendChild(node));
722
+ flagGroup.appendChild(flagGrid);
540
723
  adv.appendChild(flagGroup);
541
724
  return { node: adv, ...f };
542
725
  }
@@ -620,15 +803,13 @@ function providerForm() {
620
803
  if (presetNode) box.appendChild(presetNode);
621
804
 
622
805
  const grid = el("div", "pform-grid");
623
- const fType = providerInput(editing.type, "text", "openai-compat");
624
- fType.setAttribute("list", "providerTypes");
806
+ const fType = providerSelect(PROVIDER_TYPES, editing.type);
625
807
  const fLabel = providerInput(editing.label, "text", "my-provider");
626
- const fBase = providerInput(editing.baseUrl, "text", "http://localhost:1234/v1");
627
- fBase.setAttribute("list", "providerBaseUrls");
808
+ const fBase = providerBaseField(editing.baseUrl);
628
809
  const fKey = providerInput("", "password", editing.hasApiKey ? t("web.settings.provider_key_keep") : "");
629
810
  grid.appendChild(pfield("web.settings.provider_type", fType));
630
811
  grid.appendChild(pfield("web.settings.provider_label_field", fLabel));
631
- grid.appendChild(pfield("web.settings.provider_baseurl", fBase));
812
+ grid.appendChild(pfield("web.settings.provider_baseurl", fBase.node));
632
813
  grid.appendChild(pfield("web.settings.provider_apikey", fKey));
633
814
  box.appendChild(grid);
634
815
  wirePresetSync(presets, presetSel, mode, { type: fType, label: fLabel, base: fBase });
@@ -637,8 +818,6 @@ function providerForm() {
637
818
  box.appendChild(advNode);
638
819
  const f = { type: fType, label: fLabel, base: fBase, key: fKey, ...advF };
639
820
 
640
- box.appendChild(providerHints());
641
-
642
821
  const actions = el("div", "pform-actions");
643
822
  actions.appendChild(link(t("web.settings.save"), "", () => submitProviderForm(f, mode, editing)));
644
823
  actions.appendChild(link(t("web.settings.cancel"), "dim", () => closeProviderForm()));
@@ -1529,6 +1708,12 @@ function openSettings() {
1529
1708
  const active = document.querySelector("#setMenu .mi.on") || settingsTabs[0];
1530
1709
  if (active) setTimeout(() => active.focus(), 0);
1531
1710
  }
1711
+ // Open settings straight at a panel (used by the reasoning selector when the
1712
+ // backend probe failed: the row links to Settings → Reasoning).
1713
+ function openSettingsAt(panel) {
1714
+ openSettings();
1715
+ selectSettingsPanel(panel);
1716
+ }
1532
1717
  function closeSettings() {
1533
1718
  closeSubDialog();
1534
1719
  S.state.providerForm = null;
@@ -1619,6 +1804,16 @@ export function currentTheme() {
1619
1804
  return localStorage.getItem(THEME_KEY) || "auto";
1620
1805
  }
1621
1806
 
1807
+ // Web chat font size (`ui.fontSize`, server config — unlike the theme, which
1808
+ // is a browser-local preference). Applied as `body[data-font]` so the switch
1809
+ // is instant and survives re-renders; unknown values fall back to the default.
1810
+ export function applyFontSize(value) {
1811
+ const v = value || (S.state.config && S.state.config.ui && S.state.config.ui.fontSize) || "comfortable";
1812
+ const known = v === "compact" || v === "large" ? v : "comfortable";
1813
+ if (known === "comfortable") document.body.removeAttribute("data-font");
1814
+ else document.body.setAttribute("data-font", known);
1815
+ }
1816
+
1622
1817
  export function applyTheme(theme) {
1623
1818
  localStorage.setItem(THEME_KEY, theme);
1624
1819
  const light = theme === "light" || (theme === "auto" && window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches);
@@ -22,6 +22,9 @@ state: {
22
22
  skills: undefined,
23
23
  pluginsData: undefined,
24
24
  providers: undefined,
25
+ // Reasoning support of the active provider+model (`GET /api/models`
26
+ // `reasoning` shape); `undefined` until the first model list arrives.
27
+ reasoning: undefined,
25
28
  providerPresets: [],
26
29
  providerForm: null,
27
30
  // Transient error shown inline in the sessions dropdown (visible without the modal).
@@ -12,9 +12,10 @@ import { relTime, contextLabel } from "./format.js";
12
12
  import {
13
13
  makeFeed, showEmptyFeed, activeFeed, liveFeed, appendTo,
14
14
  clearEmpty, showBusy, clearFeed, finishMd, resetTurnText,
15
- finishThink, feedHasContent, loadHistory, syncUrl,
15
+ finishThink, feedHasContent, loadHistory, syncUrl, renderPlan,
16
16
  } from "./feed.js";
17
17
  import { openConfirm, setIndicator } from "./dialog.js";
18
+ import { setBusy, setPlan as railSetPlan } from "./rail.js";
18
19
 
19
20
  let sessionsErrTimer = null;
20
21
 
@@ -55,6 +56,20 @@ function renderTabstrip() {
55
56
  });
56
57
  tabstripEl.appendChild(tab);
57
58
  });
59
+ scrollActiveTabIntoView();
60
+ renderSessBar();
61
+ }
62
+
63
+ // Keep the active tab visible, aligned to its START so the beginning of the
64
+ // session name stays readable (aligning the end would cut the name off,
65
+ // which is exactly what phones showed when tapping a tab on the right).
66
+ function scrollActiveTabIntoView() {
67
+ const active = tabstripEl.querySelector(".stab.on");
68
+ if (!active) return;
69
+ const left = Math.max(0, active.offsetLeft - 4);
70
+ if (left < tabstripEl.scrollLeft || left + active.offsetWidth > tabstripEl.scrollLeft + tabstripEl.clientWidth) {
71
+ tabstripEl.scrollLeft = left;
72
+ }
58
73
  }
59
74
 
60
75
  tabstripEl.addEventListener(
@@ -117,7 +132,18 @@ export function openTab(id, name, kind, server) {
117
132
  // it is opened from the server afterward we must actually fetch the
118
133
  // transcript, or a reload of the active session shows an empty feed.
119
134
  if (!feedHasContent(tab.feed)) tab.historyLoaded = false;
120
- api("/api/sessions/activate", { method: "POST", body: { id } }).then(() => loadSessions());
135
+ // План следует за сессией: сервер уже переключил трекер на activate,
136
+ // сразу тянем свежий план в сайдбар (не ждём 2с-полла `/api/state`).
137
+ api("/api/sessions/activate", { method: "POST", body: { id } }).then(() => {
138
+ loadSessions();
139
+ api("/api/state").then((r) => {
140
+ if (!r.ok || !r.data) return;
141
+ S.state.host = r.data.state || {};
142
+ S.state.busy = !!r.data.busy;
143
+ renderPlan(S.state.host.plan);
144
+ railSetPlan(S.state.busy && r.data.state ? r.data.state.plan : null);
145
+ });
146
+ });
121
147
  loadHistory(tab);
122
148
  }
123
149
  syncUrl();
@@ -150,6 +176,14 @@ async function newChatTab() {
150
176
  const meta = r.data.session;
151
177
  openTab(meta.id, meta.name || String(meta.id), "session", false);
152
178
  await loadSessions();
179
+ // Новая сессия — без плана: сервер уже сбросил трекер, сразу чистим сайдбар.
180
+ const st = await api("/api/state");
181
+ if (st.ok && st.data) {
182
+ S.state.host = st.data.state || {};
183
+ S.state.busy = !!st.data.busy;
184
+ renderPlan(S.state.host.plan);
185
+ railSetPlan(S.state.busy && st.data.state ? st.data.state.plan : null);
186
+ }
153
187
  return;
154
188
  }
155
189
  // Server-side creation failed (no manager / offline): keep a local-only tab
@@ -275,6 +309,73 @@ sessBtn.addEventListener("click", (e) => {
275
309
  setSessOpen(!sessDD.classList.contains("open"));
276
310
  });
277
311
 
312
+ // --- mobile session sub-header -------------------------------------------
313
+ // Phones show a session control bar under the topbar: prev/next arrows step
314
+ // through the open tabs, ✕ closes the active tab, and the + / list tools
315
+ // relocate here (see placeTopTools) so the topbar keeps room for tabs.
316
+ const sessBar = document.getElementById("sessBar");
317
+ const sessNameBtn = document.getElementById("sessNameBtn");
318
+ const sessNameEl = document.getElementById("sessName");
319
+ const sessPrevBtn = document.getElementById("sessPrev");
320
+ const sessNextBtn = document.getElementById("sessNext");
321
+ const sessCloseBtn = document.getElementById("sessClose");
322
+ const sessTools = document.getElementById("sessTools");
323
+ const barTop = document.querySelector(".bar-top");
324
+ const newChatBtn = document.getElementById("newChat");
325
+ // Anchor remembering the desktop slot of the + / list controls.
326
+ const topToolsAnchor = document.createComment("top-tools");
327
+ if (barTop && newChatBtn) barTop.insertBefore(topToolsAnchor, newChatBtn);
328
+
329
+ function stepTab(dir) {
330
+ if (S.tabs.length < 2) return;
331
+ const i = S.tabs.findIndex((x) => x.id === S.activeTabId);
332
+ const n = i === -1 ? 0 : (i + dir + S.tabs.length) % S.tabs.length;
333
+ const tab = S.tabs[n];
334
+ // Same semantics as clicking the tab in the strip.
335
+ if (tab) openTab(tab.id, tab.name, tab.kind, true);
336
+ }
337
+
338
+ function renderSessBar() {
339
+ if (!sessBar) return;
340
+ const active = S.tabs.find((x) => x.id === S.activeTabId);
341
+ const label = active ? active.name || String(active.id) : "—";
342
+ sessNameEl.textContent = label;
343
+ sessNameBtn.title = label;
344
+ const has = S.tabs.length > 0;
345
+ sessPrevBtn.disabled = S.tabs.length < 2;
346
+ sessNextBtn.disabled = S.tabs.length < 2;
347
+ sessCloseBtn.disabled = !has;
348
+ sessNameBtn.disabled = !has;
349
+ }
350
+
351
+ // Nodes are relocated (not duplicated), so their listeners and the sessions
352
+ // dropdown anchoring (.dd is position:relative) survive the move.
353
+ function placeTopTools() {
354
+ if (!barTop || !sessTools || !newChatBtn || !sessDD) return;
355
+ if (window.matchMedia("(max-width: 760px)").matches) {
356
+ sessTools.append(newChatBtn, sessDD);
357
+ } else {
358
+ barTop.insertBefore(newChatBtn, topToolsAnchor);
359
+ barTop.insertBefore(sessDD, topToolsAnchor);
360
+ }
361
+ }
362
+
363
+ if (sessPrevBtn) sessPrevBtn.addEventListener("click", () => stepTab(-1));
364
+ if (sessNextBtn) sessNextBtn.addEventListener("click", () => stepTab(1));
365
+ if (sessCloseBtn) sessCloseBtn.addEventListener("click", () => {
366
+ if (S.activeTabId) closeTab(S.activeTabId);
367
+ });
368
+ if (sessNameBtn) sessNameBtn.addEventListener("click", (e) => {
369
+ e.stopPropagation();
370
+ document.querySelectorAll("[data-sel]").forEach((s) => s.classList.remove("open"));
371
+ setSessOpen(!sessDD.classList.contains("open"));
372
+ });
373
+ if (typeof window.matchMedia === "function") {
374
+ const narrowMq = window.matchMedia("(max-width: 760px)");
375
+ if (typeof narrowMq.addEventListener === "function") narrowMq.addEventListener("change", placeTopTools);
376
+ placeTopTools();
377
+ }
378
+
278
379
  document.addEventListener("click", () => {
279
380
  document.querySelectorAll("[data-sel]").forEach((s) => s.classList.remove("open"));
280
381
  setSessOpen(false);
@@ -662,6 +763,7 @@ export function reflectBusy() {
662
763
  }
663
764
  const escHint = document.getElementById("hintEsc");
664
765
  if (escHint) escHint.hidden = !S.state.busy;
766
+ setBusy(S.state.busy);
665
767
  }
666
768
 
667
769
  export async function loadSessions() {