Commit Graph
4425 Commits
Author SHA1 Message Date
Harendra Pratap Singh 4e5703fc62 fix(streaming): inspect run_conversation signature before passing persist_user_timestamp (#6935) 2026-08-12 17:55:52 +05:30
nesquena-hermesandhappy5318 fb5a40f132 docs(changelog): stamp #6938 + add loadSession compression-cleanup regression test
Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
2026-08-12 06:52:45 +00:00
MHJ e4c3c728d9 fix(commands): wait for bundle metadata before skill suggestions 2026-08-12 00:32:57 +09:00
MHJ eea4f03db8 fix(commands): search skills by keyword in slash autocomplete 2026-08-11 23:40:00 +09:00
Frank Song 7c5add1903 fix: settle extension lifecycle before notifying 2026-08-11 17:05:06 +08:00
Frank Song c45691141e Add extension turn lifecycle events 2026-08-11 16:38:52 +08:00
nesquena-hermes 5564b98b56 Merge remote-tracking branch 'origin/master' into franksong2702/e0-extension-identity 2026-08-11 03:20:23 +00:00
nesquena-hermes f642dc6820 Merge remote-tracking branch 'origin/master' into release/stage-6410-1786414679
# Conflicts:
#	CHANGELOG.md
2026-08-11 03:04:27 +00:00
nesquena-hermes 892e73f7d6 Merge remote-tracking branch 'origin/master' into release/stage-6410-1786414679 2026-08-11 02:57:31 +00:00
nesquena-hermes b77d4a2fb0 Merge remote-tracking branch 'origin/master' into release/stage-6757-1786414089 2026-08-11 02:57:05 +00:00
nesquena-hermes 90426570f2 test(kanban): drop synthetic 480x320 case; keep scroll-reachable off-viewport fix
The kanban board layout test's 480x320 short-landscape case was environment-
marginal: multiple assertions (inline field-vs-modal geometry AND the shared
off-viewport check) each differ by a few px between the CI runner's font render
and local Chromium, producing a CI-only flake that varied by shard + locale.
480x320 landscape is a viewport essentially no real device uses.

Drop that one synthetic case (keep 1280x800, 768x800, 400x800, 1024x600 x
en/ru/de). The scroll-reachable off-viewport tolerance in _layout_helpers.py +
its 2 control tests are retained as a genuine correctness improvement.
2026-08-11 02:51:25 +00:00
nesquena-hermes 903ace3f49 test(layout): don't flag scroll-reachable fields as off-viewport
The shared assert_layout_sane degenerate check flagged an interactive element
as 'off-viewport' when it sat below the fold at rest but was reachable by
scrolling an overflow-y:auto ancestor (tall dialog on a short viewport). This
produced a CI-environment-sensitive false positive on the Kanban board settings
modal at 480x320 German (the field is inside the .kanban-modal-overlay which is
overflow-y:auto), passing locally but failing on the CI runner's font render.

Add reachableByScrollableAncestor(): an off-viewport edge absorbed by a
scrollable (overflow auto/scroll, content-overflowing) ancestor is reachable and
not a defect. Genuinely-unreachable elements (no scroll escape, e.g. inside
overflow:hidden) are still flagged. +2 unit tests covering both directions.
2026-08-11 02:40:48 +00:00
nesquena-hermesandrodboev 8b873c55eb Release exp-v0.52.198: Refine selected transcript text in composer (#6410, @rodboev)
Ships PR #6410 with a maintainer UX fix folded in (authorship preserved).

Adds a 'Refine' action to the message text-selection toolbar: select text in a
chat message -> Refine seeds it into the composer as an editable quoted draft
(marker-free, one-way, existing draft + attachments preserved, 44x44 mobile
icon buttons, all 15 locales). ChatGPT/Notion-style quote-to-edit flow.

Maintainer fix (Fable UX gate): French instruction string now uses the
space-before-colon convention (Instruction d'affinage :).

Gate: Codex SAFE TO SHIP (marker-free, no XSS, one-way Range, Reply/drafts
preserved), Fable SHIP-WITH-UX-FIXES (fix applied), tests 19/19, ESLint clean.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-08-11 02:19:06 +00:00
nesquena-hermesandfranksong2702 cf7eb48e56 Release exp-v0.52.197: preserve api_content for agent replay (#6757, @franksong2702)
Ships PR #6757 with a maintainer hardening pass folded in (authorship preserved).

Preserves the exact provider-facing reply text (api_content) through the internal
session store for byte-accurate agent replay, while guaranteeing it never reaches
a browser: every client-facing surface strips it via the public projection.

Maintainer fixes (2 defects my adversarial re-gate reproduced):
- Selective session redaction restored: only transcript fields are scrubbed;
  operational fields (e.g. workspace path) pass through unchanged, so a path
  containing credential-shaped text is no longer masked into an invalid path.
- Runner-backed SSE relay now projects payloads before emit, closing an
  api_content leak through _stream_runner_run_events.
+5 regression tests. Codex adversarial re-gate: SAFE TO SHIP. Full suite clean.

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-08-11 02:10:21 +00:00
nesquena-hermes 2fdc919dcc Merge branch 'pr6037stage' into release/stage-6037-1786396633 2026-08-10 21:17:15 +00:00
nesquena-hermes 66277fbeb8 fix(kanban): short-landscape overlay scroll + robust mobile-responsive CSS test 2026-08-10 20:45:21 +00:00
nesquena-hermes f50a80beb6 Merge branch 'pr6055stage' into release/stage-6055b-1786393156 2026-08-10 20:19:18 +00:00
nesquena-hermes 6f7fa623cb Merge branch 'pr-6091-head' into release/stage-6091-1786388870 2026-08-10 19:07:52 +00:00
cdc50e232a fix(streaming): route image input mode by the session's actual provider/model (#6882)
* fix(streaming): route image input mode by the session's actual provider/model

Image routing (native embed vs vision_analyze text pipeline) previously
resolved the provider/model from the global config default only. A session
switched to a text-only model (e.g. deepseek-v4-flash) still had uploaded
images forwarded natively to a model that cannot see them.

Thread the session's resolved provider/model (active_provider/active_model)
and its pre-canonicalization identity (requested_provider) through
_resolve_image_input_mode, _build_native_multimodal_message,
_sanitize_messages_for_api, and both gateway chat paths. When no session
identity is present the behaviour is unchanged (global default).

Tests: upstream parity, session-level native/text routing, requested_provider
capability lookup, and multimodal/sanitize forwarding.

* test: use generic provider/model names in fixtures and docstrings

Replace real provider/model names (custom:newapi, jd-kimi-k2.6,
stepfunplan, step-3.7-flash, deepseek-v4-flash) with neutral placeholders
(mygateway, my-vision-model, my-text-model, my-vision) so the test suite
does not reference any particular deployment's configuration.

test_active_vision_model_routes_native now passes requested_provider to
keep the assertion independent of models.dev capability data.

* fix(streaming): preserve session provider identity across auth-heal retry

Both credential-healing retry branches unconditionally overwrote
_session_requested_provider with the already-canonicalized resolved_provider,
so a named custom:slug session lost its exact vision-capability entry after an
auth-heal retry -- a text-only custom model would then wrongly RESTORE the
historical image_url parts it correctly stripped on the first attempt.

Make the heal-path assignment conditional (initialize only when empty),
preserving the pre-canonicalization identity captured at first resolve.

Regression test asserts both arms: identity preserved -> images stripped;
identity lost -> images kept (the bug).

Addresses Codex gate CORE finding + greptile P1 on #6882.

Co-authored-by: happy5318 <happy5318@users.noreply.github.com>

* test(vision-routing): skip capability-based tests when hermes-agent absent

The capability-derived 'text' verdict delegates to agent.image_routing
(the single source of truth), which is NOT installed in the WebUI standalone
CI environment -- there _resolve_image_input_mode falls back to native, so the
7 tests asserting a capability-based text verdict failed on CI (they only ever
passed locally where the agent package is on PYTHONPATH).

Guard them with a module-level agent-availability probe + skipif (the same
pytest.importorskip pattern used across the suite for agent-dependent tests):
they run fully where the agent is present and skip honestly where it is not.
Agent-free tests (native carve-out, explicit-signal fallback) still run on CI.

* test(vision-routing): drop unused imports (base64, os, TemporaryDirectory)

Pre-existing F401 unused-imports that ruff's diff-vs-master flagged once the
import block entered the changed-line scope. Path is retained (used in the
_make_png / _normalized_attachments type annotations).

---------

Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-09 09:08:52 -07:00
fa087c53d1 fix(cron): mark running cron sessions as active in sidebar (#6734)
* fix(cron): mark running cron sessions as active in sidebar

* fix(cron): include cron_running in effective-streaming predicate + test updates (#6734)

* fix(cron): longest-prefix-first cron job matching in session list cache (#6728)

* fix(cron): require exact run-timestamp suffix in cron_running prefix match

Gate re-review (Codex) found the longest-prefix-first match could misattribute:
cron_job_prefixes is built ONLY from RUNNING jobs, so when the true longer owner
(backup_full) is NOT running, a running shorter job (backup) swallowed a completed
cron_backup_full_YYYYMMDD_HHMMSS session (its prefix cron_backup_ still matched),
deferring that row's completion/unread transition. Now the text after a matched
cron_{jid}_ prefix must be EXACTLY a run timestamp (\d{8}_\d{6}), so a shorter job
id can't claim a longer job id's session regardless of which jobs are running.
Regression test added (fails without the guard).

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>

---------

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-09 02:40:05 -07:00
04d5652699 fix(title): honor auxiliary.title_generation.enabled=false in WebUI title paths (#6814) (#6877)
* fix(title): honor auxiliary.title_generation.enabled=false in WebUI title paths (#6814)

Hermes Agent gates title generation on auxiliary.title_generation.enabled
(default true), but the WebUI orchestration only used the aux config to pick
a model route, so enabled=false still triggered LLM title calls via the
active chat model or the aux route, and could persist a fallback title.

Add a profile-scoped _aux_title_generation_enabled() helper beside
_get_aux_title_config() with the same default/truthy semantics as the Agent
(boolean and string forms). Gate three automatic paths on it:

- _run_background_title_update(): skip with status skipped /
  title_generation_disabled, provisional title untouched, no fall-through
  to the active agent model.
- _run_background_title_refresh(): skip with refresh_skipped /
  title_generation_disabled.
- generate_session_title_for_session(): explicit (None,
  'title_generation_disabled', '') so manual regenerate returns an
  explicit disabled response and imported-session auto-titling stays quiet.

Checks run inside the profile-scoped worker context so a background job
cannot read another profile after a switch.

Tests (tests/test_issue6814_title_generation_enabled_gate.py, 20 cases):
default-on when omitted, disabled with/without aux route (no LLM call,
title untouched, skipped status, stream_end still emitted), string/boolean
false forms, adaptive refresh path, manual regenerate explicit response,
helper parsing matrix. fail-before: 19 failed on unpatched code.
Regression: title family 154 passed (incl. test_2235 routing cases).

* docs: document auxiliary.title_generation.enabled behavior (#6814)

Addresses the Greptile #6877 review note: document the newly enforced
setting and the manual-regenerate disabled response in
docs/advanced-chat-setup.md.

* fix(title): mirror Hermes agent's canonical is_truthy_value contract exactly

Gate review (Codex) found the enabled-parser diverged from the agent's canonical
contract: it used a 'disable on false/0/no/off' blocklist, but the agent uses
is_truthy_value(value, default=True) — an allowlist (utils.TRUTHY_STRINGS =
{1,true,yes,on}). So None wrongly routed through the string branch and empty/
unrecognized strings wrongly stayed ENABLED. Now: None->True (default), bool
unchanged, str truthy ONLY for {1,true,yes,on}, else bool(value) — byte-for-byte
with agent/title_generator._auto_title_enabled. Test matrix corrected + expanded.

Co-authored-by: X-Shihai <X-Shihai@users.noreply.github.com>

---------

Co-authored-by: X-Shihai <48356232+xushihai@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: X-Shihai <X-Shihai@users.noreply.github.com>
2026-08-09 07:35:47 +00:00
a662e37f89 fix(settings): honor persisted auto-follow across boot failures and partial bodies (#6856)
* fix(settings): honor persisted auto-follow across boot failures and partial bodies (#6819)

The Auto-follow new content toggle was silently re-enabled by two paths:
- boot.js settings-fetch-failure fallback hardcoded _autoScrollFollow=true,
  clobbering an explicit OFF for the whole session (post-turn scroll yanks
  until refresh);
- _applySavedSettingsUi assigned body.auto_scroll_follow!==false
  unconditionally, so a partial settings body without the key evaluated
  undefined!==false -> true.

Fix: mirror the resolved value into profile-namespaced localStorage
(JSON map keyed by active profile), make the boot fallback read the mirror
instead of hardcoding ON, and only override the runtime flag when a settings
body actually owns the key. Fresh users (no mirror) still default to ON.

Tests: update test_issue4006 fallback assertion; add
test_issue6819_auto_scroll_follow_persistence covering source-shape guards
and extracted-helper behavior (fresh default, persisted OFF, profile
isolation, partial-body no-clobber, save-path persist).

* fix(settings): resolve auto-follow mirror profile from cookie before S.activeProfile init (#6856 review)

P1 (greptile-apps review): the profile-namespaced mirror was keyed by
S.activeProfile, but the boot settings-fetch-failure fallback runs before
boot resolves S.activeProfile, so a non-default profile with a failed fetch
read the 'default' mirror entry. Resolve the key from the hermes_profile
cookie (set per-client by /api/profile/switch, readable synchronously at
page load) first, falling back to S.activeProfile, then 'default'.

P2: document the persistence contract in the helper block (storage shape,
profile-key resolution order, read semantics, upgrade path).

Tests: extend the extracted-helper node test with the P1 scenario (cookie
authoritative over stale S.activeProfile, per-profile isolation via cookie,
no-cookie fallback) and add source-shape guards for the cookie read.

* fix(settings): defer auto-follow mirror read until profile resolves (P1 follow-up)

greptile-apps review: the hermes_profile cookie is HttpOnly — document.cookie
can never read it, so the cookie-based profile key from 39fed70b always fell
back to 'default', re-introducing the wrong-namespace bug for non-default
profiles on a failed settings fetch.

Correct model: the active profile is only known asynchronously via
/api/profile/active. The boot settings-fetch-failure path now sets a safe
default (ON) plus a deferred-reapply flag; once S.activeProfile resolves,
the mirror is read with the profile-correct namespace and the flag clears.
The success path (which already applied+persisted the authoritative value)
never touches the flag.

Tests updated to the deferred model: source-shape guard asserts the re-apply
runs AFTER profile resolution (fallback < profile-resolve < reapply ordering),
no document.cookie read in the profile key, and the node behavioral test
simulates boot-failure -> profile-resolve -> deferred re-apply.

* fix(settings): defer auto-follow mirror write until profile resolves (P1 round 3)

greptile-apps review: the SUCCESS boot path called _persistAutoScrollFollow
before S.activeProfile initialized, so a non-default profile's successful
settings fetch stored its value in the 'default' namespace. A later
failed-fetch boot would then find no entry for the real profile and restore
auto-follow ON.

The success path now defers the mirror write the same way the failure path
defers its read: it records the value in _autoScrollFollowDeferredPersist,
and the post-profile-resolution block persists it under the correct
namespace (then clears the flag). Both deferred branches now run together
after S.activeProfile resolves.

Tests: success path must not call _persistAutoScrollFollow before profile
resolution (source-shape), deferred-persist anchor ordered after profile
resolve, and the node behavioral test covers success-boot -> profile-resolve
-> deferred write under the resolved profile (and NOT under default).

* fix(settings): make auto-follow mirror global to match the backend contract (maintainer review)

Maintainer review on #6856: the mirror was profile-keyed, but the backend
authority is GLOBAL — SETTINGS_FILE = STATE_DIR / 'settings.json'
(api/config.py), and /api/settings calls load_settings() with no
profile-specific file. A profile-keyed map leaves a freshly-opened profile
with no mirror entry, so a failed settings fetch on that profile wrongly
restored ON despite the global OFF — the original failure stayed
reproducible.

Revert to a single global last-known value ('1'/'0' under one key):
- _persistAutoScrollFollow / _readPersistedAutoScrollFollow now use a
  plain scalar, not a JSON profile map.
- Boot success path persists synchronously (no deferred write — the global
  mirror has no profile dimension).
- Boot failure fallback reads synchronously (no deferred re-apply — no
  profile resolution needed).
- Remove _autoScrollFollowProfileKey, _autoScrollFollowDeferredPersist,
  _autoScrollFollowDeferredReapply entirely.

Tests: behavioral node test now asserts the maintainer-requested regression
(global OFF survives profile change + failed fetch, single scalar storage);
source-shape guards assert the profile map and deferred machinery are gone;
boot success persists; partial-body and save-path assertions unchanged.
14/14 pass.

* fix(settings): never persist synthesized auto-follow from a failed autosave (Greptile P1)

Greptile review on #6856: in _autosaveAppearanceSettings, when the POST
fails (saved is falsy), '!saved' synthesizes _autoScrollFollow=true, and
the added persist call wrote that synthesized ON into the mirror. A later
boot settings-fetch failure would then restore ON from the corrupted
mirror, reintroducing post-turn scroll yanks for a user who explicitly
disabled auto-follow.

Persist ONLY from an explicit boolean in the server response:
  if(saved && typeof saved.auto_scroll_follow==='boolean' && typeof _persistAutoScrollFollow==='function'){
    _persistAutoScrollFollow(saved.auto_scroll_follow);
  }
A failed save or a partial response without the key leaves the mirror
untouched (last authoritative value preserved).

Tests: source-shape guard asserts the explicit-boolean requirement; the
node behavioral test replicates the failed-autosave path (mirror stays
OFF) and the successful-autosave path (explicit boolean persists).

* fix(settings): sync auto-follow mirror on successful settings-panel GET

Gate review (Codex) found a stale-mirror gap in #6856's own contract: a
successful loadSettingsPanel() GET applied the authoritative auto_scroll_follow
value to window._autoScrollFollow but did not persist the global mirror, so an
explicit OFF applied via the panel left a stale ON mirror that a later
boot-fetch failure would restore (reopening #6819). Persist from the panel GET
too, only when the server sent an explicit boolean (never a synthesized
default) — matching the boot + autosave contract. Adds a bite-tested regression.

Co-authored-by: sand01chi <sand01chi@users.noreply.github.com>

---------

Co-authored-by: sand01chi <sand01chi@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-09 03:36:49 +00:00
Vincent Dukeandnesquena-hermes 64c2c12df7 feat(sidebar): add show_kanban_sessions toggle to hide kanban worker sessions (#6780)
* feat(sidebar): add show_kanban_sessions toggle to hide kanban worker sessions

* fix(sidebar): kanban source-filter override + filtered-view cap (cron/webhook parity)

Gate re-review (Codex) found the kanban-hide addition missed cron/webhook
parity for the source-filter case:
- An explicit kanban source_filter now overrides the default hide (threaded
  source_filter into _dedupe_cli_sidebar_sessions_for_api, normalized with
  strip().lower() to match the loader), so a deliberate kanban view shows
  those rows even with the toggle off. Scoped to the matching source only.
- A dedicated KANBAN_PROJECT_CHIP_LIMIT bounds a filtered kanban-only view,
  mirroring CRON/WEBHOOK_PROJECT_CHIP_LIMIT.

Deliberately NOT changing the general exclude_sources: the original PR lets
kanban load on the source_filter=None path so the toggle-ON case works (the
hide is applied by _hide_from_default_sidebar only when the toggle is off).
Excluding kanban there would save the 20-row budget but break toggle-on
unless a bounded kanban merge is added (cron/webhook don't do that either) —
that query-budget optimization is deferred as a separate cross-cutting change.
The pinned-survives-hide finding is also out of scope (cron/webhook hide pinned
identically, confirmed by Codex). Adds bite-tested regressions.

Co-authored-by: vduke <vduke@users.noreply.github.com>

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-09 02:53:25 +00:00
webtecnicaandwebtecnica 064e95a38a fix(streaming): reject obsolete cancel finalizer after successor completes (#6636)
Prevents a delayed cancelled streaming worker from writing back stale output over a completed successor's result, and clears the Gateway writeback-owner entry on teardown/pre-start-cancel to avoid a slow SESSION_WRITEBACK_OWNERS leak.

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
2026-08-09 01:10:49 +00:00
cabb467b36 fix(stream): stop fade-text effect from re-fading the whole message on tool-call rewinds (#6783)
* fix(stream): stop fade-text effect from re-fading whole message on tool-call rewinds

When the fade-text effect is enabled, Hermes long tasks visibly blink the
entire message on every tool call. Root cause: tool-call XML
(<function_calls>) appears inline in the assistant text stream, and the
unclosed-tag stripping in _stripXmlToolCalls temporarily rewinds
displayText to a strict prefix. That rewind zeroed the fade playout
(_streamFadeNextText) and tripped _smdWrite's self-heal, which cleared
assistantBody.innerHTML and re-created every word as a fresh is-new
span — replaying the fade on all visible text at once.

Fix: on a detected rewind, shrink the playout cursor to the common
prefix instead of resetting to zero, and mute the fade renderer for
that common prefix during any rebuild (both the main smd path and the
anchor prose incremental node) so only the post-rewind tail animates.

Adds regression tests: playout rewind keeps common prefix visible,
rebuild mutes the common prefix, and _smdWrite's self-heal sets the
silent-prefix window.

* fix(#6783): compute rewind mute prefix in rendered-text space

Review feedback (maintainer round on 56c9307f): the silent-prefix budget
was computed in SOURCE-text space (charCodeAt over markdown source), but
the fade add_text hook only receives RENDERED prose. Markdown delimiters,
link destinations and MEDIA token bytes never reach add_text, so the
source-space budget over-muted the first genuinely new word after a
rewind (e.g. '**alpha** beta' -> '**alpha** gamma' muted gamma).

Fix: snapshot the OLD rendered text before the rebuild (assistantBody
textContent / anchor .msg-body textContent), then after parser_write
compare it with the NEW rendered text and strip is-new only from spans
inside the rendered common prefix (_streamFadeMuteRenderedPrefix).

- Rendered-space boundary, scoped to the owning node (no stream-global
  counter that can survive a rebuild without a fade consumer)
- No state left behind on reduced-motion / safe-renderer / empty-text /
  parser-finalization paths: _rewindPrevRendered is a local variable
  consumed right after the rebuild
- Tests: plain-word mute, markdown/MEDIA byte regression (the blocker),
  per-root scoping (main vs anchor), updated _smdWrite self-heal test

Gate: node -c OK; test_smooth_text_fade + test_smd_media_in_stream +
test_svg_audio_video_rendering + test_insights -> 83 passed, 18 subtests.

* fix(#6783): drop f-prefix from _fade_fake_node JS block (F541)

CI lint failure on a6d37b22: tests/test_smooth_text_fade.py:723 F541
f-string without any placeholders. The f-prefix was only used to escape
JS braces ({{ }}) — a plain string with literal braces is equivalent
and passes the ruff E9+F+B gate.

---------

Co-authored-by: silent-reader-cn <silent-reader-cn@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-08 23:37:54 +00:00
e7da197d4f fix(branch): preserve displayed conclusion in session forks (#6735)
* fix(branch): slice fork transcript in GET /api/session merged-view coordinates

keep_count sent by forkFromMessage is an index into the merged display
view (sidecar lineage stitched across compression snapshots, append-only
merged with state.db rows, parent backfill). The branch handler sliced the
raw sidecar instead, which diverges from that view in both length and
content after compactions: forks from the final conclusion landed rows
too early, ending the fork mid tool-run and dropping the conclusion.

Build source_messages through the exact same pipeline as the
GET /api/session load_messages path for non-messaging sessions.

Regression test: test_branch_route_slices_merged_display_view_not_raw_sidecar
(fails on the old code with the fork ending on a tool row).

* fix(branch): align fork source with display bounds

---------

Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-08 22:57:09 +00:00
starship-sandnesquena-hermes 1480718767 feat(typography): establish three-font contract (#5918)
* feat(typography): establish three-font contract

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: OpenCode:gpt-5.3-codex-spark
Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.3-codex-spark

* fix(terminal): refit after live font changes

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: OpenCode:gpt-5.3-codex-spark
Assisted-by: Claude Code:claude-opus-4-8

* docs(typography): preserve global serif guardrail

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Claude Code:claude-opus-4-8

* fix(typography): enforce font tokens in rendered editors

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.3-codex-spark

* fix(terminal): resync after stylesheet font changes

Observe live extension stylesheet changes and completed stylesheet loads so open terminals refresh font metrics and backend dimensions.

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.3-codex-spark
Assisted-by: Greptile:undisclosed

* test(typography): remove dynamic harness execution

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.3-codex-spark
Assisted-by: nesquena-hermes:undisclosed

* test(typography): skip missing Playwright browser

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-luna
Assisted-by: nesquena-hermes:undisclosed

* fix(terminal): refit after web fonts load

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-luna
Assisted-by: Greptile:undisclosed

---------

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-08 22:40:21 +00:00
49437e20ab fix: phantom 'Compressing context' barrier when compressed SSE is lost/delayed (#6572)
* fix: clear stale compression barrier on turn-done when compressed SSE is lost

When a turn completes (done SSE event) but the "compressed" SSE event was
lost or delayed, the compression UI state remained in phase="running".
The done handler unconditionally rebound the stale state sessionId to
the continuation session, surfacing a phantom "Compressing context"
barrier on a session that never actually compressed.

Fix: when the done event session_id differs from the active session AND
the compression UI is still in "running" phase, clear it via
clearCompressionUi() instead of rebinding. Non-running states (e.g.
"done") are still rebound as before.

Added regression tests verifying:
- stale running state is cleared (not rebound) on session change
- non-running states are still rebound
- the old unconditional rebind is gone

* fix: also clear phantom compression on same-session done (A->A case)

Reviewer feedback on PR #6572: the previous fix only cleared stale running
compression when the session ID changed (A->B). But the backend also supports
compression without session rotation (A->A), where the compressed SSE being
lost leaves the same phantom barrier.

Fix: in the done handler, clear any owned automatic compression state that is
still in "running" phase, regardless of whether the session ID changed. The
condition is now phase==='running' alone, not session_id change AND running.

Added behavioral state-transition tests for all four cases:
- running A->B (rotation, compressed lost): cleared
- running A->A (no rotation, compressed lost): cleared
- non-running A->B (rotation, compressed seen): rebound
- non-running A->A (no rotation, compressed seen): rebound

* test: real SSE regression for compression phantom barrier

Rewrite test_compression_phantom_barrier.py to use real browser + SSE
instead of handwritten Python simulation. The new tests:

- Boot real WebUI server with isolated state
- Use deterministic mock gateway (Runs API mode)
- Verify done handler clears stale running compression state
- Cover both A→B (rotation) and A→A (no rotation) cases

Key insight: /v1/capabilities must return run_approval_response=true
to enable the Runs API path in gateway_chat.py.

Fixes reviewer feedback: simulation never called attachLiveStream,
clearCompressionUi, or registered done listeners.

* test: replace Playwright test with hermetic source-grep suite for compression phantom barrier

The previous Playwright-based test failed CI in no-egress sandboxes due to
external resource errors (net::ERR_INTERNET_DISCONNECTED) and was structurally
unsound: the mock window.S shadowed the lexical S, so the compressing handler
short-circuited before the barrier could be established, producing a false-
positive assertion.

New suite uses static source analysis (same convention as test_issue840,
test_issue3040, test_bg_task_complete_focus_drop):
- No browser, no network, no playwright dependency
- Proves the done handler clears phase:'running' without gating on session ID
  change (A->A case coverage)
- Asserts gating conditions: automatic && owner && done session exists
- Negative controls: non-owner / manual compression are ignored
- Source-level regression proof: reverting the production hunk breaks tests

13 tests. All pass in <3s on bare Python.

* test: exercise compression cleanup through real SSE handlers

Replace source-grep assertions with a hermetic Playwright harness that loads the shipping classic scripts and dispatches MockEventSource events through attachLiveStream. Cover A-to-A/A-to-B cleanup, non-running rebind, ownership controls, canonical cleanup side effects, and a production-branch mutation proof.

---------

Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-08 22:10:04 +00:00
webtecnicaandnesquena-hermes b284371bf2 fix(notifications): new toast on same-session turns (#6673) (#6681)
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-08 22:00:18 +00:00
Rod Boevandrodboev 2260a393f0 fix(#6612): isolate update_channel writes from generic Preferences autosave (#6666)
Removes update_channel from the generic preferences autosave payload and gives the channel selector a dedicated sequenced write path with FIFO settings-POST serialization and revert-on-failure, so a stale tab cannot silently flip the release channel.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-08-08 20:36:47 +00:00
Rod Boev 0f15416cb9 test(#5932): stabilize narrow modal scroll proof 2026-08-08 15:54:47 -04:00
Rod Boev 94aaac1e32 fix(#5932): contain narrow board modal 2026-08-08 15:45:09 -04:00
Rod Boev ea3163e234 fix(#5932): harden board workspace modal proof 2026-08-08 15:44:56 -04:00
Rod Boev eb9298ba91 fix(#5932): omit unchanged board workdir on settings edits 2026-08-08 15:44:55 -04:00
Rod Boev 2b0614ce35 feat(#5932): add per-board default workdir 2026-08-08 15:44:55 -04:00
allenliang2022andallenliang2022 bfff32baf5 perf(session): reduce active recovery snapshot payloads (#6858)
Project active run-journal recovery snapshots into a non-mutating compact transport form before returning them from /api/session, removing duplicate representations while preserving the live message timestamp. Thanks @allenliang2022.

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-08-08 19:11:09 +00:00
7f27942393 Release exp-v0.52.180: don't drop async delegation completions while origin session busy (#6632 + #6662) (#6852)
* fix: preserve async completion delivery attempts while busy

* fix(async): keep legacy busy completions retryable

* fix(async): extend keep_legacy_retrying to resolved-target transient-failure exits; keep formatting failures bounded (combined #6632+#6662 gate findings)

* Release exp-v0.52.180: don't drop async delegation completions delivered while origin session busy (#6632, #6662)

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
2026-08-08 00:14:21 -07:00
21a8160338 Release exp-v0.52.179: gateway watcher index-covered fingerprint + failure-state preservation (#6821) (#6850)
* perf: keep gateway watcher fingerprint index-covered

* [verified] fix: preserve watcher state on projection failure

* Release exp-v0.52.179: keep gateway watcher fingerprint index-covered + preserve state on projection failure (#6821)

---------

Co-authored-by: StefanPieter <stefan@noble-pro.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 23:18:33 -07:00
c1fc3f2c79 Release exp-v0.52.178: honor configured appearance defaults for no-settings-file users (#6834) (#6849)
* fix(settings): _SETTINGS_DEFAULTS theme/skin never reach a user with no settings file

`_read_raw_settings_file()` returns `{}` for a MISSING settings.json. `{}` is a
dict, so `load_settings()` always took its `isinstance(stored, dict)` arms:

    settings["theme"], settings["skin"] = _normalize_appearance(
        stored.get("theme") if isinstance(stored, dict) else settings.get("theme"),
        stored.get("skin") if isinstance(stored, dict) else settings.get("skin"),
    )

`stored.get("theme")` is then None, `_normalize_appearance(None, None)` falls
through to its unknown-theme branch, and the result is ("dark", "default").
`_SETTINGS_DEFAULTS["theme"]` and `["skin"]` are therefore unreachable for the
one case they exist to serve: a user who has no settings file yet.

This is invisible on the shipped defaults, because dark/default is exactly what
the fallback produces — the two paths agree, so nothing ever caught it. It only
surfaces once the defaults are changed, at which point the dict silently does
nothing: a deployment (or fork, or a future change to the shipped values) sets an
appearance and every new user still gets dark.

The fix falls back to the defaults instead of to None, gated on the PAIR of
keys. Per-field (`or settings.get(...)`) looks equivalent and is not: with a
stored legacy theme and no stored skin, `slate` normalises to ("dark", "slate"),
but per-field fallback injects the default skin and returns ("dark", "default"),
silently dropping the legacy migration. That is the same absent-vs-explicit
distinction the boot script draws in #6808 — this is its server-side half. #6808
stops the browser fabricating a choice; without this, the server still answers
with the fallback rather than the configured default.

Behaviour on stock defaults is provably unchanged: `_normalize_appearance(None,
None)` and `_normalize_appearance("dark", "default")` both return
("dark", "default"). A stored preference is untouched in every case.

13 tests. Both directions are pinned, and both were checked by injection:
reverting api/config.py fails exactly the two "defaults are honoured" cases and
nothing else; substituting the per-field variant fails 8, including four of the
five legacy migrations. Ran the full settings/appearance surface — 61 test files,
873 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Release exp-v0.52.178: honor configured appearance defaults for users with no settings file (#6834)

---------

Co-authored-by: Hermes Agent <hermes@aip.de>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 23:06:57 -07:00
80e3574eca Release exp-v0.52.177: scope aux-title test module stubs (#6661) (#6848)
* test(#6630): isolate auxiliary client fixtures

* test(#6630): route both aux-title suites through one scoped module stub

* test(#6630): drop dead import and record the patch.dict restore constraint

* Release exp-v0.52.177: scope auxiliary-title test module stubs, stop cross-file sys.modules leak (#6661)

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 22:47:51 -07:00
5e5a41e79f Release exp-v0.52.176: start profile list-cache TTL after row build (#6846) (#6847)
* fix(profiles): start list cache TTL after row build

Slow row builds currently consume the cache TTL before the result is stored.
Timestamp the cache at completion so every successful build receives the full TTL.

Refs #5364

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-luna

* Release exp-v0.52.176: start profile list-cache TTL after row build (#6846)

---------

Co-authored-by: starship-s <45587122+starship-s@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 22:40:22 -07:00
nesquena-hermesandnesquena-hermes fcef6ba1af Release exp-v0.52.175: identical-prompt retry settlement regression (#6631) (#6845)
* test: pin identical-prompt retry settlement

* Release exp-v0.52.175: two-direction regression for identical-prompt retry settlement (#6631)

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 20:05:49 -07:00
e09ccbaac9 Release exp-v0.52.174: regression coverage for empty tool_calls drop on mirror path (#6803) (#6844)
* test(streaming): cover empty tool_calls drop in _api_safe_message_positions (#6796)

* Release exp-v0.52.174: regression coverage for empty tool_calls drop on mirror path (#6803)

---------

Co-authored-by: Webtecnica Bot <bot@webtecnica.com.br>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 19:53:02 -07:00
898a01b766 Release exp-v0.52.173: don't mutate system prompt on workspace switch (#6683) (#6841)
* fix(cache): don't mutate system prompt on workspace switch (#6672)

* test(#6672): pin created_workspace freeze + compact round-trip + legacy fallback (closes coverage gap flagged in review)

* Release exp-v0.52.173: don't mutate system prompt on workspace switch (#6683)

---------

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 19:42:15 -07:00
a818be1ed4 Release exp-v0.52.172: preserve reader scroll intent during live renders (#6453) (#6838)
* fix: preserve reader scroll intent during live renders

* fix: enforce programmatic scroll guard freshness

* test: update programmatic scroll guard assertion

* test: update scroll safety valve assertion

* fix: reconcile scroll intent helper after base refresh

* fix: timestamp fallback history scroll writes

* test: cover stale older-message fallback scroll guard

* test: execute the real slow-render history fallback

* test: provide the history fallback DOM helper

* test: retain async declaration in history fallback harness

* test: remove trailing blank line from scroll intent regression

* test(#6414): update scroll-intent harness for window._autoScrollFollow + _messageScrollInputGeneration (master drift)

* Release exp-v0.52.172: preserve reader scroll intent during live renders (#6453)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Frank Song <xuefu.song@qq.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 18:37:31 -07:00
37547a7bd1 Release exp-v0.52.171: keep settled Worklog in place across stream settlement (#6454) (#6837)
* test: prove worklog continuity across stream settle

* fix: keep settled worklog in place on stream done

* fix: preserve settle follow ordering

* test: wire settled worklog frame proof

* fix: preserve settled worklog detail disclosure

* fix: preserve default-open settled worklogs

* fix: preserve blank-owner worklogs

* fix: keep render fallback self-contained

* ci: retrigger hosted checks

* style(worklog): grow collapsed Worklog rail tap target to 44px on touch devices (#6454 follow-up)

* Release exp-v0.52.171: keep settled Worklog in place across stream settlement (#6454)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 18:06:57 -07:00
dss539 41df2a7f83 docs(test): clarify qualified model routing boundary 2026-08-07 23:09:32 +00:00
dss539 707fef9f4f fix(session): strip @provider qualifier only at the gateway boundary
The previous revision stripped the '@provider:' qualifier from model_value
in _session_model_state_from_request BEFORE _resolve_compatible_session_model_state
ran. That defeated two behaviors that rely on the qualified form:

  1. provider-switch repair - a session pinned to a removed/unconfigured
     provider (@removed:mistral-large) no longer reverted to the active
     default; it returned (bare_model, removed) and targeted an unavailable
     provider.
  2. PR #6718's custom-provider normalizer, which needs the still-qualified
     value to map @custom:<slug>:<model> to the canonical slug.

Keep the qualifier through session-state resolution (matching master) and
strip it to the bare model ONLY at the gateway request boundary via
_gateway_model_field(), which already runs on both gateway request sites.

Red-before-green: the rewritten test asserts @removed:mistral-large repairs
to the active default; it failed on the prior head and passes now.
The gateway-body assertions (TestGatewayRequestBodiesCarryBareModel) still
prove only the outbound payload is bare.

Verified: 275 tests pass across the issue-6722, provider-mismatch,
model-resolution-fast-path, gateway-approval (legacy+runs), gateway-routing,
custom-provider, session-model-switch and model-not-found suites.
2026-08-07 23:09:32 +00:00
dss539 b296fa2ff7 test: cover @provider:model resolution for multi-segment providers
Pins the provider/model pair for every qualified-model shape at all
three layers that reach the gateway: the routes splitter, the provider
normalizer, the request-state builder, and both gateway request bodies
(legacy chat completions + runs API).

The table-driven cases include the shapes a positional colon split gets
wrong -- @custom:backup:model-a, @custom:backup:model-a:free,
@custom:<host>:<port>:model and @ollama-cloud:model:tag -- and assert
the routes splitter agrees with config._parse_provider_qualified_model_id()
exactly, so re-introducing a local split heuristic fails loudly.

The gateway assertions read the JSON actually written to the request
body rather than mocking the helper under test.

All 12 fail on master before the fix and pass after it.
2026-08-07 23:09:32 +00:00
nesquena-hermes 434c61a376 lint: drop unused pytest import in test_custom_provider_dict_models (F401) 2026-08-07 22:41:05 +00:00