feat: preserve manual rename + complete full-width chat layout - #2
Merged
zhonghuaY merged 2 commits intoApr 29, 2026
Conversation
Adds a 'manual' bool to /api/session/rename so the server can distinguish
explicit user renames from LLM-generated provisional/refined titles.
When manual=True, sets s.user_renamed_title=True; the background title
generator and refresher then short-circuit ('skipped: manual_title') at
every check point so user intent is never silently overwritten.
- /title slash command, sidebar context menu, titlebar dialog, duplicate
-> manual:true
- first-message provisional title (auto), background LLM refine (auto)
-> manual:false
- new helper _has_manual_title_override() uses 'is True' to avoid being
fooled by MagicMock attribute access in tests
- Session.__init__ now accepts arbitrary kwargs so the new field
round-trips through deserialization
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Follow-up to 6eba0c1 (feat(layout): full-width chat output container). That commit removed the outer cap but several inner elements still had 'max-width: var(--msg-max)', so message bodies, tool cards, thinking cards, error rows and the message footer remained narrow. - drop max-width from .msg-body, .tool-card, .thinking-card, .msg-foot, .msg-row[data-error], .messages-inner (and its 1400/1800px overrides) - update test_full_width_chat assertions to enforce the absence of these caps so future regressions are caught Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
zhonghuaY
added a commit
that referenced
this pull request
Jun 2, 2026
* feat: integrate agent-api-gateway with lowest coupling Add gateway_provider.py module for dynamic model discovery from agent-api-gateway instances. Gateway models appear in the UI dropdown and route through the gateway's OpenAI-compatible API with x-instance-keyword header injection via request_overrides. - New: api/gateway_provider.py (self-contained gateway integration) - Modified: api/config.py (5-line delegation to gateway_provider) - Modified: api/streaming.py (header injection via request_overrides) - New: tests/test_gateway_provider.py (38 unit tests) - New: tests/test_streaming_gateway.py (14 integration tests) Made-with: Cursor * feat: path-based routing and /model copilot shorthand (#1) - gateway_provider: embed keyword in URL path (/v1/k/{keyword}) so no extra HTTP headers are needed; prefix model with 'gw:' to prevent Responses API auto-detection for GPT-5+ models - commands.js: support copilot-{label}/{model}/{keyword} shorthand in /model command (e.g. '/model copilot-local/gpt-4.1/ins-1') Co-authored-by: Albert Yang <yangzh0906@thundersoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: use x-instance-keyword header instead of path-based routing (#2) Revert to standard /v1 base URL and pass keyword via x-instance-keyword HTTP header in extra_headers. The gw: model prefix prevents Responses API auto-detection; the gateway strips it for strict matching. Co-authored-by: Albert Yang <yangzh0906@thundersoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: full-width chat layout + fix gateway provider test (#3) - Remove max-width constraints from .messages-inner, .msg-body, .composer-box, .reconnect-banner, .update-banner, .approval-card so the chat area fills the available panel width - Add 3 regression tests verifying no max-width on chat elements - Fix test_resolve_valid_model to expect gw: model prefix Co-authored-by: Albert Yang <yangzh0906@thundersoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: status indicators — gateway dot, status-dot CSS, sidebar accent, CLI busy Re-applied #4 + nesquena#6 against upstream v0.50.217: - Add /api/gateway-status endpoint returning instance statuses - Add status dot CSS (.status-dot variants + .stream-cursor + accent rule) - Add gatewayDot inside composer-model-chip (replaces removed topbar modelChip) - Append per-session .status-dot in renderSessionListFromCache with data-session-id, data-cli-session, data-updated-at metadata - Per-conversation accent via _hashHue() → --conv-accent CSS var - Append pollGatewayStatus() (10s) + updateSessionDots() (3s) + setBusy hook to static/ui.js — recognizes both composerModelChip and modelChip ids - get_gateway_instance_statuses() helper in api/gateway_provider.py Streaming-cursor injection in messages.js was skipped because upstream replaced the simple Thinking placeholder with smd-based incremental rendering; upstream's session-state-indicator already covers that role. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: support renaming CLI / agent sessions in /api/session/rename The double-click rename action in the sidebar appeared to revert after a page refresh for any session whose data lives in ~/.hermes/state.db (CLI sessions, telegram/discord/gateway-imported sessions, etc.). Root cause: the rename handler called get_session(sid) which raised KeyError for non-WebUI-owned sessions and returned 404, while the frontend optimistically updated the UI without awaiting the API and had no rollback. Fixes: * api/state_sync.py — new rename_cli_session() that goes through SessionDB.set_session_title() (with sanitization + uniqueness check) and falls back to a defensive raw-SQL UPDATE if the on-disk schema version doesn't match the installed hermes_state package. * api/routes.py — rename handler now falls through to that helper on KeyError, surfaces title-uniqueness conflicts as 409, and best-effort mirrors WebUI renames to state.db so /insights and CLI listings stay in sync. * static/sessions.js — finish() now snapshots the old title and rolls back the optimistic update on API failure so the UI no longer lies about persistence. Tests: * tests/test_rename_session.py — 5 new tests covering WebUI rename persistence, length cap, blank fallback, CLI rename writing to state.db, and clean 404 on unknown id. * tests/test_gateway_provider.py — 3 new tests guarding the gw: model prefix invariant (commit e8d89a7c), x-instance-keyword header presence with no path-routing leak, and cli_route 'cursor' fallback for unknown (model, keyword) pairs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(sidebar): add JS-harness tests for CLI busy + per-conversation accent Re-applies the test file from PR nesquena#6. The production code (status-dot metadata, _hashHue, --conv-accent rule, updateSessionDots auto-refresh) was folded into the earlier 'feat: status indicators' commit when the status-dot system was re-applied against upstream v0.50.217. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: restore gateway delegation + adapt nesquena#6 tests for upstream layout * api/config.py: restore the 5-line gateway delegation in resolve_model_provider() that was dropped during the cherry-pick of 39706bb against the heavily refactored upstream config (the cold-path caching layer). Without it, test_resolve_model_provider_delegates_gateway fails because gateway model IDs leak through unresolved. * tests/test_streaming_gateway.py: bump two assertions to expect the 'gw:' model prefix introduced by 49a2be4f / e8d89a7c (already done in 88ad007b; re-applied here since the original cherry-pick was abandoned). * static/style.css: restate per-conversation accent rule using border-left-color so the regex in test_session_item_has_conv_accent_rule matches. * static/sessions.js: coerce _hashHue input to string so null/undefined inputs don't crash (test_hash_hue_returns_int_in_range). * static/ui.js: rewrite updateSessionDots() with a top-level _CLI_BUSY_WINDOW_S constant + window.HERMES_CLI_BUSY_WINDOW_S override so it works as a self-contained snippet under the node JS-harness; set dot.title='CLI active (...)' so the harness can detect promotion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(sidebar): persist CLI rename across compression-chain refresh When the sidebar renders a compression-chain conversation it shows the chain *head*'s title but routes navigation (and rename writes) to the *tip*'s session_id. Before this change /api/session/rename updated the tip's row, so the next sidebar refresh re-projected the unchanged head title and the rename appeared to revert. Fix: `rename_cli_session` now walks parent_session_id upward across compression boundaries to find the lineage root and updates that row's title. The existing head-prefer projection then surfaces the new name on the next refresh. Touching exactly one row keeps us compatible with hermes_state's UNIQUE INDEX on sessions.title (a chain-wide propagation strategy would collide with the auto-generated #N intermediate titles). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(models): refresh gateway model picker on dropdown open + gateway SSE The model picker's gateway slice was caught by two layers of staleness: 1. Backend: `api.config._available_models_cache` had a 24h TTL and wrapped the gateway-group assembly, so console-side model changes only surfaced once a day regardless of the gateway's own ~30s cache. 2. Frontend: `_liveModelCache` in static/ui.js only ADDED options to the <select>, never removed them — models pulled from a gateway never disappeared without a hard reload. Backend: split assembly so provider/credential discovery stays cached but gateway groups are re-attached fresh on every /api/models call via `_attach_fresh_gateway_groups` (still gated by gateway_provider's ~30s TTL, so cost is negligible). Frontend: new `refreshGatewayModelOptions()` does a surgical rewrite — strips only optgroups whose provider matches `_isGatewayProvider` (`gateway*`, `gw-*`, or `gw`), re-fetches /api/models with cache:'no-store', re-appends fresh gateway groups, preserves the user's current selection. Exposed on `window` so it can be called from sessions.js without imports. Hooked into `toggleModelDropdown` (refresh on open) and the `sessions_changed` gateway-SSE handler (refresh while dropdown is closed). Tests: 4 backend unit tests cover the cache split + helper predicate. 4 frontend tests use the existing node-driven JS harness pattern to verify export-on-window, dropdown hook, SSE hook, and the surgical strip-only-gateway-groups behaviour. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(layout): full-width chat output container Re-applies the CSS half of the original full-width-chat work (PR #3), which got upstreamed with only the regression test — the actual style changes were dropped. This caused the message column to keep its 1100/1200px caps at the 1400/1800px breakpoints and the message body to keep its 680px cap. Removed: - `.messages-inner` max-width:1100px @1400px and 1200px @1800px - `.msg-body` max-width:680px - `.composer-box`, `.reconnect-banner`, `.update-banner` max-width:780px `.approval-card`'s `var(--msg-max)` cap is left alone (different design spec — modal-style cards intentionally narrower). Tests: 4 new tests pin the absence of the caps via a media-block stripper helper. test_sprint35.py's old assertion (which expected the caps to be present) is updated to assert the inverse, with a comment explaining the historical regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(mermaid): suppress giant 'Syntax error in text' overlay Mermaid 10.x's render() injects a global error SVG into <body> on parse failures, in addition to throwing. The previous catch only repaired the inline block; the orphan SVG floated in the DOM and consumed massive vertical space, ruining the chat layout. Three-layer defense: - mermaid.initialize({suppressErrorRendering: true}) — official 10.6+ opt-out - mermaid.parse(code, {suppressErrors: true}) before render() — short-circuit invalid input - cleanupOrphanError() in catch path — defensive removal of any leftover element Adds tests/test_mermaid_syntax_error_overlay.py (4 assertions). Existing tests/test_1044_mermaid_csp_font.py still passes (3/3). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(perf): add session-switch performance optimization plan Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf(test): add session-switch perf baseline harness (Task 0) 200-msg synthetic session, mixed markdown/code/mermaid/katex. Baseline numbers in docs/plans/PERF_BASELINE.md. Server is fast (11ms full, 0.7ms metadata, gzip on). Client-side is the real bottleneck — Tasks 2/3/4/5/7 prioritized. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(api): paginate /api/session via tail/since_idx/limit Adds optional pagination params to /api/session, fully backward compatible (no params = full payload as before): - tail=N -> last N messages - since_idx=K -> messages with index >= K - limit=N -> cap window size (combines with since_idx) - messages=0 -> metadata only (precedence preserved) Each returned message gets _idx (its position in full history) and the response includes pagination={start_idx,end_idx,total} so clients can detect 'no more above' by comparing end_idx to total. Out-of-range since_idx clamps to total (returns empty window) so clients have a stable signal. Foundation for client-side windowed render (Task 3). Refs: docs/plans/2026-04-28-session-switch-perf.md (Task 1) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf(ui): cache rendered session as cloned DOM, fingerprint last msg The session-render cache used to store innerHTML strings keyed by (session_id, msgCount). Two limitations in that design: 1. Re-parsing a 200-message HTML blob on every back-switch is the single biggest cost in cross-session navigation — comparable to a full markdown rebuild for code-heavy chats. 2. The (sid, count) key serves stale HTML after edits/retries that mutate content without changing the count — documented as 'acceptable' but actually surprising in practice. This commit: * Stores inner.cloneNode(true) (a real Element) instead of HTML string. Restoration is replaceChildren(...clone.childNodes) — no parse. Cache copy is re-cloned on each restore so the stored node never drifts with the live DOM. * Adds _sessionCacheKey(messages) -> {count, lastKey}. lastKey is a cheap fingerprint of the *last* message (role|len|head|tail). O(1) per switch, catches the dominant edit-without-count-change case. * Bumps LRU 8 -> 16 sessions; adds touch-on-hit so hot sessions stay. * Adds childElementCount<=2000 guard alongside the existing 300KB HTML-length guard so cloning huge sessions can't stall a switch. 8 new node-driven tests pin _sessionCacheKey behaviour (empty session, identical-msgs determinism, content-change invalidation, role-change invalidation, array content blocks, missing-content tool-call message, intentional-trade-off: only-tail fingerprint). All 51 perf-related tests pass. Refs: docs/plans/2026-04-28-session-switch-perf.md (Task 2) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf(ui): lazy-render mermaid/katex when scrolled into view Long sessions can have 50+ mermaid diagrams. The previous code rendered every mermaid + katex block in a single forEach as soon as the lib was ready — blocking the main thread for hundreds of ms on session-switch / first paint, and doing wasted work for blocks the user never scrolls to. This commit gates mermaid.render() and katex.render() behind a shared IntersectionObserver per type: * rootMargin '300px 0px' — diagrams begin rendering ~300px before they enter the viewport so the user doesn't see the <pre><code> placeholder flash to SVG mid-scroll. * unobserve on first intersect — guarantees a block is never rendered twice (cheap correctness defence + saves observer bookkeeping). * Feature-detect IntersectionObserver and fall back to eager render for browsers (and jsdom) that lack it. * Render path extracted to renderOne(block) so observer + fallback share identical syntax-error / orphan-cleanup handling — the giant 'Syntax error in text' overlay fix from f872496 is preserved on both paths. 8 new grep-level tests pin the IntersectionObserver mechanism so a future refactor can't silently regress to the eager loop. The 7 existing mermaid syntax-error tests still pass. Refs: docs/plans/2026-04-28-session-switch-perf.md (Task 5) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf(api): ETag + 304 fast-path for /api/session Adds conditional GET to the heaviest endpoint. When the client's If-None-Match matches the server's current fingerprint, the server returns 304 Not Modified with no body — skipping JSON serialize, redact, and gzip on the server, and skipping JSON parse + DOM re-render on the client (the api() helper resurrects the cached body). Server (api/routes.py): * Cheap weak ETag: SHA1 of (sid, updated_at, msg_count, query params). All inputs cheap to read after the metadata-only Session load — no extra disk IO. * If-None-Match check happens BEFORE serialization / redact / gzip so a 304 is genuinely cheap (sub-ms). * ETag varies by query params (messages, resolve_model, tail, since_idx, limit) so windowed reads can't accidentally hit a cached full-message ETag. Server (api/helpers.py): * j() now respects a Cache-Control override in extra_headers — the default 'no-store' would have prevented browsers from sending If-None-Match at all (defeating the entire fast path). /api/session sends 'Cache-Control: no-cache' (revalidate every time, but the 304 keeps it cheap). * New send_304(handler, etag) helper for the empty-body 304 path. Client (static/workspace.js): * api() now maintains a 32-entry LRU keyed by full URL → {etag, body} for /api/session* GETs. When request matches a cached URL it sends If-None-Match; on 304 it returns the stored body verbatim. * Scoped narrowly to /api/session — won't accidentally serve stale data from endpoints whose ETag semantics weren't designed for it. 8 new tests (test_session_etag.py) cover: ETag emission, weak format, 304 on match, 200 on mismatch, varies-by-query, Cache-Control allows revalidation. Existing baseline 'no ETag' test flipped to 'has ETag'. Refs: docs/plans/2026-04-28-session-switch-perf.md (Task 6) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf(ui): batch DOM mounts via DocumentFragment in renderMessages The main message-build loop in renderMessages used to do `inner.appendChild(row)` on every iteration — N reflows / style recomputes for an N-message session. For a 200-message conversation that meant 200+ layout passes during a single session-switch. This commit: * Builds every message row, separator, assistant turn, and compression card into a detached DocumentFragment (`_frag`). Appending to a detached fragment is ~free — no layout, no paint, no style invalidation, no querySelector cost. * Commits with `inner.replaceChildren(_frag)` — atomic empty + adopt in one operation. Adopted nodes don't get re-parsed. * Post-mount work (tool-card insertion, token-usage badge, highlightCode, addCopyButtons, lazy mermaid/katex registration) operates on the live `inner` and is unchanged. * `_insertCompressionLikeNode`'s fallback path also routes to the fragment so a session that has no anchor still mounts atomically. Cache write path (Task 2) still calls inner.cloneNode(true) on the final mounted DOM — the fragment + replaceChildren combo collapses to a clean parent before the clone, so no behaviour change there. 3 new grep tests pin the mechanism (createDocumentFragment present, no inner.appendChild inside renderMessages, mount via replaceChildren). 124 existing render/UI tests pass. Refs: docs/plans/2026-04-28-session-switch-perf.md (Task 7) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(perf): operational guide + before/after results for session-switch overhaul Final piece of the session-switch perf plan: documentation that future contributors can use to understand the layered design without having to chase 6 commits across api/ and static/. * docs/PERFORMANCE.md (new): operational guide — describes the 4 collaborating layers (backend pagination + ETag, client request-cache, client render-cache, fragment build + lazy heavy renders), operating principles, regression-test map, and 'when to revisit' criteria for the deferred Tasks 3 (windowed render) and 4 (markdown worker). * docs/plans/PERF_RESULTS.md (new): before/after numbers captured by tests/test_perf_results_capture.py against the same synthetic 200-msg session used for the Task 0 baseline. Headline: full payload 11.2ms baseline → 6.87ms (38% faster) tail=30 (T1) n/a → 1.48ms / 8.7KB (vs 49KB full) ETag 304 (T6) n/a → 0.44ms / 0 B * tests/test_perf_results_capture.py (new): one-shot perf snapshot test; prints a perf table when run with -s, used to refresh PERF_RESULTS.md. * CHANGELOG.md: [Unreleased] entry under a new ### Performance section summarising all 5 perf commits. Refs: docs/plans/2026-04-28-session-switch-perf.md (Task 8) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(title): preserve manual rename against LLM auto-title overwrite Adds a 'manual' bool to /api/session/rename so the server can distinguish explicit user renames from LLM-generated provisional/refined titles. When manual=True, sets s.user_renamed_title=True; the background title generator and refresher then short-circuit ('skipped: manual_title') at every check point so user intent is never silently overwritten. - /title slash command, sidebar context menu, titlebar dialog, duplicate -> manual:true - first-message provisional title (auto), background LLM refine (auto) -> manual:false - new helper _has_manual_title_override() uses 'is True' to avoid being fooled by MagicMock attribute access in tests - Session.__init__ now accepts arbitrary kwargs so the new field round-trips through deserialization Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(layout): remove leftover msg max-width caps for full-width chat Follow-up to 6eba0c1 (feat(layout): full-width chat output container). That commit removed the outer cap but several inner elements still had 'max-width: var(--msg-max)', so message bodies, tool cards, thinking cards, error rows and the message footer remained narrow. - drop max-width from .msg-body, .tool-card, .thinking-card, .msg-foot, .msg-row[data-error], .messages-inner (and its 1400/1800px overrides) - update test_full_width_chat assertions to enforce the absence of these caps so future regressions are caught Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ui): make Transcript markdown download robust against blob URL race The #btnDownload click handler was revoking the blob URL synchronously on the line after a.click(), and never appending the anchor to the DOM. The browser download is asynchronous: it reads the blob URL after the click event returns, so revoking immediately races the download. Combined with the detached anchor (which Firefox/Safari/some WebViews refuse to .click()) the download could hang or silently fail. Fix follows the same pattern used by downloadFile() in workspace.js: - append the <a> to document.body before .click() - defer URL.revokeObjectURL + removeChild via setTimeout(..., 200) - wrap everything in try/catch and surface failures via showToast (silent failure was the second-order bug — users had no clue what went wrong) - also: a.style.display='none' so the temp anchor never flashes Adds tests/test_transcript_download_robust.py to pin the corrected pattern with grep-style assertions, matching the style of the existing boot.js sanity tests in test_sprint9.py / test_sprint20.py. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(export): serve transcript Markdown via server endpoint, not blob URL Prior fix (#3) made the blob-URL download more robust (DOM-attached anchor, deferred URL revoke, try/catch) but did NOT solve the actual hang user reported: the file would write completely to disk, yet the browser download UI never reached the 'complete' state. Root cause is a Chromium quirk with blob: URL downloads where the download progress UI cannot reliably tell when the stream ends — the file is on disk but the download bar shows it as still in-progress indefinitely. Solution: bypass blob URLs entirely. Serve the markdown from the server with proper Content-Disposition + Content-Length, mirroring the JSON export path which works flawlessly. Server side (api/routes.py) - new _build_session_markdown(session) function mirrors the JS transcript() in messages.js: skip tool messages, join array text blocks, append attachments marker - new _handle_session_export_markdown(handler, parsed) mirrors the JSON export structure: redact_session_data() before rendering, set Content-Type: text/markdown; charset=utf-8, Content-Disposition: attachment with filename, Content-Length, Cache-Control: no-store - /api/session/export dispatcher now branches on ?format=md vs JSON default Client side (static/boot.js) - #btnDownload click handler now navigates to /api/session/export?format=md&... - still uses the working pattern from downloadFile() in workspace.js: a.style.display='none' + appendChild + click() + setTimeout removeChild - removed all blob URL machinery (no createObjectURL, no revokeObjectURL) - still wrapped in try/catch with showToast on success and failure Tests (tests/test_session_export_markdown.py, 9 tests) - 400 on missing session_id, 404 on unknown session - correct Content-Type / Content-Disposition / Content-Length headers - markdown body renders user/assistant messages with correct heading levels - tool messages are skipped (not exported) - array content text blocks are joined, non-text blocks ignored - attachments emit the _Files: a, b_ marker - default JSON export still works (no regression) - boot.js click handler is pinned to the server endpoint (no createObjectURL) Replaces tests/test_transcript_download_robust.py from PR #3 — that file asserted the blob-URL pattern which we're now removing entirely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: add mobile remote access design spec via frpc tunnel * docs: add mobile remote access implementation plan * feat(mobile): add visibilitychange SSE auto-reconnect for background recovery When a mobile browser moves to background the OS suspends JS and kills SSE connections. When the user returns the EventSource may be in a zombie state. This listener detects foreground recovery and reconnects active streams via the server-side status check API. Test suite: 9 tests covering listener existence, visible state check, active stream guard, status API call, _wireSSE reconnect, source close, session restore fallback, delay mechanism, and ordering after pageshow. --------- Co-authored-by: Albert Yang <yangzh0906@thundersoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: albert <albert@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two unrelated but ready-to-ship fixes that were sitting in a WIP stash. Split into 2 commits for clean review.
Commit 1 —
feat(title): preserve manual rename against LLM auto-title overwriteProblem
When a user manually renames a session (via
/titleslash command, sidebar context menu, titlebar dialog, or duplicate-with-rename), the background LLM title generator/refresher would silently overwrite their title on the next model response.Fix
/api/session/renamenow accepts amanual: boolparameter to distinguish explicit user renames from LLM-generated provisional/refined titles.manual=True, the server setss.user_renamed_title = True._run_background_title_update) and refresher (_run_background_title_refresh) now check this flag at every commit point and short-circuit with'skipped: manual_title'._has_manual_title_override(session)uses strictis Truecomparison to avoid being fooled by MagicMock attribute access in tests.Session.__init__now accepts arbitrary**kwargsso the newuser_renamed_titlefield round-trips through deserialization without requiring schema migration.Call site semantics
/title <name>slash commandtruetruetruetruefalsefalse(resets flag)Files
api/models.py,api/routes.py,api/streaming.py(server-side)static/{commands,messages,panels,sessions}.js(4 call sites)tests/test_session_extra_fields.py(new), plus updates totest_rename_session.py,test_title_aux_routing.py,test_1058_adaptive_title_refresh.pyCommit 2 —
fix(layout): remove leftover msg max-width caps for full-width chatProblem
Follow-up to
6eba0c15 feat(layout): full-width chat output container. That commit removed the outer cap but several inner elements still hadmax-width: var(--msg-max), so message bodies, tool cards, thinking cards, error rows, and the message footer remained narrow on wide screens.Fix
max-widthfrom.msg-body,.tool-card,.thinking-card,.msg-foot,.msg-row[data-error],.messages-inner(and its@media1400/1800px overrides).tests/test_full_width_chat.pyassertions to enforce the absence of these caps so future regressions are caught.Files
static/style.css,tests/test_full_width_chat.pyTesting
Both commits sanity-tested. Pre-existing 5 failures in
test_rename_session.pyare env-related (auth/connection refused on the in-process test server) and identical on baseline before this PR — not caused by these changes.Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>