diff --git a/CHANGELOG.md b/CHANGELOG.md index 09db7ea1..56ab74be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,30 @@ - New WebUI sessions no longer persist `display.personality` into per-session `Session.personality`; only explicit personality changes remain durable, preventing stale global display defaults from overriding profile-scoped session behavior. Closes #2845. +## [v0.51.129] — 2026-05-24 — Release DA (stage-batch11 — 4-PR feature + perf batch) + +### Performance + +- **PR #2836** by @v2psv — HTTP/1.1 keep-alive for WebUI responses. Bumps `Handler.protocol_version` from the HTTP/1.0 default to `HTTP/1.1` so browsers can reuse TCP connections across normal API and static-file requests. Adds explicit `Content-Length` headers to hand-written responses that weren't already using shared `j()` / `t()` helpers. Adds `Content-Length: 0` to empty redirect / range-error responses. Switches SSE-style streaming endpoints from `Connection: keep-alive` to `Connection: close` (keep-alive is only safe when the response body is framed; SSE bodies have no fixed length). Significant first-paint / session-open improvements on high-RTT / VPN / proxied paths — author reports ~47% faster first paint and ~30-40% improvements on panel-load flows on a typical remote-host setup. + + **Opus pre-release advisor caught one missing framing site** in the on-the-fly folder ZIP download path (`/api/folder/download`): the body has no known length, doesn't use chunked encoding, and was relying on HTTP/1.0 connection-close-equals-EOF. Under HTTP/1.1 this would have left clients hanging waiting for the next response after the central-directory bytes finished. Patched inline before tag: add `Connection: close` header to mirror the SSE-endpoint pattern. Opus verified this was the ONLY remaining streaming response in the codebase that needed the header — all 12 hand-written response paths + 8 SSE streams + j()/t() helpers + auth flow were already correctly framed by the PR. + +### Added + +- **PR #2680** by @mccxj — Auxiliary Models settings card in Settings → Preferences. Lets users configure per-task model routing for 9 canonical side-task slots: vision, web extract, compression, session search, approval, MCP tool reasoning, title generation, skills hub, curator. Each slot exposes a provider dropdown + model dropdown plus an "auto (use main model)" / "auto (use provider default)" pair so users can keep aux routing implicit when they don't care. New endpoints: `GET /api/model/auxiliary` returns current assignments; `POST /api/model/set` writes assignments (`scope=auxiliary` for aux slots, `scope=main` for the default chat model) and supports `task="__reset__"` to reset all slots back to auto. 16 new i18n keys added across all 12 locales (en, it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr, tr — Turkish translations added in-stage to cover the sibling-PR collision with v0.51.127's Turkish locale baseline). 24 source-level test assertions covering HTML structure, JS logic, i18n parity, and route registration. + +- **PR #2842** by @AJV20 — PWA polish for installed launches. New `static/pwa-startup.js` is loaded synchronously in `` before the main UI bundle, so the page knows whether it's running standalone / in-browser / on iOS / offline before first paint. Marks `pwa-standalone`, `pwa-browser`, `pwa-ios`, `pwa-offline`, and short-lived `pwa-resumed` classes on ``. Exposes `window.HermesPWA.{isStandalone, syncMode, launchAction, promptInstall}` helpers and captures `beforeinstallprompt` / `appinstalled` early enough that any future install-prompt UI can chain off them. Manifest gains app identity / scope / `display_override` (`window-controls-overlay` → `standalone` → `minimal-ui`) and a "New conversation" PWA App Shortcut. Service worker pre-caches the startup helper, switches navigation and shell-asset fetches to `cache: 'no-store'` before falling back to CacheStorage. Boot path wires `?source=pwa&action=new-chat` to start a fresh chat instead of reopening the last saved session. The viewport meta now sets `maximum-scale=1, user-scalable=no` for native-feel — acknowledged trade-off against WCAG 2.1 1.4.4 (Resize text), intentionally kept for the PWA-installed feel of this user base. + +- **PR #2794** by @Michaelyklam — Runtime adapter route selection harness. Routes explicit adapter-mode chat starts through `build_runtime_adapter(...)` and keeps `legacy-direct` as the default `/api/chat/start` path. Continues the #1925 RFC slice progression: this is slice 4e, the default-off chat-start route-selection seam. Returns a bounded `501 Not Configured` response when `runner-local` is explicitly selected before a supervised runner client exists, instead of silently starting a legacy WebUI-owned run. New `_chat_start_response_from_run_start(...)` helper whitelists legacy-compatible chat-start response fields and keeps adapter-internal `run_id`, `status`, and `active_controls` out of public responses. Updates `docs/rfcs/hermes-run-adapter-contract.md` to mark #2744 shipped and define slice 4e. + +### Notes + +- Full pytest: **6,467 passed / 6 skipped / 3 xpassed / 8 subtests passed**. +- Opus pre-release advisor reviewed all 7 risk areas (HTTP framing surface completeness, PWA startup ordering, sibling-PR `api/routes.py` interaction, service worker cache invalidation, viewport-meta trade-off, runtime adapter response shape, locale-counter brittleness). Verdict: **1 MUST-FIX patched inline** (folder ZIP `Connection: close` header), **0 inline SHOULD-FIX**, 1 follow-up suggested (`set_auxiliary_model` could validate `task` against `AUX_TASK_SLOTS` whitelist — auth-gated, low severity, filing as follow-up). +- Agent self-verified: protocol_version bumped, SSE Connection-close + Content-Length plumbing, Auxiliary Models API surface (config + endpoints + frontend), PWA helpers + manifest shortcuts + display_override, Runtime adapter wiring + whitelisting, i18n parity for all 12 locales on the 16 new aux keys. +- Browser-verified at 1920×1080: Auxiliary Models card renders correctly under Settings → Preferences, 9 task slots with provider/model dropdowns, "Reset all to auto" button, layout consistent with surrounding Settings cards, no clutter or clipping. PWA classes populate on `` and HermesPWA namespace populates with 4 helpers as expected. +- In-stage commits added Turkish translations for #2680's 16 `settings_aux_*` / `settings_label_auxiliary_models` / `settings_desc_auxiliary_models` keys to close the sibling-collision gap with v0.51.127's Turkish locale (#2772). Bumped `test_auxiliary_models_settings.py::test_all_locales_have_auxiliary_keys` from `count == 11` to `count == 12` (the locale set grew when Turkish landed). + ## [v0.51.128] — 2026-05-24 — Release CZ (stage-batch10 — 2-PR perf + correctness batch) ### Fixed diff --git a/api/auth.py b/api/auth.py index df5f0e4c..5a49516f 100644 --- a/api/auth.py +++ b/api/auth.py @@ -435,10 +435,12 @@ def check_auth(handler, parsed) -> bool: return True # Not authorized if parsed.path.startswith('/api/'): + body = b'{"error":"Authentication required"}' handler.send_response(401) handler.send_header('Content-Type', 'application/json') + handler.send_header('Content-Length', str(len(body))) handler.end_headers() - handler.wfile.write(b'{"error":"Authentication required"}') + handler.wfile.write(body) else: handler.send_response(302) # Pass the original path as ?next= so login.js redirects back after auth. @@ -468,6 +470,7 @@ def check_auth(handler, parsed) -> bool: # `?`, `&`, `=`) gets percent-encoded. _next = _urlparse.quote(_path_with_query, safe='/') handler.send_header('Location', 'login?next=' + _next) + handler.send_header('Content-Length', '0') handler.end_headers() return False diff --git a/api/config.py b/api/config.py index 293dc953..66bcb570 100644 --- a/api/config.py +++ b/api/config.py @@ -2185,6 +2185,112 @@ def set_hermes_default_model(model_id: str) -> dict: return {"ok": True, "model": persisted_model} +# ── Auxiliary model configuration ────────────────────────────────────────── + +# Canonical auxiliary task slots. Keep in sync with hermes_cli/config.py +# DEFAULT_CONFIG["auxiliary"] and hermes_cli/web_server.py _AUX_TASK_SLOTS. +AUX_TASK_SLOTS: tuple[str, ...] = ( + "vision", + "web_extract", + "compression", + "session_search", + "skills_hub", + "approval", + "mcp", + "title_generation", + "curator", +) + + +def get_auxiliary_models() -> dict: + """Return current auxiliary task assignments from config.yaml. + + Shape: + { + "tasks": [ + {"task": "vision", "provider": "auto", "model": "", "base_url": ""}, + ... + ], + "main": {"provider": "openrouter", "model": "anthropic/claude-opus-4.7"}, + } + """ + reload_config() + model_cfg = cfg.get("model", {}) + if not isinstance(model_cfg, dict): + model_cfg = {} + main_provider = str(model_cfg.get("provider") or "").strip() + main_model = str(model_cfg.get("default") or model_cfg.get("name") or "").strip() + + aux_cfg = cfg.get("auxiliary", {}) + if not isinstance(aux_cfg, dict): + aux_cfg = {} + + tasks = [] + for slot in AUX_TASK_SLOTS: + entry = aux_cfg.get(slot, {}) + if not isinstance(entry, dict): + entry = {} + tasks.append({ + "task": slot, + "provider": str(entry.get("provider") or "auto").strip(), + "model": str(entry.get("model") or "").strip(), + "base_url": str(entry.get("base_url") or "").strip(), + }) + + return { + "tasks": tasks, + "main": {"provider": main_provider, "model": main_model}, + } + + +def set_auxiliary_model(task: str, provider: str, model: str) -> dict: + """Persist an auxiliary model assignment in config.yaml. + + Special case: task='__reset__' clears all auxiliary slots. + """ + config_path = _get_config_path() + with _cfg_lock: + config_data = _load_yaml_config_file(config_path) + + if task == "__reset__": + # Per-slot reset: set each slot to auto, preserving extra fields + # (timeout, extra_body, api_key, base_url, download_timeout, etc.) + aux_cfg = config_data.get("auxiliary", {}) + if not isinstance(aux_cfg, dict): + aux_cfg = {} + for slot in AUX_TASK_SLOTS: + slot_cfg = aux_cfg.get(slot, {}) + if not isinstance(slot_cfg, dict): + slot_cfg = {} + slot_cfg["provider"] = "auto" + slot_cfg["model"] = "" + aux_cfg[slot] = slot_cfg + config_data["auxiliary"] = aux_cfg + else: + aux_cfg = config_data.get("auxiliary", {}) + if not isinstance(aux_cfg, dict): + aux_cfg = {} + slot_cfg = aux_cfg.get(task, {}) + if not isinstance(slot_cfg, dict): + slot_cfg = {} + slot_cfg["provider"] = provider or "auto" + slot_cfg["model"] = model or "" + if provider and (provider.startswith("custom:") or provider == "custom"): + try: + _, _, resolved_base_url = resolve_model_provider(model) + if resolved_base_url: + slot_cfg["base_url"] = str(resolved_base_url).strip().rstrip("/") + except Exception: + pass + aux_cfg[task] = slot_cfg + config_data["auxiliary"] = aux_cfg + + _save_yaml_config_file(config_path, config_data) + + reload_config() + return {"ok": True, "task": task, "provider": provider, "model": model} + + # ── TTL cache for get_available_models() ───────────────────────────────────── _available_models_cache: dict | None = None _available_models_cache_ts: float = 0.0 diff --git a/api/kanban_bridge.py b/api/kanban_bridge.py index 63bef9cd..f0d5d261 100644 --- a/api/kanban_bridge.py +++ b/api/kanban_bridge.py @@ -1022,7 +1022,7 @@ def _handle_events_sse_stream(handler, parsed): handler.send_header("Content-Type", "text/event-stream; charset=utf-8") handler.send_header("Cache-Control", "no-cache") handler.send_header("X-Accel-Buffering", "no") - handler.send_header("Connection", "keep-alive") + handler.send_header("Connection", "close") handler.end_headers() # Send an initial frame so the client knows the connection is open diff --git a/api/routes.py b/api/routes.py index b6ee042c..54b8818a 100644 --- a/api/routes.py +++ b/api/routes.py @@ -3687,6 +3687,11 @@ def handle_get(handler, parsed) -> bool: if parsed.path == "/api/models/live": return _handle_live_models(handler, parsed) + # ── Auxiliary models (GET/POST) ── + if parsed.path == "/api/model/auxiliary": + from api.config import get_auxiliary_models + return j(handler, get_auxiliary_models()) + if parsed.path == "/api/dashboard/status": from api import dashboard_probe @@ -4857,6 +4862,25 @@ def handle_post(handler, parsed) -> bool: except RuntimeError as e: return bad(handler, str(e), 500) + # ── Auxiliary model set (POST) ── + if parsed.path == "/api/model/set": + scope = str(body.get("scope") or "").strip() + task = str(body.get("task") or "").strip() + provider = str(body.get("provider") or "auto").strip() + model = str(body.get("model") or "").strip() + if scope == "auxiliary": + from api.config import set_auxiliary_model + try: + return j(handler, set_auxiliary_model(task, provider, model)) + except Exception as exc: + return bad(handler, str(exc), status=400) + if scope == "main": + try: + return j(handler, set_hermes_default_model(model)) + except ValueError as exc: + return bad(handler, str(exc), status=400) + return bad(handler, f"unknown scope: {scope}", status=400) + # ── Providers (POST) ── if parsed.path == "/api/providers": provider_id = (body.get("provider") or "").strip().lower() @@ -6216,13 +6240,15 @@ def handle_post(handler, parsed) -> bool: _record_login_attempt(client_ip) return bad(handler, "Invalid password", 401) cookie_val = create_session() + body = json.dumps({"ok": True}).encode() handler.send_response(200) handler.send_header("Content-Type", "application/json") + handler.send_header("Content-Length", str(len(body))) handler.send_header("Cache-Control", "no-store") _security_headers(handler) set_auth_cookie(handler, cookie_val) handler.end_headers() - handler.wfile.write(json.dumps({"ok": True}).encode()) + handler.wfile.write(body) return True if parsed.path == "/api/auth/logout": @@ -6231,13 +6257,15 @@ def handle_post(handler, parsed) -> bool: cookie_val = parse_cookie(handler) if cookie_val: invalidate_session(cookie_val) + body = json.dumps({"ok": True}).encode() handler.send_response(200) handler.send_header("Content-Type", "application/json") + handler.send_header("Content-Length", str(len(body))) handler.send_header("Cache-Control", "no-store") _security_headers(handler) clear_auth_cookie(handler) handler.end_headers() - handler.wfile.write(json.dumps({"ok": True}).encode()) + handler.wfile.write(body) return True # ── Checkpoints / Rollback (POST) ── @@ -6577,7 +6605,7 @@ def _handle_sse_stream(handler, parsed): handler.send_header("Content-Type", "text/event-stream; charset=utf-8") handler.send_header("Cache-Control", "no-cache") handler.send_header("X-Accel-Buffering", "no") - handler.send_header("Connection", "keep-alive") + handler.send_header("Connection", "close") handler.end_headers() try: _replay_run_journal(handler, stream_id, _parse_run_journal_after_seq(qs)) @@ -6589,7 +6617,7 @@ def _handle_sse_stream(handler, parsed): handler.send_header("Content-Type", "text/event-stream; charset=utf-8") handler.send_header("Cache-Control", "no-cache") handler.send_header("X-Accel-Buffering", "no") - handler.send_header("Connection", "keep-alive") + handler.send_header("Connection", "close") handler.end_headers() try: while True: @@ -6720,7 +6748,7 @@ def _handle_terminal_output(handler, parsed): handler.send_header("Content-Type", "text/event-stream; charset=utf-8") handler.send_header("Cache-Control", "no-cache") handler.send_header("X-Accel-Buffering", "no") - handler.send_header("Connection", "keep-alive") + handler.send_header("Connection", "close") handler.end_headers() try: while True: @@ -6798,7 +6826,7 @@ def _handle_gateway_sse_stream(handler, parsed): handler.send_header('Content-Type', 'text/event-stream; charset=utf-8') handler.send_header('Cache-Control', 'no-cache') handler.send_header('X-Accel-Buffering', 'no') - handler.send_header('Connection', 'keep-alive') + handler.send_header('Connection', 'close') handler.end_headers() q = watcher.subscribe() @@ -6831,7 +6859,7 @@ def _handle_session_events_stream(handler): handler.send_header('Content-Type', 'text/event-stream; charset=utf-8') handler.send_header('Cache-Control', 'no-cache') handler.send_header('X-Accel-Buffering', 'no') - handler.send_header('Connection', 'keep-alive') + handler.send_header('Connection', 'close') handler.end_headers() q = subscribe_session_events() @@ -6917,6 +6945,7 @@ def _serve_file_bytes(handler, target: Path, mime: str, disposition: str, cache_ handler.send_response(416) handler.send_header("Content-Range", f"bytes */{file_size}") handler.send_header("Accept-Ranges", "bytes") + handler.send_header("Content-Length", "0") _security_headers(handler) handler.end_headers() return True @@ -7027,10 +7056,12 @@ def _handle_media(handler, parsed): if is_auth_enabled(): cv = parse_cookie(handler) if not (cv and verify_session(cv)): + body = b'{"error":"Authentication required"}' handler.send_response(401) handler.send_header("Content-Type", "application/json") + handler.send_header("Content-Length", str(len(body))) handler.end_headers() - handler.wfile.write(b'{"error":"Authentication required"}') + handler.wfile.write(body) return qs = parse_qs(parsed.query) @@ -7264,6 +7295,15 @@ def _handle_folder_download(handler, parsed): _content_disposition_value("attachment", zip_name), ) handler.send_header("Cache-Control", "no-store") + # Under HTTP/1.1 (Handler.protocol_version, see server.py post-#2836) + # a response with no Content-Length and no Transfer-Encoding requires + # Connection: close so the client knows the body ends at FIN. The ZIP + # is built on-the-fly so we cannot send Content-Length up front; mirror + # the SSE-endpoint pattern #2836 uses. Without this header the client + # hangs waiting for the next pipelined response after the central + # directory bytes finish. Caught by Opus pre-release advisor on + # stage-batch11. + handler.send_header("Connection", "close") handler.end_headers() written = 0 @@ -7389,7 +7429,7 @@ def _handle_approval_sse_stream(handler, parsed): handler.send_header('Content-Type', 'text/event-stream; charset=utf-8') handler.send_header('Cache-Control', 'no-cache') handler.send_header('X-Accel-Buffering', 'no') - handler.send_header('Connection', 'keep-alive') + handler.send_header('Connection', 'close') handler.end_headers() from api.streaming import _sse @@ -7490,7 +7530,7 @@ def _handle_clarify_sse_stream(handler, parsed): handler.send_header('Content-Type', 'text/event-stream; charset=utf-8') handler.send_header('Cache-Control', 'no-cache') handler.send_header('X-Accel-Buffering', 'no') - handler.send_header('Connection', 'keep-alive') + handler.send_header('Connection', 'close') handler.end_headers() from api.streaming import _sse @@ -8463,6 +8503,41 @@ def _start_chat_stream_for_session( return response +def _runtime_runner_client_factory(): + """Return the runner-local client when a supervised backend exists. + + Slice 4d wires the `/api/chat/start` selection point without silently falling + back to the legacy in-process runtime when `runner-local` is explicitly + requested. The supervised runner backend itself is intentionally not created + in this helper yet; a later slice can replace this factory with the concrete + client while keeping the route contract stable. + """ + raise NotImplementedError("runner-local chat backend is not configured") + + +def _chat_start_response_from_run_start(result): + """Expose only the legacy browser-facing chat-start response fields.""" + payload = dict(getattr(result, "payload", {}) or {}) + response = {} + for key in ( + "stream_id", + "session_id", + "pending_started_at", + "turn_id", + "title", + "effective_model", + "effective_model_provider", + "error", + "active_stream_id", + "_status", + ): + if key in payload: + response[key] = payload[key] + response.setdefault("stream_id", result.stream_id) + response.setdefault("session_id", result.session_id) + return response + + def _runtime_adapter_goal_action(goal_args: str) -> str: """Return the bounded RuntimeAdapter goal action for WebUI /goal args.""" action = str(goal_args or "").strip().lower() @@ -8672,10 +8747,12 @@ def _handle_chat_start(handler, body, diag=None): from api.runtime_adapter import ( LegacyJournalRuntimeAdapter, StartRunRequest, + build_runtime_adapter, runtime_adapter_enabled, + runtime_adapter_runner_enabled, ) - if runtime_adapter_enabled(): + if runtime_adapter_enabled() or runtime_adapter_runner_enabled(): def _legacy_start_run(request: StartRunRequest) -> dict: return _start_chat_stream_for_session( s, @@ -8688,23 +8765,32 @@ def _handle_chat_start(handler, body, diag=None): diag=diag, ) - adapter = LegacyJournalRuntimeAdapter(start_run_delegate=_legacy_start_run) - result = adapter.start_run( - StartRunRequest( - session_id=s.session_id, - message=msg, - attachments=attachments, - workspace=workspace, - profile=getattr(s, "profile", None), - provider=model_provider, - model=model, - source="webui", - metadata={"route": "/api/chat/start"}, + def _legacy_adapter_factory(): + return LegacyJournalRuntimeAdapter(start_run_delegate=_legacy_start_run) + + try: + adapter = build_runtime_adapter( + legacy_adapter_factory=_legacy_adapter_factory, + runner_client_factory=_runtime_runner_client_factory, ) - ) - response = dict(result.payload) - response.setdefault("stream_id", result.stream_id) - response.setdefault("session_id", result.session_id) + if adapter is None: + raise NotImplementedError("runtime adapter selection returned no adapter") + result = adapter.start_run( + StartRunRequest( + session_id=s.session_id, + message=msg, + attachments=attachments, + workspace=workspace, + profile=getattr(s, "profile", None), + provider=model_provider, + model=model, + source="webui", + metadata={"route": "/api/chat/start"}, + ) + ) + except NotImplementedError as exc: + return j(handler, {"error": str(exc)}, status=501) + response = _chat_start_response_from_run_start(result) else: response = _start_chat_stream_for_session( s, diff --git a/docs/rfcs/hermes-run-adapter-contract.md b/docs/rfcs/hermes-run-adapter-contract.md index 9732044e..71ef4da2 100644 --- a/docs/rfcs/hermes-run-adapter-contract.md +++ b/docs/rfcs/hermes-run-adapter-contract.md @@ -4,7 +4,7 @@ - **Author:** @Michaelyklam - **Updated by:** @franksong2702 - **Created:** 2026-05-11 -- **Revised:** 2026-05-21 +- **Revised:** 2026-05-23 - **Tracking issue:** [#1925](https://github.com/nesquena/hermes-webui/issues/1925) ## Credit and Scope @@ -52,7 +52,7 @@ The immediate goal is not to build a sidecar. The immediate goal is to define th browser contract, classify current runtime state, and gate the first reversible journal slice. -## Current Gate State — 2026-05-21 +## Current Gate State — 2026-05-23 Slice 1 is now past the first active validation gate: @@ -104,11 +104,14 @@ adapter-seam work: `runner-local` adapter selection point and `build_runtime_adapter(...)` factory wiring around an injected runner client. Live browser chat routes still stay on the legacy backend, and no supervised runner process exists yet. -- The next implementation gate is a supervised/local runner backend proposal and - route-selection harness. It must stay default-off, keep legacy fallback intact, - pass explicit profile/workspace/model payloads instead of mutating WebUI - process globals, and avoid recreating `STREAMS` / `CANCEL_FLAGS` / approval - queues / clarify queues under new names. +- #2744 shipped the Slice 4d supervised runner route gate in v0.51.108. +- The next implementation slice is a default-off runner route-selection harness + for `/api/chat/start`. It should only engage when `runner-local` is explicitly + selected, return a bounded not-configured error until a supervised runner + client exists, keep `legacy-direct` / `legacy-journal` fallback intact, pass + explicit profile/workspace/model payloads instead of mutating WebUI process + globals, and avoid recreating `STREAMS` / `CANCEL_FLAGS` / approval queues / + clarify queues under new names. The next gate is runner-backend plumbing, not queue implementation by default. Queue / continue routing should only move before Slice 4 if a future @@ -843,6 +846,10 @@ Non-goals for Slice 4c: #### Slice 4d: Supervised runner backend route gate +Status as of 2026-05-23: shipped in v0.51.108 via #2744. The gate remains a +docs/test contract: it defines the default-off route-selection requirements but +does not itself route live chat to a runner backend. + After `runner-local` selection exists, the next reviewable gate should define the first supervised/local runner backend and the route-selection harness before live browser chat can use it. This is still a contract/test slice first: no default-on @@ -896,6 +903,51 @@ Non-goals for Slice 4d: - no broad UI/product surface migration; WebUI remains the rich workbench while only execution ownership moves. +#### Slice 4e: Default-off runner chat-start route-selection harness + +The first implementation after the Slice 4d gate should wire the +`/api/chat/start` selection point to the existing `RuntimeAdapter` factory +without adding a supervised runner process yet. The harness must make the +selection behavior explicit: `legacy-direct` stays default, `legacy-journal` +continues to delegate to the legacy in-process stream path, and `runner-local` +does not silently fall back to legacy when no runner client is configured. + +Scope: + +- route `/api/chat/start` through `build_runtime_adapter(...)` when an adapter + mode is explicitly selected; +- keep the successful browser response whitelisted to legacy-compatible fields + such as `stream_id`, `session_id`, `pending_started_at`, `turn_id`, `title`, + and effective model/provider metadata; +- return a bounded not-configured error for `runner-local` until a supervised + runner client/backend lands; +- pass the existing explicit `StartRunRequest` payload fields across the seam. + +Acceptance tests for Slice 4e: + +1. **Default remains legacy-direct.** With no adapter env var, `/api/chat/start` + keeps using `_start_chat_stream_for_session(...)` directly. +2. **Legacy-journal remains behavior-preserving.** The flagged legacy adapter + still delegates to the same stream-start helper and preserves the public + response shape. +3. **Runner-local does not fallback silently.** If `runner-local` is selected but + no runner client exists, the route returns a bounded error instead of starting + a WebUI-owned legacy run behind the operator's back. +4. **No adapter-internal response drift.** `run_id`, `status`, and + `active_controls` remain internal until a later contract explicitly exposes + them. +5. **No runtime-surrogate globals.** The harness does not add runner-owned stream, + cancel, approval, clarify, cached-agent, goal, or queue maps to the main WebUI + process. + +Non-goals for Slice 4e: + +- no supervised runner process yet; +- no default-on runner mode; +- no execution-survives-WebUI-restart claim for production chat turns; +- no removal of `legacy-direct` or `legacy-journal`; +- no server-side queue endpoint or queue scheduler just for adapter symmetry. + ## First Meaningful Success Criteria The first meaningful milestones are deliberately split. diff --git a/server.py b/server.py index e6636209..e6a2c65a 100644 --- a/server.py +++ b/server.py @@ -170,6 +170,13 @@ class QuietHTTPServer(ThreadingHTTPServer): class Handler(BaseHTTPRequestHandler): + # HTTP/1.1 enables keep-alive connection reuse — major latency win on + # high-RTT links where every saved TCP handshake is 2×RTT. Each response + # MUST declare framing (Content-Length, Transfer-Encoding: chunked, or + # Connection: close) so the client knows where the message ends. Helpers + # j()/t() emit Content-Length; SSE/streaming endpoints emit + # Connection: close because the body has no terminator. See PR notes. + protocol_version = "HTTP/1.1" timeout = 30 # seconds — kills idle/incomplete connections to prevent thread exhaustion def setup(self): diff --git a/static/boot.js b/static/boot.js index 2b6d8347..b1c67dd6 100644 --- a/static/boot.js +++ b/static/boot.js @@ -1662,6 +1662,17 @@ function applyBotName(){ if(typeof fetchReasoningChip==='function') fetchReasoningChip(); if(typeof refreshProviderQuotaIndicator==='function') refreshProviderQuotaIndicator(); const urlSession=(typeof _sessionIdFromLocation==='function')?_sessionIdFromLocation():null; + const pwaLaunchAction=(window.HermesPWA&&typeof window.HermesPWA.launchAction==='function') + ? window.HermesPWA.launchAction() + : null; + if(pwaLaunchAction==='new-chat'){ + try{ + await newSession(true); + if(S.session) await _startBootModelDropdown(); + S._bootReady=true; + syncTopbar();syncWorkspacePanelState();await renderSessionList();if(typeof startGatewaySSE==='function')startGatewaySSE();return; + }catch(e){console.warn('[pwa] new-chat launch action failed', e);} + } const savedLocal=localStorage.getItem('hermes-webui-session'); const saved=urlSession||savedLocal; if(saved){ diff --git a/static/i18n.js b/static/i18n.js index c1ba196f..d103d996 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -548,6 +548,22 @@ const LOCALES = { settings_save_btn: 'Save Settings', settings_label_model: 'Default Model', settings_desc_model: 'Used for new conversations. Existing conversations keep their selected model.', + settings_label_auxiliary_models: 'Auxiliary Models', + settings_desc_auxiliary_models: 'Side-task routing for vision, compression, title generation, etc. "Auto" uses your main chat model.', + settings_btn_reset_aux_models: 'Reset all to auto', + settings_btn_apply_aux_models: 'Apply changes', + settings_aux_provider_auto: 'use main model', + settings_aux_model_auto: 'auto (use provider default)', + settings_aux_model_custom: 'Custom model…', + settings_aux_model_custom_prompt: 'Enter model ID:', + settings_aux_loading: 'Loading auxiliary models…', + settings_aux_load_failed: 'Could not load auxiliary model settings. Make sure the agent API is available.', + settings_aux_reset_confirm_title: 'Reset auxiliary models?', + settings_aux_reset_confirm_msg: 'This will set all auxiliary tasks to auto (use main model).', + settings_aux_reset_done: 'Auxiliary models reset to auto', + settings_aux_save_failed: 'Failed to save auxiliary model', + settings_aux_saved: 'Auxiliary models updated', + settings_aux_no_changes: 'No changes to apply', settings_label_send_key: 'Send Key', settings_label_theme: 'Theme', settings_label_skin: 'Skin', @@ -1791,6 +1807,22 @@ const LOCALES = { settings_save_btn: 'Salva Impostazioni', settings_label_model: 'Modello Predefinito', settings_desc_model: 'Usato per le nuove conversazioni. Le conversazioni esistenti mantengono il modello selezionato.', + settings_label_auxiliary_models: 'Modelli Ausiliari', + settings_desc_auxiliary_models: 'Routing per attività secondarie come visione, compressione, generazione titoli, ecc. "Auto" utilizza il modello chat principale.', + settings_btn_reset_aux_models: 'Ripristina tutto ad auto', + settings_btn_apply_aux_models: 'Applica modifiche', + settings_aux_provider_auto: 'usa modello principale', + settings_aux_model_auto: 'auto (usa predefinito del provider)', + settings_aux_model_custom: 'Modello personalizzato…', + settings_aux_model_custom_prompt: 'Inserisci ID modello:', + settings_aux_loading: 'Caricamento modelli ausiliari…', + settings_aux_load_failed: 'Impossibile caricare le impostazioni dei modelli ausiliari. Assicurati che l’API dell’agent sia disponibile.', + settings_aux_reset_confirm_title: 'Ripristinare modelli ausiliari?', + settings_aux_reset_confirm_msg: 'Questo imposterà tutte le attività ausiliarie ad auto (usa modello principale).', + settings_aux_reset_done: 'Modelli ausiliari ripristinati ad auto', + settings_aux_save_failed: 'Salvataggio del modello ausiliario non riuscito', + settings_aux_saved: 'Modelli ausiliari aggiornati', + settings_aux_no_changes: 'Nessuna modifica da applicare', settings_label_send_key: 'Tasto Invio', settings_label_theme: 'Tema', settings_label_skin: 'Skin', @@ -3026,6 +3058,22 @@ const LOCALES = { settings_save_btn: '設定を保存', settings_label_model: 'デフォルトモデル', settings_desc_model: '新しい会話で使用されます。既存の会話は選択済みモデルを保持します。', + settings_label_auxiliary_models: '補助モデル', + settings_desc_auxiliary_models: 'ビジョン、圧縮、タイトル生成などの補助タスクのルーティング。「自動」はメインチャットモデルを使用します。', + settings_btn_reset_aux_models: 'すべて自動にリセット', + settings_btn_apply_aux_models: '変更を適用', + settings_aux_provider_auto: 'メインモデルを使用', + settings_aux_model_auto: '自動(プロバイダーのデフォルトを使用)', + settings_aux_model_custom: 'カスタムモデル…', + settings_aux_model_custom_prompt: 'モデルIDを入力:', + settings_aux_loading: '補助モデルを読み込み中…', + settings_aux_load_failed: '補助モデル設定を読み込めませんでした。エージェントAPIが利用可能であることを確認してください。', + settings_aux_reset_confirm_title: '補助モデルをリセットしますか?', + settings_aux_reset_confirm_msg: 'すべての補助タスクを自動(メインモデルを使用)に設定します。', + settings_aux_reset_done: '補助モデルを自動にリセットしました', + settings_aux_save_failed: '補助モデルの保存に失敗しました', + settings_aux_saved: '補助モデルを更新しました', + settings_aux_no_changes: '適用する変更はありません', settings_label_send_key: '送信キー', settings_label_theme: 'テーマ', settings_label_skin: 'スキン', @@ -4077,6 +4125,22 @@ const LOCALES = { settings_save_btn: 'Сохранить настройки', settings_label_model: 'Модель по умолчанию', settings_desc_model: 'Используется для новых бесед. Существующие беседы сохраняют выбранную модель.', + settings_label_auxiliary_models: 'Вспомогательные модели', + settings_desc_auxiliary_models: 'Маршрутизация побочных задач: зрение, сжатие, генерация заголовков и т.д. «Авто» использует основную модель чата.', + settings_btn_reset_aux_models: 'Сбросить всё на авто', + settings_btn_apply_aux_models: 'Применить изменения', + settings_aux_provider_auto: 'использовать основную модель', + settings_aux_model_auto: 'авто (по умолчанию провайдера)', + settings_aux_model_custom: 'Пользовательская модель…', + settings_aux_model_custom_prompt: 'Введите ID модели:', + settings_aux_loading: 'Загрузка вспомогательных моделей…', + settings_aux_load_failed: 'Не удалось загрузить настройки вспомогательных моделей. Убедитесь, что API агента доступен.', + settings_aux_reset_confirm_title: 'Сбросить вспомогательные модели?', + settings_aux_reset_confirm_msg: 'Все вспомогательные задачи будут установлены на «авто» (использовать основную модель).', + settings_aux_reset_done: 'Вспомогательные модели сброшены на авто', + settings_aux_save_failed: 'Не удалось сохранить вспомогательную модель', + settings_aux_saved: 'Вспомогательные модели обновлены', + settings_aux_no_changes: 'Нет изменений для применения', settings_label_send_key: 'Клавиша отправки', settings_label_theme: 'Тема', settings_label_language: 'Язык', @@ -5231,6 +5295,22 @@ const LOCALES = { settings_save_btn: 'Guardar configuración', settings_label_model: 'Modelo predeterminado', settings_desc_model: 'Se usa para conversaciones nuevas. Las conversaciones existentes conservan su modelo seleccionado.', + settings_label_auxiliary_models: 'Modelos auxiliares', + settings_desc_auxiliary_models: 'Enrutamiento para tareas secundarias como visión, compresión, generación de títulos, etc. «Auto» usa el modelo de chat principal.', + settings_btn_reset_aux_models: 'Restablecer todo a auto', + settings_btn_apply_aux_models: 'Aplicar cambios', + settings_aux_provider_auto: 'usar modelo principal', + settings_aux_model_auto: 'auto (usar predeterminado del proveedor)', + settings_aux_model_custom: 'Modelo personalizado…', + settings_aux_model_custom_prompt: 'Ingrese ID del modelo:', + settings_aux_loading: 'Cargando modelos auxiliares…', + settings_aux_load_failed: 'No se pudieron cargar las configuraciones de modelos auxiliares. Asegúrese de que la API del agente esté disponible.', + settings_aux_reset_confirm_title: '¿Restablecer modelos auxiliares?', + settings_aux_reset_confirm_msg: 'Esto establecerá todas las tareas auxiliares en auto (usar modelo principal).', + settings_aux_reset_done: 'Modelos auxiliares restablecidos a auto', + settings_aux_save_failed: 'Error al guardar el modelo auxiliar', + settings_aux_saved: 'Modelos auxiliares actualizados', + settings_aux_no_changes: 'Sin cambios para aplicar', settings_label_send_key: 'Tecla de envío', settings_label_theme: 'Tema', settings_label_skin: 'Piel', @@ -6388,6 +6468,22 @@ const LOCALES = { settings_save_btn: 'Einstellungen speichern', settings_label_model: 'Standard-Modell', settings_desc_model: 'Wird für neue Chats verwendet. Bestehende Chats behalten ihr ausgewähltes Modell.', + settings_label_auxiliary_models: 'Hilfsmodelle', + settings_desc_auxiliary_models: 'Routing für Nebenaufgaben wie Vision, Komprimierung, Titelgenerierung usw. „Auto" verwendet das Haupt-Chat-Modell.', + settings_btn_reset_aux_models: 'Alle auf auto zurücksetzen', + settings_btn_apply_aux_models: 'Änderungen anwenden', + settings_aux_provider_auto: 'Hauptmodell verwenden', + settings_aux_model_auto: 'auto (Provider-Standard verwenden)', + settings_aux_model_custom: 'Benutzerdefiniertes Modell…', + settings_aux_model_custom_prompt: 'Modell-ID eingeben:', + settings_aux_loading: 'Hilfsmodelle werden geladen…', + settings_aux_load_failed: 'Hilfsmodelle-Einstellungen konnten nicht geladen werden. Stellen Sie sicher, dass die Agent-API verfügbar ist.', + settings_aux_reset_confirm_title: 'Hilfsmodelle zurücksetzen?', + settings_aux_reset_confirm_msg: 'Dies setzt alle Hilfsaufgaben auf auto (Hauptmodell verwenden).', + settings_aux_reset_done: 'Hilfsmodelle auf auto zurückgesetzt', + settings_aux_save_failed: 'Hilfsmodell konnte nicht gespeichert werden', + settings_aux_saved: 'Hilfsmodelle aktualisiert', + settings_aux_no_changes: 'Keine Änderungen anzuwenden', settings_label_send_key: 'Sende-Taste', settings_label_theme: 'Theme', settings_label_skin: 'Skin', @@ -7597,6 +7693,22 @@ const LOCALES = { settings_save_btn: '保存设置', settings_label_model: '默认模型', settings_desc_model: '用于新对话。现有对话保持各自选定的模型。', + settings_label_auxiliary_models: '辅助模型', + settings_desc_auxiliary_models: '视觉分析、上下文压缩、标题生成等辅助任务的路由。"自动"表示使用主聊天模型。', + settings_btn_reset_aux_models: '全部重置为自动', + settings_btn_apply_aux_models: '应用更改', + settings_aux_provider_auto: '使用主模型', + settings_aux_model_auto: '自动(使用提供商默认)', + settings_aux_model_custom: '自定义模型…', + settings_aux_model_custom_prompt: '输入模型 ID:', + settings_aux_loading: '正在加载辅助模型…', + settings_aux_load_failed: '无法加载辅助模型设置,请确保 Agent API 可用。', + settings_aux_reset_confirm_title: '重置辅助模型?', + settings_aux_reset_confirm_msg: '这将把所有辅助任务设置为自动(使用主模型)。', + settings_aux_reset_done: '辅助模型已重置为自动', + settings_aux_save_failed: '辅助模型保存失败', + settings_aux_saved: '辅助模型已更新', + settings_aux_no_changes: '没有需要应用的更改', settings_label_send_key: '发送快捷键', settings_label_theme: '主题', settings_label_skin: '皮肤', @@ -8783,6 +8895,22 @@ const LOCALES = { settings_save_btn: '\u5132\u5b58\u8a2d\u5b9a', settings_label_model: '\u9ed8\u8a8d\u6a21\u578b', settings_desc_model: '\u7528\u65bc\u65b0\u6703\u8a71\u3002\u73fe\u6709\u6703\u8a71\u6703\u4fdd\u7559\u5404\u81ea\u9078\u5b9a\u7684\u6a21\u578b\u3002', + settings_label_auxiliary_models: '\u8f14\u52a9\u6a21\u578b', + settings_desc_auxiliary_models: '\u8996\u89ba\u5206\u6790\u3001\u4e0a\u4e0b\u6587\u58d3\u7e2e\u3001\u6a19\u984c\u7522\u751f\u7b49\u8f14\u52a9\u4efb\u52d9\u7684\u8def\u7531\u3002\u300c\u81ea\u52d5\u300d\u8868\u793a\u4f7f\u7528\u4e3b\u804a\u5929\u6a21\u578b\u3002', + settings_btn_reset_aux_models: '\u5168\u90e8\u91cd\u7f6e\u70ba\u81ea\u52d5', + settings_btn_apply_aux_models: '\u61c9\u7528\u8b8a\u66f4', + settings_aux_provider_auto: '\u4f7f\u7528\u4e3b\u6a21\u578b', + settings_aux_model_auto: '\u81ea\u52d5\uff08\u4f7f\u7528\u63d0\u4f9b\u5546\u9810\u8a2d\uff09', + settings_aux_model_custom: '\u81ea\u8a02\u6a21\u578b\u2026', + settings_aux_model_custom_prompt: '\u8f38\u5165\u6a21\u578b ID\uff1a', + settings_aux_loading: '\u6b63\u5728\u8f09\u5165\u8f14\u52a9\u6a21\u578b\u2026', + settings_aux_load_failed: '\u7121\u6cd5\u8f09\u5165\u8f14\u52a9\u6a21\u578b\u8a2d\u5b9a\uff0c\u8acb\u78ba\u4fdd Agent API \u53ef\u7528\u3002', + settings_aux_reset_confirm_title: '\u91cd\u7f6e\u8f14\u52a9\u6a21\u578b\uff1f', + settings_aux_reset_confirm_msg: '\u9019\u5c07\u628a\u6240\u6709\u8f14\u52a9\u4efb\u52d9\u8a2d\u5b9a\u70ba\u81ea\u52d5\uff08\u4f7f\u7528\u4e3b\u6a21\u578b\uff09\u3002', + settings_aux_reset_done: '\u8f14\u52a9\u6a21\u578b\u5df2\u91cd\u7f6e\u70ba\u81ea\u52d5', + settings_aux_save_failed: '\u8f14\u52a9\u6a21\u578b\u5132\u5b58\u5931\u6557', + settings_aux_saved: '\u8f14\u52a9\u6a21\u578b\u5df2\u66f4\u65b0', + settings_aux_no_changes: '\u6c92\u6709\u9700\u8981\u61c9\u7528\u7684\u8b8a\u66f4', settings_label_send_key: '\u767c\u9001\u5feb\u6377\u9375', settings_label_theme: '\u4e3b\u984c', settings_label_skin: '佈景', @@ -10096,6 +10224,22 @@ const LOCALES = { settings_save_btn: 'Salvar Configurações', settings_label_model: 'Modelo Padrão', settings_desc_model: 'Usado para novas conversas. Conversas existentes mantêm o modelo selecionado.', + settings_label_auxiliary_models: 'Modelos auxiliares', + settings_desc_auxiliary_models: 'Roteamento para tarefas secundárias como visão, compressão, geração de títulos, etc. "Auto" usa o modelo de chat principal.', + settings_btn_reset_aux_models: 'Restaurar tudo para auto', + settings_btn_apply_aux_models: 'Aplicar alterações', + settings_aux_provider_auto: 'usar modelo principal', + settings_aux_model_auto: 'auto (usar padrão do provedor)', + settings_aux_model_custom: 'Modelo personalizado…', + settings_aux_model_custom_prompt: 'Insira o ID do modelo:', + settings_aux_loading: 'Carregando modelos auxiliares…', + settings_aux_load_failed: 'Não foi possível carregar as configurações de modelos auxiliares. Certifique-se de que a API do agente esteja disponível.', + settings_aux_reset_confirm_title: 'Restaurar modelos auxiliares?', + settings_aux_reset_confirm_msg: 'Isso definirá todas as tarefas auxiliares para auto (usar modelo principal).', + settings_aux_reset_done: 'Modelos auxiliares restaurados para auto', + settings_aux_save_failed: 'Falha ao salvar o modelo auxiliar', + settings_aux_saved: 'Modelos auxiliares atualizados', + settings_aux_no_changes: 'Nenhuma alteração para aplicar', settings_label_send_key: 'Tecla de Envio', settings_label_theme: 'Tema', settings_label_skin: 'Skin', @@ -11234,6 +11378,22 @@ const LOCALES = { settings_save_btn: '설정 저장', settings_label_model: '기본 모델', settings_desc_model: '새 대화에 사용됩니다. 기존 대화는 선택된 모델을 유지합니다.', + settings_label_auxiliary_models: '보조 모델', + settings_desc_auxiliary_models: '비전, 압축, 제목 생성 등 보조 작업 라우팅. "자동"은 기본 채팅 모델을 사용합니다.', + settings_btn_reset_aux_models: '모두 자동으로 재설정', + settings_btn_apply_aux_models: '변경 사항 적용', + settings_aux_provider_auto: '기본 모델 사용', + settings_aux_model_auto: '자동 (제공자 기본값 사용)', + settings_aux_model_custom: '사용자 정의 모델…', + settings_aux_model_custom_prompt: '모델 ID 입력:', + settings_aux_loading: '보조 모델 로딩 중…', + settings_aux_load_failed: '보조 모델 설정을 로드할 수 없습니다. 에이전트 API가 사용 가능한지 확인하세요.', + settings_aux_reset_confirm_title: '보조 모델을 재설정하시겠습니까?', + settings_aux_reset_confirm_msg: '모든 보조 작업이 자동(기본 모델 사용)으로 설정됩니다.', + settings_aux_reset_done: '보조 모델이 자동으로 재설정됨', + settings_aux_save_failed: '보조 모델 저장 실패', + settings_aux_saved: '보조 모델 업데이트됨', + settings_aux_no_changes: '적용할 변경 사항 없음', settings_label_send_key: '전송 키', settings_label_theme: '테마', settings_label_skin: '스킨', @@ -12385,6 +12545,22 @@ const LOCALES = { settings_save_btn: 'Enregistrer les paramètres', settings_label_model: 'Modèle par défaut', settings_desc_model: 'Utilisé pour les nouvelles conversations. Les conversations existantes conservent leur modèle sélectionné.', + settings_label_auxiliary_models: 'Modèles auxiliaires', + settings_desc_auxiliary_models: 'Routage des tâches secondaires : vision, compression, génération de titres, etc. « Auto » utilise le modèle de chat principal.', + settings_btn_reset_aux_models: 'Tout réinitialiser à auto', + settings_btn_apply_aux_models: 'Appliquer les modifications', + settings_aux_provider_auto: 'utiliser le modèle principal', + settings_aux_model_auto: 'auto (utiliser la valeur par défaut du fournisseur)', + settings_aux_model_custom: 'Modèle personnalisé…', + settings_aux_model_custom_prompt: 'Entrez l\u2019ID du modèle :', + settings_aux_loading: 'Chargement des modèles auxiliaires…', + settings_aux_load_failed: 'Impossible de charger les paramètres des modèles auxiliaires. Vérifiez que l\u2019API de l\u2019agent est disponible.', + settings_aux_reset_confirm_title: 'Réinitialiser les modèles auxiliaires ?', + settings_aux_reset_confirm_msg: 'Cela définira toutes les tâches auxiliaires sur auto (utiliser le modèle principal).', + settings_aux_reset_done: 'Modèles auxiliaires réinitialisés à auto', + settings_aux_save_failed: 'Échec de la sauvegarde du modèle auxiliaire', + settings_aux_saved: 'Modèles auxiliaires mis à jour', + settings_aux_no_changes: 'Aucune modification à appliquer', settings_label_send_key: 'Envoyer la clé', settings_label_theme: 'Thème', settings_label_skin: 'Peau', @@ -13637,7 +13813,23 @@ const LOCALES = { settings_tab_system: 'Sistem', settings_title: 'Ayarlar', settings_save_btn: 'Ayarları Kaydet', - settings_label_model: 'Varsayılan Model', + settings_label_model: 'Varsayılan Model', + settings_label_auxiliary_models: 'Yardımcı Modeller', + settings_desc_auxiliary_models: 'Görü, sıkıştırma, başlık oluşturma vb. yan görevler için yönlendirme. "Otomatik" ana sohbet modelinizi kullanır.', + settings_aux_loading: 'Yardımcı modeller yükleniyor…', + settings_aux_load_failed: 'Yardımcı model ayarları yüklenemedi. Agent API\'sinin kullanılabilir olduğundan emin olun.', + settings_aux_model_auto: 'otomatik (sağlayıcı varsayılanını kullan)', + settings_aux_provider_auto: 'ana modeli kullan', + settings_aux_model_custom: 'Özel model…', + settings_aux_model_custom_prompt: 'Model kimliğini girin:', + settings_aux_no_changes: 'Uygulanacak değişiklik yok', + settings_aux_saved: 'Yardımcı modeller güncellendi', + settings_aux_save_failed: 'Yardımcı model kaydedilemedi', + settings_aux_reset_confirm_title: 'Yardımcı modeller sıfırlansın mı?', + settings_aux_reset_confirm_msg: 'Bu, tüm yardımcı görevleri otomatik (ana modeli kullan) olarak ayarlayacaktır.', + settings_aux_reset_done: 'Yardımcı modeller otomatiğe sıfırlandı', + settings_btn_apply_aux_models: 'Değişiklikleri uygula', + settings_btn_reset_aux_models: 'Tümünü otomatiğe sıfırla', settings_desc_model: 'Yeni konuşmalar için kullanılır. Mevcut konuşmalar seçilen modellerini korur.', settings_label_send_key: 'Anahtar Gönder', settings_label_theme: 'Tema', diff --git a/static/index.html b/static/index.html index e5518943..222998e8 100644 --- a/static/index.html +++ b/static/index.html @@ -2,7 +2,7 @@ - + Hermes