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.
- package/CHANGELOG.md +33 -0
- package/dist/assets/app.js +22 -4
- package/dist/assets/index.html +29 -0
- package/dist/assets/js/dom.js +2 -1
- package/dist/assets/js/feed.js +38 -26
- package/dist/assets/js/format.js +1 -1
- package/dist/assets/js/rail.js +114 -0
- package/dist/assets/js/settings-controls.js +2 -2
- package/dist/assets/js/settings.js +246 -51
- package/dist/assets/js/store.js +3 -0
- package/dist/assets/js/tabs.js +104 -2
- package/dist/assets/styles.css +215 -66
- package/dist/cli/cache-line.js +1 -0
- package/dist/cli/command-suggest.js +5 -4
- package/dist/cli/commands.js +231 -122
- package/dist/cli/completer.js +132 -31
- package/dist/cli/index.js +1 -0
- package/dist/cli/json-payload.js +4 -4
- package/dist/cli/main.js +166 -61
- package/dist/cli/onboarding-commands.js +126 -0
- package/dist/cli/plugin-commands.js +2 -1
- package/dist/cli/relaunch.js +16 -13
- package/dist/cli/repl-commands-lsp.js +75 -0
- package/dist/cli/repl-commands-skills.js +105 -0
- package/dist/cli/repl-commands.js +589 -266
- package/dist/cli/repl.js +436 -177
- package/dist/cli/run-result.js +5 -6
- package/dist/cli/security-commands.js +37 -28
- package/dist/cli/setup-order.js +4 -55
- package/dist/cli/setup-prompt.js +3 -2
- package/dist/cli/setup.js +27 -107
- package/dist/cli/web-command.js +98 -0
- package/dist/config/budget.js +5 -5
- package/dist/config/config.js +103 -96
- package/dist/config/defaults.js +38 -8
- package/dist/config/domains.js +24 -27
- package/dist/config/env.js +46 -0
- package/dist/config/experts.js +2 -0
- package/dist/config/index.js +1 -0
- package/dist/config/paths.js +56 -0
- package/dist/config/redact.js +44 -0
- package/dist/config/registry-extra.js +334 -0
- package/dist/config/registry.js +584 -0
- package/dist/config/security.js +35 -36
- package/dist/config/timeouts.js +74 -0
- package/dist/config/utils.js +16 -15
- package/dist/core/agent/artifact-progress.js +59 -0
- package/dist/core/agent/artifact-resolve.js +79 -0
- package/dist/core/agent/audit-gate.js +7 -3
- package/dist/core/agent/compaction.js +2 -2
- package/dist/core/agent/constants.js +33 -4
- package/dist/core/agent/context-log.js +454 -0
- package/dist/core/agent/context-renderer.js +5 -0
- package/dist/core/agent/critic-gate.js +132 -0
- package/dist/core/agent/hallucination-gate.js +148 -38
- package/dist/core/agent/loop-state.js +15 -0
- package/dist/core/agent/prefix-monitor.js +1 -1
- package/dist/core/agent/reasoning-resolver.js +10 -1
- package/dist/core/agent/token-tracker.js +7 -4
- package/dist/core/agent/tool-batch.js +93 -18
- package/dist/core/agent/tool-images.js +18 -0
- package/dist/core/agent/tool-output.js +65 -11
- package/dist/core/agent/tool-pairing.js +60 -0
- package/dist/core/agent-moe.js +25 -24
- package/dist/core/agent.js +429 -108
- package/dist/core/bootstrap/agent-phase.js +66 -0
- package/dist/core/bootstrap/config-phase.js +86 -0
- package/dist/core/bootstrap/modules-phase.js +269 -0
- package/dist/core/bootstrap/provider-phase.js +183 -0
- package/dist/core/bootstrap/session-phase.js +204 -0
- package/dist/core/bootstrap/state.js +94 -0
- package/dist/core/bootstrap/workspace-phase.js +115 -0
- package/dist/core/bootstrap.js +92 -660
- package/dist/core/crash-handler.js +17 -7
- package/dist/core/environment.js +37 -12
- package/dist/core/host-plan.js +43 -0
- package/dist/core/host.js +23 -0
- package/dist/core/index.js +1 -0
- package/dist/core/prompt-builder.js +17 -1
- package/dist/core/prompt-overflow.js +42 -43
- package/dist/core/session-logger.js +122 -30
- package/dist/core/system-prompt.js +117 -0
- package/dist/core/version.js +6 -4
- package/dist/core/web-host.js +34 -0
- package/dist/core/workspace.js +19 -15
- package/dist/i18n/en.json +106 -21
- package/dist/i18n/index.js +37 -3
- package/dist/i18n/ru.json +109 -21
- package/dist/index.js +7 -3
- package/dist/llm/image-utils.js +55 -64
- package/dist/llm/llm-errors.js +9 -0
- package/dist/llm/model-loader.js +10 -32
- package/dist/llm/models-dev.js +121 -0
- package/dist/llm/openai-compat.js +146 -73
- package/dist/llm/orchestrator.js +33 -11
- package/dist/llm/provider-budget.js +2 -2
- package/dist/llm/provider-context.js +233 -0
- package/dist/llm/provider.js +1 -1
- package/dist/llm/response.js +5 -0
- package/dist/llm/token-counter.js +27 -11
- package/dist/logger/app-logger.js +48 -22
- package/dist/logger/file-log.js +28 -19
- package/dist/logger/index.js +1 -0
- package/dist/main.js +24285 -22447
- package/dist/migration/backup.js +14 -3
- package/dist/migration/detect.js +12 -3
- package/dist/migration/index.js +1 -0
- package/dist/modules/artifacts/store.js +9 -8
- package/dist/modules/browser/actions.js +6 -0
- package/dist/modules/browser/bridge-client.js +14 -12
- package/dist/modules/browser/bridge-path.js +4 -4
- package/dist/modules/browser/console-buffer.js +37 -0
- package/dist/modules/browser/cookie-store.js +5 -0
- package/dist/modules/browser/driver-types.js +1 -0
- package/dist/modules/browser/driver.js +19 -11
- package/dist/modules/browser/index.js +3 -1
- package/dist/modules/browser/module.js +2 -1
- package/dist/modules/browser/session.js +10 -30
- package/dist/modules/browser/snapshot.js +19 -13
- package/dist/modules/browser/types.js +1 -0
- package/dist/modules/certification/cli.js +28 -20
- package/dist/modules/certification/fact-checker.js +9 -1
- package/dist/modules/certification/loader.js +9 -2
- package/dist/modules/certification/manifest.js +83 -21
- package/dist/modules/certification/runner.js +23 -13
- package/dist/modules/certification/scenarios.js +20 -20
- package/dist/modules/certification/syntax-scenarios.js +1 -2
- package/dist/modules/context/chunk-query.js +11 -9
- package/dist/modules/context/fact-extractor.js +37 -39
- package/dist/modules/context/index.js +1 -0
- package/dist/modules/context/manager.js +106 -80
- package/dist/modules/critic/client.js +176 -0
- package/dist/modules/critic/evidence.js +81 -0
- package/dist/modules/critic/provider.js +45 -0
- package/dist/modules/critic/tools.js +78 -0
- package/dist/modules/critic/types.js +1 -0
- package/dist/modules/execution/audit-runners.js +28 -34
- package/dist/modules/execution/auditor.js +53 -49
- package/dist/modules/execution/execution-plugin.js +142 -102
- package/dist/modules/execution/index.js +1 -1
- package/dist/modules/execution/input-from.js +37 -13
- package/dist/modules/execution/module.js +368 -191
- package/dist/modules/execution/moe-executor.js +53 -26
- package/dist/modules/execution/plan-actions.js +320 -202
- package/dist/modules/execution/plan-checks.js +35 -0
- package/dist/modules/execution/plan-coverage.js +36 -33
- package/dist/modules/execution/plan-steps.js +11 -3
- package/dist/modules/execution/plan-store.js +83 -17
- package/dist/modules/execution/plan-tool.js +89 -35
- package/dist/modules/execution/plan-validator.js +30 -10
- package/dist/modules/execution/planner.js +22 -13
- package/dist/modules/execution/stuck-detector.js +196 -93
- package/dist/modules/execution/tracker.js +22 -8
- package/dist/modules/execution/verifier.js +69 -48
- package/dist/modules/execution/windows-commands.js +12 -12
- package/dist/modules/hallucination/artifact-claim.js +93 -0
- package/dist/modules/hallucination/confidence.js +13 -11
- package/dist/modules/hallucination/consistency.js +10 -2
- package/dist/modules/hallucination/detector.js +10 -5
- package/dist/modules/hallucination/factual.js +43 -26
- package/dist/modules/hallucination/index.js +1 -0
- package/dist/modules/hallucination/js-identifiers.js +44 -45
- package/dist/modules/hallucination/llm-judge.js +17 -12
- package/dist/modules/hallucination/success-claim.js +59 -0
- package/dist/modules/indexer/cache.js +52 -4
- package/dist/modules/indexer/index.js +1 -0
- package/dist/modules/indexer/map-command.js +2 -4
- package/dist/modules/indexer/map-select.js +16 -16
- package/dist/modules/indexer/module.js +47 -12
- package/dist/modules/indexer/project-profile.js +8 -7
- package/dist/modules/indexer/symbols.js +15 -15
- package/dist/modules/indexer/walker.js +159 -29
- package/dist/modules/lsp/check-tool.js +7 -5
- package/dist/modules/lsp/client.js +39 -39
- package/dist/modules/lsp/command.js +17 -24
- package/dist/modules/lsp/config.js +12 -11
- package/dist/modules/lsp/index.js +1 -0
- package/dist/modules/lsp/module.js +48 -46
- package/dist/modules/lsp/probe.js +13 -14
- package/dist/modules/lsp/project-root.js +14 -17
- package/dist/modules/lsp/startup-check.js +18 -16
- package/dist/modules/mcp/client.js +59 -62
- package/dist/modules/mcp/index.js +1 -0
- package/dist/modules/mcp/module.js +66 -37
- package/dist/modules/mcp/registry.js +5 -0
- package/dist/modules/memory/index.js +1 -0
- package/dist/modules/memory/module.js +12 -8
- package/dist/modules/memory/search.js +4 -1
- package/dist/modules/memory/store.js +40 -2
- package/dist/modules/onboarding/init.js +47 -0
- package/dist/modules/onboarding/meet.js +30 -0
- package/dist/modules/onboarding/mode.js +30 -0
- package/dist/modules/onboarding/prompts.js +43 -0
- package/dist/modules/pipelines/engine.js +10 -0
- package/dist/modules/pipelines/index.js +1 -0
- package/dist/modules/pipelines/parser.js +4 -1
- package/dist/modules/pipelines/template.js +4 -0
- package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
- package/dist/modules/plugins/builtin/notify.js +2 -0
- package/dist/modules/plugins/index.js +1 -0
- package/dist/modules/plugins/loader.js +18 -3
- package/dist/modules/plugins/manager.js +37 -17
- package/dist/modules/pricing/index.js +10 -11
- package/dist/modules/pricing/prices.js +12 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +27 -31
- package/dist/modules/processes/registry.js +44 -25
- package/dist/modules/processes/runner.js +4 -4
- package/dist/modules/providers/capabilities.js +28 -0
- package/dist/modules/providers/create.js +3 -4
- package/dist/modules/providers/factory.js +7 -7
- package/dist/modules/providers/fallback.js +12 -12
- package/dist/modules/providers/health.js +4 -4
- package/dist/modules/providers/manager.js +50 -19
- package/dist/modules/providers/naming.js +15 -0
- package/dist/modules/providers/preflight.js +71 -0
- package/dist/modules/providers/presets.js +39 -8
- package/dist/modules/providers/registry.js +7 -2
- package/dist/modules/reasoning/policy.js +8 -6
- package/dist/modules/reasoning/probe.js +30 -25
- package/dist/modules/security/audit-log.js +19 -36
- package/dist/modules/security/audit-notifier.js +35 -75
- package/dist/modules/security/command-validator.js +99 -51
- package/dist/modules/security/content-scanner.js +8 -8
- package/dist/modules/security/data-sanitizer.js +9 -16
- package/dist/modules/security/encryption.js +32 -52
- package/dist/modules/security/index.js +2 -1
- package/dist/modules/security/network-validator.js +120 -43
- package/dist/modules/security/path-validator.js +61 -46
- package/dist/modules/security/protected-paths.js +73 -0
- package/dist/modules/security/rate-limiter.js +17 -49
- package/dist/modules/security/security-policies.js +25 -57
- package/dist/modules/security/session-encryption.js +32 -71
- package/dist/modules/security/session-isolation.js +15 -28
- package/dist/modules/session/auto-name.js +63 -0
- package/dist/modules/session/export.js +98 -0
- package/dist/modules/session/index.js +1 -0
- package/dist/modules/session/manager.js +79 -28
- package/dist/modules/session/module.js +4 -3
- package/dist/modules/session/store.js +57 -20
- package/dist/modules/setup/answers.js +35 -0
- package/dist/modules/setup/apply.js +79 -0
- package/dist/modules/setup/discovery.js +72 -0
- package/dist/modules/skills/index.js +1 -0
- package/dist/modules/skills/loader.js +9 -1
- package/dist/modules/skills/module.js +11 -0
- package/dist/modules/updater/changelog-reader.js +15 -16
- package/dist/modules/updater/checker.js +9 -4
- package/dist/modules/updater/dev-detect.js +7 -6
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +17 -15
- package/dist/modules/user-profile/compressor.js +2 -0
- package/dist/modules/user-profile/index.js +1 -0
- package/dist/modules/user-profile/profile.js +10 -1
- package/dist/modules/webui/answer-api.js +29 -0
- package/dist/modules/webui/assets.js +50 -0
- package/dist/modules/webui/body.js +21 -0
- package/dist/modules/webui/commands-settings.js +209 -0
- package/dist/modules/webui/commands-shared.js +60 -0
- package/dist/modules/webui/commands.js +174 -0
- package/dist/modules/webui/config-api.js +134 -0
- package/dist/modules/webui/file-api.js +202 -0
- package/dist/modules/webui/forward.js +159 -0
- package/dist/modules/webui/history-api.js +112 -0
- package/dist/modules/webui/host-state.js +60 -0
- package/dist/modules/webui/i18n-api.js +9 -0
- package/dist/modules/webui/limits.js +6 -0
- package/dist/modules/webui/log-forward.js +20 -0
- package/dist/modules/webui/memory-commands.js +64 -0
- package/dist/modules/webui/model-cache.js +18 -0
- package/dist/modules/webui/module.js +148 -0
- package/dist/modules/webui/persist.js +24 -0
- package/dist/modules/webui/prompt-io.js +97 -0
- package/dist/modules/webui/providers-api.js +337 -0
- package/dist/modules/webui/providers-view.js +56 -0
- package/dist/modules/webui/reasoning-view.js +15 -0
- package/dist/modules/webui/run-agent.js +81 -0
- package/dist/modules/webui/server.js +284 -0
- package/dist/modules/webui/sessions-api.js +178 -0
- package/dist/modules/webui/settings-api.js +561 -0
- package/dist/modules/webui/setup-api.js +201 -0
- package/dist/modules/webui/state.js +74 -0
- package/dist/modules/webui/util.js +261 -0
- package/dist/output/bus.js +7 -2
- package/dist/output/channel.js +80 -28
- package/dist/output/format.js +2 -1
- package/dist/output/index.js +1 -0
- package/dist/output/json-sink.js +5 -3
- package/dist/output/machine.js +4 -3
- package/dist/output/session-sink.js +4 -4
- package/dist/tools/approve.js +4 -0
- package/dist/tools/attach-image.js +5 -4
- package/dist/tools/auto-fixer.js +26 -44
- package/dist/tools/auto-tags.js +83 -0
- package/dist/tools/bash.js +193 -116
- package/dist/tools/browser.js +76 -31
- package/dist/tools/chunk-query.js +14 -6
- package/dist/tools/create-dir.js +9 -5
- package/dist/tools/delete-file.js +9 -5
- package/dist/tools/download-file.js +32 -7
- package/dist/tools/edit-file.js +52 -14
- package/dist/tools/enable-tools.js +6 -6
- package/dist/tools/executor.js +132 -27
- package/dist/tools/file-info.js +3 -1
- package/dist/tools/filter-tools.js +3 -3
- package/dist/tools/glob-tool.js +4 -1
- package/dist/tools/grep-tool.js +13 -10
- package/dist/tools/hidden-tools-block.js +6 -6
- package/dist/tools/index.js +16 -8
- package/dist/tools/list-dir.js +4 -2
- package/dist/tools/load-skill.js +7 -0
- package/dist/tools/mcp-call.js +15 -4
- package/dist/tools/meet-done.js +14 -0
- package/dist/tools/move-file.js +14 -7
- package/dist/tools/path-utils.js +29 -29
- package/dist/tools/pipeline-run.js +29 -7
- package/dist/tools/preview.js +1 -1
- package/dist/tools/process-ids.js +31 -0
- package/dist/tools/process-kill.js +9 -16
- package/dist/tools/process-list.js +1 -0
- package/dist/tools/process-log.js +11 -16
- package/dist/tools/process-wait.js +63 -0
- package/dist/tools/prompt-io.js +4 -3
- package/dist/tools/question.js +172 -25
- package/dist/tools/read-file.js +55 -34
- package/dist/tools/recall.js +14 -8
- package/dist/tools/registry.js +6 -5
- package/dist/tools/remember.js +8 -4
- package/dist/tools/scope-request.js +11 -6
- package/dist/tools/search-history.js +5 -2
- package/dist/tools/session-info.js +2 -0
- package/dist/tools/set-thinking.js +8 -4
- package/dist/tools/subagent.js +59 -39
- package/dist/tools/syntax-validator.js +292 -90
- package/dist/tools/user-input.js +22 -18
- package/dist/tools/web-browse.js +9 -3
- package/dist/tools/web-fetch.js +12 -23
- package/dist/tools/web-search.js +7 -5
- package/dist/tools/write-file.js +45 -15
- package/dist/ui/action-log.js +13 -0
- package/dist/ui/box.js +4 -3
- package/dist/ui/colors.js +2 -0
- package/dist/ui/diff.js +6 -5
- package/dist/ui/index.js +1 -0
- package/dist/ui/line-editor.js +230 -122
- package/dist/ui/line-math.js +10 -2
- package/dist/ui/md-formatter.js +57 -37
- package/dist/ui/output.js +8 -3
- package/dist/ui/plan-view.js +21 -10
- package/dist/ui/renderer.js +393 -67
- package/dist/ui/spinner.js +16 -5
- package/dist/ui/table.js +15 -17
- package/dist/ui/theme.js +143 -0
- package/dist/utils/child-run.js +20 -0
- package/dist/utils/error.js +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/retry.js +2 -2
- package/dist/utils/truncate.js +1 -1
- 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
|
|
157
|
-
if (
|
|
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.
|
|
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
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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", "",
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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);
|
package/dist/assets/js/store.js
CHANGED
|
@@ -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).
|
package/dist/assets/js/tabs.js
CHANGED
|
@@ -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
|
-
|
|
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() {
|