Commit graph

481 commits

Author SHA1 Message Date
nesquena-hermes
d195b094bb
Release: GLM per-version reasoning controls (#6219, @rh-id) (#6243)
* fix(reasoning): gate reasoning_effort ladder to GLM-5.2+ on native zai

Z.AI's API (docs.z.ai) defines reasoning_effort as GLM-5.2+ exclusive, but
hemes-webui advertised the full 6-level ladder for all 7 GLM models because
_candidate_supports_reasoning has an unconditional glm token match and
_filter_reasoning_efforts_for_provider had no ZAI branch. Six of seven catalog
models (glm-5.1, glm-5, glm-5-turbo, glm-4.7, glm-4.5, glm-4.5-flash) showed a
selector whose values the endpoint silently ignores, and GLM-4.7 (forced thinking
that cannot be disabled per Z.AI docs) showed a 'none' option with no effect.

Add a ZAI branch to _filter_reasoning_efforts_for_provider mirroring the existing
OpenAI/Gemini/Anthropic ceiling pattern: strip the whole ladder for pre-5.2 GLM
models and for the forced-thinking GLM-4.7 family; preserve the full ladder for
GLM-5.2+ (whose accepted values match VALID_REASONING_EFFORTS exactly). The gate
is scoped to the native zai provider only (aliases glm/z-ai/z.ai/zhipu all
resolve to zai); aggregator providers are untouched because they route through
their own routers, not Z.AI's native endpoint.

The glm family-detection heuristic in _candidate_supports_reasoning is unchanged
— GLM models DO support the thinking on/off toggle at the family level; this fix
is specifically about the reasoning_effort intensity ladder.

State layer: agent.reasoning_effort config + UI dropdown options derived from
resolve_model_reasoning_efforts. Invariant: UI options and coercion now agree
and match Z.AI's per-model docs (max offered only for GLM-5.2+, none never
offered for forced-thinking models). Out of scope: the thinking:{type:...}
request-field translation lives in the external agent/gateway layer.

* fix(reasoning): close ZAI coercion gap + harden test assertions

Address Greptile review on #6219 (round 1):

1. Vacuous test assertion (test_glm_5_2_preserves_none_sentinel): the 'or'
   fallback made the assertion always-true, so a regression stripping 'none'
   for GLM-5.2 would go undetected. Rewrote to inject 'none' via the raw source
   (mocking _resolve_model_reasoning_efforts_impl) and assert it survives — the
   test now genuinely exercises the preservation branch.

2. Coercion gap for non-max stored levels on pre-5.2 GLM: the existing
   'if ceiling and raw not in ceiling' guard treats an empty ceiling as 'no
   rule' (preserving the configured effort verbatim per #3505), so a stored
   'high'/'medium'/'low' for glm-5.1/glm-4.5/glm-4.7 on native zai was forwarded
   to Z.AI unchanged and silently ignored — contradicting the PR's stated
   UI/coercion agreement invariant.

   Root cause: the ZAI gate returns [] to mean 'known-empty' (no
   reasoning_effort at all), but the coercion path treated all [] as
   'ambiguous/unknown, preserve verbatim'. Fixed by extracting the ZAI decision
   into _zai_glm_reasoning_efforts_supported (True/False/None sentinel) shared by
   both the filter and coercion, then special-casing the known-False result in
   coerce_reasoning_effort_for_model to return '' (send no field). The #3505
   preserve-verbatim behavior for genuinely-unknown models on non-zai providers
   is unchanged.

Added 10 regression tests (all fail before the coercion fix, pass after):
- All 6 levels (max..minimal) coerce to '' for each pre-5.2 GLM + glm-4.7
- All 4 aliases (glm/z-ai/z.ai/zhipu) resolve through the same coercion gate
- GLM-5.2 preserves all 6 levels verbatim
- Regression guard: unknown model on custom: provider STILL preserves verbatim

State layer: agent.reasoning_effort config + the value forwarded to Z.AI.
Invariant now fully holds: UI offers no options for pre-5.2 GLM AND coercion
sends no reasoning_effort field for any stored level on those models.

* fix(reasoning): preserve ZAI GLM 4.5-5.1 thinking toggle when effort ladder empty

Address nesquena-hermes round-2 review on #6219: returning [] for the effort
ladder on sub-5.2 GLM models hid the entire reasoning chip in the composer
(static/ui.js:4932 treats empty supported_efforts as 'no reasoning control at
all'), silently regressing the working thinking on/off toggle for GLM-4.5/4.6/
5.0/5.1 users. Per Z.AI's own docs, those models accept the thinking
{type:enabled|disabled} toggle even though they do not accept the
reasoning_effort intensity ladder.

Fix decouples thinking-toggle capability from the effort ladder:

Backend (api/config.py):
- Refactor _zai_glm_classification returns one of 'effort' (GLM-5.2+), 'thinking'
  (GLM-4.5 up to but not including 5.2), 'forced' (GLM-4.7 family), or None
  (non-zai / non-GLM). Single source of truth shared by all three consumers.
- _zai_glm_reasoning_efforts_supported now wraps classification for the coercion
  contract (unchanged behavior).
- _zai_glm_thinking_toggle_supported returns True for 'effort' or 'thinking',
  False for 'forced', None otherwise.
- get_reasoning_status gains a supports_thinking_toggle field = bool(supported)
  OR (zai_thinking is True). Non-zai providers default to bool(supported_efforts)
  so their chip-visibility behavior is unchanged.

Frontend (static/ui.js):
- New _currentReasoningToggleSupported state var (default undefined = treat as
  true so legacy responses without the field do not newly hide the chip).
- _applyReasoningChip shows the chip when hasEffortLadder OR toggleSupported.
  Empty efforts + toggle=True keeps the chip visible with just the None/On
  control (the existing _applyReasoningOptions already shows 'none' when the
  ladder is empty). Empty efforts + toggle=False (GLM-4.7 forced) hides it.
- Profile-transition and fetch-failure resets now pass
  supports_thinking_toggle:false alongside the empty efforts so the chip hides
  during the unknown-state window, matching the prior reset contract.

Tests (30 new):
- _zai_glm_classification parametrized across all three tiers + aliases + defer
- get_reasoning_status supports_thinking_toggle per tier (GLM-5.2 both, GLM-4.6
  toggle-only, GLM-4.7 neither, non-zai defaults to effort capability)
- Frontend _applyReasoningChip behavior via node driver: empty efforts +
  toggle=True stays visible, toggle=False hides, effort ladder alone is
  sufficient, absent field keeps prior behavior

State layer: agent.reasoning_effort config + supports_thinking_toggle field in
/api/reasoning + composer chip visibility. Invariant: the chip is hidden ONLY
when the model supports neither the effort ladder nor the thinking toggle
(GLM-4.7 forced, or genuinely non-reasoning models); GLM-4.5-5.1 retain the
working On/None control they had before the round-1 effort gate.

* fix(reasoning): make ZAI thinking toggle two-way + force GLM-4.7 stored none

Address nesquena-hermes round-3 review on #6219 — two SILENT gaps in the
thinking-toggle path, plus a click-handler sibling I found while auditing.

Gap #1 — ONE-WAY toggle for GLM-4.5/4.6/5.0/5.1 (api/config.py:4247,
static/ui.js:4902, static/index.html:757):
The round-2 fix kept the chip visible for thinking-tier models but the only
rendered dropdown option was 'None' (the HTML had no Default option, and
set_reasoning_effort rejected empty effort with 400). So a GLM-4.6 user could
turn thinking OFF but never back ON — worse than the original bug.

Fix:
- set_reasoning_effort now accepts empty effort as 'clear the override' (removes
  agent.reasoning_effort so the provider default takes effect). Invalid values
  still raise ValueError.
- static/index.html gains a <div data-effort=''>Default</div> option.
- _applyReasoningOptions always shows both Default ('') and None alongside the
  effort ladder, so a thinking-tier model (empty ladder + toggle=true) renders
  an operable Default+None two-state control.
- Click handler (ui.js:5106) checks option presence (if(opt)) not truthiness
  (if(effort)) — the old check silently ignored data-effort='' clicks, which
  would have left the Default button dead even after the HTML/backend changes.

Gap #2 — GLM-4.7 not forced when 'none' stored (api/config.py:3984, :3857):
When GLM-4.7 had agent.reasoning_effort=none configured, coercion preserved
'none' via the early return at line 3985, so streaming built disabled reasoning
for a model that forces thinking on regardless. Separately, when the raw
capability source listed 'none', resolve_model_reasoning_efforts reattached it
to GLM-4.7's supported_efforts (['none']), leaking an 'off' option to the UI
for a forced-thinking model.

Fix:
- coerce_reasoning_effort_for_model checks _zai_glm_classification == 'forced'
  BEFORE the generic 'none' early-return, coercing stored 'none' to '' (default
  = thinking on) for forced models.
- resolve_model_reasoning_efforts returns [] early for forced-tier models,
  skipping the 'none' reattachment entirely.

Tests (17 new):
- Gap #2: coerce('none', glm-4.7) -> '', regression guard that non-forced GLM
  still accepts 'none'; resolve does not reattach 'none' for forced but DOES
  for thinking-tier; end-to-end get_reasoning_status for forced+stored-none.
- Gap #1 backend: set('') clears the key (no raise), set('garbage') still
  raises, all 7 valid levels still save.
- Gap #1 frontend: three new node-driver tests asserting the dropdown exposes
  both Default and None for thinking-tier (two-state), Default+None+ladder for
  effort-tier, and the off->on->off round trip keeps both visible throughout.
- Updated test_reasoning_show_hide.test_set_reasoning_effort_rejects_invalid to
  reflect the new contract (empty accepted, garbage rejected).

Regression gate: gap#2 coerce tests fail without the forced-tier check (3
failures); gap#1 backend test fails with 'ValueError: effort is required'
without the empty-acceptance change. 219 passed, 1 pre-existing skip across
the full reasoning + chip + config-cache surface.

State layer: agent.reasoning_effort config + supports_thinking_toggle field +
composer dropdown options. Invariant: the thinking toggle is now genuinely
two-way for GLM-4.5-5.1 (Default=on, None=off, both always visible) and
GLM-4.7 forced-thinking never offers 'none' in any path (coerce, resolve,
status, or UI).

* CHANGELOG: GLM per-version reasoning controls (#6219)

---------

Co-authored-by: Ruby Hartono <58564005+rh-id@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-18 05:44:33 -07:00
ai-ag2026
7707ea1d2a fix(webui): stop password managers autofilling the search box, and keep unsaved sessions startable
Two independent defects that chain into "new conversations cannot be started":
every POST /api/session/draft and /api/chat/start 404s a few seconds after
POST /api/session/new returned 200.

1. Search inputs are parsed as username fields.

index.html contains no <form> at all, but does contain password inputs (the
Settings "Access Password" panel). Chromium therefore groups the document's
unowned fields into one synthetic password form and picks the first text input
in the DOM — #sessionSearch, the sidebar conversation filter — as that form's
username field, then autofills the saved account name into it on load.
autocomplete="off" was already present; it is ignored for credential
heuristics.

The autofill fires oninput -> filterSessions() -> GET /api/sessions/search
?content=1&depth=5: a full content search the user never asked for, repeated
on every page load.

Fix: give the four search inputs type="search" plus the per-manager opt-outs
(data-1p-ignore / data-lpignore / data-bwignore / data-form-type), and give the
Settings password inputs a real <form> owner with proper autocomplete tokens so
Chromium scopes the credential form to them instead of to the whole document.
The form uses display:contents, so layout is unchanged. CSS suppresses WebKit's
native search clear button, since these fields ship their own
(#sessionSearchClear).

2. A never-persisted session is treated as evictable.

new_session() intentionally keeps a session in RAM until its first message
(#1171), so the SESSIONS cache is its only copy. _session_is_evictable()
(#4765) short-circuited on zero messages, reasoning that an empty shell "is
recreated on next access". It is not: get_session() has no recreate path and
raises KeyError, so both routes 404 and the session can never be started.

The content search from (1) pulls every hit through get_session(), which blows
past sessions_cache_max (default 300) and evicts the session being composed.
That is why this reproduces within seconds on an install with ~1700 sessions,
and why it grew worse as the session count rose.

Fix: require proof of persistence before evicting, for empty sessions too —
which is what the function's own docstring already promised ("Its full state is
already persisted to the JSON sidecar"). The zero-message branch was the single
path that bypassed it. #4765's memory bound is unaffected: sessions loaded from
disk remain evictable, and its existing tests still pass.

Trade-off: an unsaved empty session now stays resident for the process
lifetime (one per "New Conversation" click, an empty object each). That is a
deliberate application of the function's stated invariant — "slightly more RAM
for a session we are unsure about is strictly better than evicting an unsaved
session". Bounding it by age instead would also work if maintainers prefer.

Validation:
  pytest tests/test_issue4765_sessions_lru_eviction.py  ->  9 passed
  The added regression test fails on the unpatched predicate (verified by
  reverting api/models.py alone) and passes with the fix.

Note: (1) fixes what triggers the churn, (2) fixes what turns that churn into
an unstartable session. Either fix alone leaves a real defect in place, so
they are submitted together.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 10:06:41 +02:00
nesquena-hermes
6361d920ff
Release exp-v0.52.49: dictation preserves composer text (#5895) (#5978)
* fix(dictation): preserve composer text when server STT commits

Dictation via the server STT path (MediaRecorder → POST /api/transcribe)
replaced any text already in the composer with the recognized transcript.
Browser SpeechRecognition worked correctly; only the async path was broken.

Root cause: the MediaRecorder.onstop handler calls _setRecording(false)
before awaiting _transcribeBlob's fetch. _setRecording(false) clears the
module-level _prefix. By the time the fetch resolves and _commitTranscript
runs, _prefix is '' — so the existing append logic collapses to
'just the transcript' and overwrites the textarea.

Browser SR dodged this because sr.onend builds the merged string into a
local variable before invoking _setRecording(false). Server STT is async
and could not use that pattern without help.

Fix:
- _commitTranscript accepts an optional prefixOverride parameter.
- _transcribeBlob snapshots _prefix into a local before the await.
- Pass the snapshot through to _commitTranscript.

Add a Settings → Voice → 'Append dictation to composer' toggle (default
ON) so users can opt into replace mode if they prefer the old buggy
behavior. The bug fix changes the default to append, matching what the
browser SR path always did.

i18n: new keys dictation_append_label / dictation_append_desc added to
all 14 locale blocks in static/i18n.js (English fallback where no native
translation exists).

Files: static/boot.js (+43/-7), static/index.html (+7), static/i18n.js (+30).

* fix(dictation): snapshot _prefix in onstop, not inside _transcribeBlob

First attempt at the append-mode fix took the snapshot inside
_transcribeBlob via 'const prefixSnapshot = _prefix'. By the time
_transcribeBlob runs (awaited from recorder.onstop), the sync block
above has already called _setRecording(false), which clears _prefix
inside _setRecording's !on branch. So the snapshot was always '' and
the append path fell through to the replace branch.

Move the snapshot into recorder.onstop, before _setRecording(false)
runs. Pass it as a parameter into _transcribeBlob so the call site
doesn't need to re-read the (now-cleared) module-level _prefix.

No behavior change for the browser-SR path — sr.onend still builds
the merged string into a local variable before invoking
_setRecording(false), and was already correct.

* fix(dictation): trim comments to fit test_590_recording_stops_before_transcribe

test_bugbatch_apr2026.py::test_590_recording_stops_before_transcribe slices
700 chars from 'recorder.onstop' and asserts that _setRecording(false)
comes before _sendRawAudio(/_transcribeBlob( inside that window. With
the verbose 6-line comment block from the previous fix, the actual
call sites landed at offsets 834/890 — past the 700-char window — so
the test's .find() returned -1 and the assertion failed mechanically.

Also: the verbose comment in _transcribeBlob contained the substring
'recorder.onstop', which made the test's first .find() land in the
comment instead of the actual handler, returning a wrong onstop_start.

Trim both comment blocks to single lines that fit within the test's
window and don't contain the literal 'recorder.onstop' substring.
Functional behavior unchanged from the previous fix; this is purely
a comment-shape adjustment to satisfy the brittle test.

* fix(tests): anchor test_590 on handler definition + widen window per review

The reviewer's fix-spec for test_590_recording_stops_before_transcribe
was more thorough than my single-shot comment trim:

  (1) Reword the _transcribeBlob comment so it doesn't contain the
      literal 'recorder.onstop' substring.
  (2) Make the test robust to the handler growing: anchor on
      'recorder.onstop=async' instead of 'recorder.onstop', and
      widen the fixed 700-char window to 1200.

Applied both. With these changes the test passes regardless of how
much comment text gets added in or around the handler in the future.
Also restored the verbose explanatory comments in both locations
(the previous a7f84027 had trimmed them to fit the brittle 700-char
window — that workaround is no longer needed).

Backward-compat check: the new test logic also passes against
origin/master (verified locally), so this change doesn't regress
the existing green-master invariant.

* fix(dictation): preserve user's mid-transcription edits in async server STT

Greptile flagged this as a 4/5 concern on the append-mode branch:
the async server-STT path appended the dictated text to the stale
snapshot (`_prefix` captured at recording start), which clobbered
any keystrokes the user made during the ~1s transcription round-trip.

Before this fix, with append mode ON:
  User typed before mic:    'Hello, '
  User typed during STT:    'Hello, from K'  (typed while waiting)
  Dictated text:            'world'
  Result:                   'Hello, world'   (lost 'from K')

After this fix:
  User typed before mic:    'Hello, '
  User typed during STT:    'Hello, from K'
  Dictated text:            'world'
  Result:                   'Hello, from K world'  (live state preserved)

The fix prefers live `ta.value` as the append base when the textarea
is non-empty, falling back to the `prefixOverride` snapshot only
when the textarea is genuinely empty (the original empty-composer path
where nothing to race with).

Adds a regression test in test_raw_audio_upload.py that asserts the
new `ta.value ||` fallback structure on `_commitTranscript`.

* fix(dictation): don't resurrect cleared text on async transcription resolve

Greptile P1 (re-reviewed at 20:07 UTC): the previous race fix
(`ta.value || (prefixOverride ?? _prefix)`) treats an empty
`ta.value` as falsy and falls back to the recording-start snapshot.
If the user explicitly clears the textarea while server transcription
is in flight, the cleared text reappears with the dictated text
appended — the user's intent is violated.

Fix: when prefixOverride IS passed (server-STT path), trust live
ta.value UNCONDITIONALLY — even when empty. The fallback to _prefix
only fires when no prefixOverride is given AND ta.value is empty,
which is purely defensive (no current caller hits this branch — the
browser-SR path commits inline in sr.onend without calling this
function).

Concrete repro:
  User typed before mic:    'Hello, '
  User clears textarea during STT
  Dictated text:            'world'
  OLD (0239bb4e) result:    'Hello, world'  (cleared text resurrected)
  NEW result:               'world'        (user intent respected)

Greptile's suggested one-liner:
  const base = prefixOverride !== undefined ? ta.value : (ta.value || _prefix);
Applied with expanded inline comment documenting the three concerns
this function balances (race, clear-during-STT, defensive fallback).

* Release exp-v0.52.49: preserve composer text on server STT commit (#5895)

---------

Co-authored-by: Konstantin M <kopamedve@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 20:47:54 -07:00
nesquena-hermes
18793a1ba4
Release exp-v0.52.42: ?profile= deep links (#5730) + Russian locale refresh (#5778) (#5953)
* Refresh Russian localization bundle

* feat(#5682): honor profile query during boot

* fix(profile): refresh deep-link reasoning state (#5682)

* test(ci): initialize reasoning transition harness state

* test(ci): accept reasoning fetch context override

* fix(#5682): preserve retryable profile query intent

* test(#5682): accept profile switch result contract

* fix(#5682): bind reasoning fetches to profile transitions

* Release exp-v0.52.42: ?profile= deep links (#5730) + Russian locale refresh (#5778)

---------

Co-authored-by: DrMaks22 <DrMaks22@users.noreply.github.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 15:58:47 -07:00
nesquena-hermes
bd1cdf24c7 Merge #5910: add 'max' reasoning effort level (opt-in, availability-gated) 2026-07-11 04:47:37 +00:00
nesquena-hermes
54efd6078e feat(workspace): opt-in 'start new chat when switching workspace' (#5473)
Salvage-rebuild of #5473 (@ruizanthony), reworked to OPT-IN per maintainer
decision. New default-OFF setting new_chat_on_workspace_switch: when enabled,
selecting a DIFFERENT workspace starts a fresh chat bound to that workspace
(leaving the current conversation on its original workspace, avoiding stale
cross-workspace context) instead of mutating the current session in place.
Same-workspace selection stays an in-place refresh. Default off preserves the
shipped behavior for everyone.

Wiring: config default + bool key; window._newChatOnWorkspaceSwitch set on
boot + autosave; Settings checkbox (Preferences); gated branch in
switchToWorkspace() that runs before the in-place /api/session/update; 3 i18n
keys across all 15 locales; regression tests (default-off, gated-branch-present,
flag-wiring, checkbox+i18n).

Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
2026-07-11 03:13:48 +00:00
nesquena-hermes
75153f5b79 fix(share): always-on redaction boundary + rebase/UX fixes (gate findings)
Adversarial security re-gate on the rebased head found 4 issues; fixed:

1. [CORE] api/shares.py public snapshot could leak credentials/paths/tool
   payloads. The sanitizer relied on redact_session_data (gated on the
   user-toggleable api_redact_enabled) — disabling it exposed API keys, a dict
   assistant content was stringified as raw tool data, and session.workspace
   survived in transcript text. Now the per-message sanitizer applies ALWAYS-ON
   credential redaction (_redact_fn_cached, force=True, ignores the setting) +
   scrubs known session/workspace/home paths, and refuses to stringify non-text
   structured content. Title is force-redacted too. Regression test proves the
   boundary holds with api_redact_enabled=False.
2. [SILENT] api/models.py: my rebase wrongly kept anchor_activity_scenes in
   METADATA_FIELDS, moving 250-480KB scene bodies before messages and undoing
   master's metadata fast-path (#5854). Removed; 17 #5854 tests pass again.
3. [SILENT] static/index.html: the Share/StopSharing/ExportHTML button triplet
   was double-applied by the rebase (duplicate IDs → only the first wired).
   Deduped to one each + a count assertion test.
4. tests: fixed the stale Session.load-after-/api/session/new setup (master
   keeps new sessions memory-only until first message).

Also (UX, found in the visual flow sweep): a clipboard-copy failure after a
SUCCESSFUL share showed a red 'Share failed' toast and skipped opening the page.
The copy is now isolated — the share succeeds, the page opens, and the toast
shows the link if the clipboard write fails, never a false failure.

Co-authored-by: MicoRobot.
2026-07-11 02:06:34 +00:00
nesquena-hermes
e061e8534c Rebase #5337 (public read-only conversation sharing) onto master
Rebased 363 commits forward (branch-point 2026-07-05). Applied the PR's code
diff cleanly across 16 files; resolved api/models.py additively (both master's
process_wakeup_pause/anchor_scene_index fields AND the PR's share_token/
share_created_at fields kept in the Session constructor, metadata list, and
to_dict). CI workflow (.github/workflows/tests.yml) intentionally NOT taken from
the stale branch — master owns it (the branch's apparent removal of Playwright/
Office steps was a PAT-workflow-scope revert; rebase restores master's CI).

Co-authored-by: MicoRobot. Concept approved by maintainer (opt-in public share,
2026-07-11). Note: 4 dynamic share tests fail identically on the original
unrebased head (pre-existing test-env Session.load fragility, not rebase-caused);
to be resolved during the security re-gate in a proper sandbox.
2026-07-11 01:39:42 +00:00
nesquena-hermes
d56123f796 feat: add max reasoning effort level (salvage of #4627)
Salvaged from #4627 by @perejaslav. Core Hermes already supports
reasoning_effort=max; this closes the WebUI parity gap (Anchor-A) — adds
max to VALID_REASONING_EFFORTS + dropdown/chip/reasoning-command, keeps
the degrade ladder for capped models.

Co-authored-by: perejaslav <perejaslav@users.noreply.github.com>
2026-07-11 01:13:36 +00:00
nesquena-hermes
0af0a5fcf0 Merge #5746: final answer only activity display mode 2026-07-10 23:50:28 +00:00
sheldon-im
67f8414656 Keep closed workspace dropdown out of SR navigation 2026-07-09 02:40:36 +00:00
sheldon-im
06e0df4656 Announce New Chat workspace transiently 2026-07-09 02:40:36 +00:00
sheldon-im
23de53c004 Improve workspace switcher screen reader state 2026-07-09 02:40:35 +00:00
t
0a56f44f5e fix(updates): Fable UX gate — channel re-check race + copy + mobile badge
MUST-FIX: /api/updates/check now accepts an explicit 'channel' in the POST body;
the Settings dropdown passes the just-picked value to checkUpdatesNow(channel) so
the immediate re-check can't race the debounced autosave PUT and answer for the
previous channel (verified live: body channel wins over stale saved setting).

SHOULD-FIX:
- copy: 'soaked' -> 'soak-tested'; append 'Applies to WebUI updates only.' to the
  channel helper text (all 15 locales, native translations)
- mobile: .settings-version-badge gets max-width/overflow/ellipsis in the <=768px
  query so the '· Experimental' suffix can't overflow a 320px viewport

(#5 badge-suffix i18n key deferred — non-blocking fast-follow per Fable, matches
the existing English-only version badge on master.)
2026-07-08 20:01:39 +00:00
t
2cf43527a3 feat(updates): stable/experimental release channels
Add an update_channel setting (stable|experimental) that selects which git-tag
stream the self-updater tracks on the single linear master line:

  stable       -> 'v*'      promoted, soaked releases (default; unchanged glob)
  experimental -> 'exp-v*'  every release batch (opt-in testers)

A channel is only a tag glob — no branches, no divergence — so every ff-only
guarantee (#2653/#2846/#3140) is preserved. Channel governs the WebUI repo only;
the Agent repo keeps its historical branch fall-through.

Correctness (advisor-reviewed, Codex + Fable):
- describe uses --match <glob> so a commit tagged both v* and exp-v* resolves
  to the channel-correct tag
- stable never falls through to origin/master when HEAD contains the latest
  stable tag (the firehose-suppression that makes channels work)
- apply_force_update refuses to reset --hard onto an ancestor ref (rewind guard)
- update cache + in-progress guard keyed by (channel, include_agent)
- channel display badge is a SEPARATE field; WEBUI_VERSION stays channel-neutral
  (asset cache-busting / SW cache / stale-client skew all do exact-string equality)

Settings UX: channel dropdown with risk-forward copy + the ff-only asymmetry
helper ('switching back to Stable keeps your current version until Stable catches
up'), channel chip on the version badge, i18n keys, re-check on switch.

Tests: new tests/test_update_channels.py (12 real-git-fixture tests) + existing
update suite updated for the channel-aware signatures.
2026-07-08 19:25:27 +00:00
nesquena-hermes
2717ea0ac5 feat(chat): add final answer only activity display mode (#5706, picker grid fixed)
Clean rebase of rodboev's #5746 (rebase-first).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-07 14:38:08 +00:00
b3nw
dcec232dfa fix: add btnClearUpdateLock markup for PR #5688 v2 frontend path
Address Greptile P1 review (discussion r3530941293) on commit e8f7e28a:
the v2 frontend recovery path was dead at the DOM level. static/ui.js
references $('btnClearUpdateLock') but no matching element existed in
static/index.html, so users hitting a lock conflict never saw a clickable
affordance -- the error panel was the only visible state.

Add the missing button next to btnForceUpdate in the update-banner action
row:

  <button class="update-btn" id="btnClearUpdateLock"
          style="display:none"
          onclick="applyClearUpdateLock(this)">
    Clear lock and retry update
  </button>

Style mirrors the neutral update-btn (NOT the red --error used by the
destructive Force update) so the recovery affordance doesn't read as a
warning. Hidden by default; revealed by the existing _showUpdateError()
helper when res.lock_conflict is set.

Regression tests in TestClearLockButton (3 tests):

- test_clear_lock_button_exists
- test_clear_lock_button_hidden_by_default
- test_clear_lock_button_calls_applyClearUpdateLock_handler

These lock in the contract that the button element must be present and
correctly bound; future refactors that rename the id or strip the button
will fail loud. Mirrors the existing TestIndexHtmlBanner conventions
for btnForceUpdate.

Verification:

- ./scripts/test.sh tests/test_updates.py  -k lock -> 35 passed
- ./scripts/test.sh tests/test_update_banner_fixes.py  -> 102 passed
- ./scripts/test.sh tests/test_api_timeout.py  -> 8 passed
- ruff_lint --diff upstream/master  -> 0 new violations
2026-07-06 17:46:30 +00:00
nesquena-hermes
02e18d4d69 release: restore prose-level fade for Transparent Stream (#5506)
Re-adds the per-word prose fade for newly-streamed assistant text in Transparent Stream WITHOUT reintroducing the row-level entrance flicker (#5367). Reduced-motion honored at both JS and CSS layers; thinking/tool rows not animated. Option A: the fade is part of Transparent Stream regardless of the off-by-default 'Fade text effect' toggle; settings description clarified accordingly. Gate: Codex SAFE, Fable SHIP-UX, suite 12158/0, browser-smoke clean.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-07-06 00:26:45 +00:00
nesquena-hermes
c2e9277c46 release: reuse the rich model picker in Preferences (#5502)
Generalizes renderModelDropdown() to serve both the composer and Preferences pickers (backward-compatible defaults keep the composer identical). Folded in Fable UX fixes: label->chip association, downward shadow for the down-opening settings picker, and mobile keyboard-pop suppression across all 3 render paths (open/internal-filter/refresh) with focus preserved while typing. Full gate (4 rounds): Codex SAFE, Fable SHIP-UX, suite 12153/0, browser clean; composer picker verified unchanged.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-07-05 23:22:31 +00:00
nesquena-hermes
d5dae70c43 review: merge #5502 (087c694c) 2026-07-05 22:35:58 +00:00
nesquena-hermes
eef981d3dc release: copyable workspace relative paths (#5548)
Adds 'Copy relative path' to the workspace file-tree context menu + preview header. Folded in gate fixes: preview-header button folds to icon-only on a narrow pane via @container rightpanel (not viewport @media), with data-i18n-title/aria-label so the tooltip+accessible name localize. Full gate: Codex SAFE, Fable SHIP-UX, suite 12147/0, browser clean.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-07-05 22:26:31 +00:00
Rod Boev
71a6259c84 Align copied paths with chat references 2026-07-04 15:26:25 -04:00
Rod Boev
7ebd898c5e feat(#5533): make preview file paths copyable 2026-07-04 13:37:39 -04:00
Rod Boev
087c694c8d feat(#5497): reuse the rich model picker in Preferences 2026-07-03 22:32:50 -04:00
Rod Boev
f32938021c fix(#5428): surface stale WebUI client recovery 2026-07-03 17:43:46 -04:00
nesquena-hermes
012c66d12c Hide inactive flyouts from screen readers (+ visual-hide CSS)
Clean rebase of sheldon-im's #5404 (rebase-first).

Co-authored-by: sheldon-im <sheldon-im@users.noreply.github.com>
2026-07-02 07:15:53 +00:00
nesquena-hermes
b931ae2f9b fix(settings): reconcile #5145 rename+steer-flip onto master's #5170 mirror
Rebase PR #5162 (rename busy_input_mode -> default_message_mode; flip the
default from 'queue' to 'steer') onto current origin/master WITHOUT dropping
the shipped #5170 localStorage persistence mirror.

Rename the mirror machinery to the new setting name for consistency:
  _BUSY_INPUT_MODES        -> _DEFAULT_MESSAGE_MODES        (values unchanged)
  _normalizeBusyInputMode  -> _normalizeDefaultMessageMode  (fallback now 'steer')
  _persistBusyInputMode    -> _persistDefaultMessageMode
  _readPersistedBusyInputMode -> _readPersistedDefaultMessageMode
  window._busyInputMode    -> window._defaultMessageMode (+ renamed exports)

localStorage: write the new 'hermes-default-message-mode' key; read it with a
fallback to the legacy 'hermes-busy-input-mode' key so an existing user's
persisted preference survives the rename.

Preserve #5170 behavior at every mirror site under the new names:
  - boot success  -> window._defaultMessageMode=_persistDefaultMessageMode(...)
  - boot FAILURE  -> window._defaultMessageMode=_readPersistedDefaultMessageMode()
    (NOT a hardcoded 'steer' — a saved 'interrupt'/'queue' must still apply when
    the server is unreachable; do not regress #5167/#5132)
  - preferences autosave, settings-panel load, and _applySavedSettingsUi all
    persist through _persistDefaultMessageMode(...)

Tests updated for the rename while keeping the persistence-behavior assertions
(test_1062, test_5145, test_5167); test_5167 gains explicit guards that the
load-failure path reads the persisted pref and never hardcodes a literal mode,
plus autosave/panel-load mirror-write coverage.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-07-02 05:44:48 +00:00
nankingjing
cabbb9a841 fix(ux): add expand control for update summary panel (#4705)
Move scrolling to an inner container and add an Expand/Collapse toggle
so long generated summaries are readable on narrow viewports.

Fixes #4705
2026-07-02 05:14:00 +00:00
nesquena-hermes
219615b5a7 fix(composer): remove Export-to-HTML button from composer footer (keep settings-panel export)
The #4968 export button was hard-inserted into the composer footer .composer-left
row, bypassing the configurable composer-control framework. On desktop it pushed the
footer over its overflow threshold, tripping _fitComposerFooter into cf-icons mode
which HIDES the model/workspace/profile text labels. Removing it restores label
visibility. Export stays available via the settings-panel HTML button (#btnExportHTML).
2026-07-02 02:48:45 +00:00
nesquena-hermes
b31f3fab3b #4968: export chat to self-contained themed HTML, rebased on master 2026-07-02 02:36:09 +00:00
nesquena-hermes
4bbb64bcc1 #5213: Claude Code sidebar visibility toggle (#4714), rebased on master 2026-07-02 01:40:19 +00:00
Paladin173
d8f0f8f97f Fix composer control reorder rebase collision
Reapply footer control ordering on current upstream/master while preserving the required situational chip renderer. Persist composer_control_order with backend validation, keep the settings descriptions reorder-aware, and make primary/situational chip renderers participate in same-group drag ordering.

Verified with: node --check static/boot.js; node --check static/panels.js; git diff --check; ./scripts/test.sh tests/test_issue4598_composer_control_visibility.py
2026-06-30 21:11:54 -05:00
nesquena-hermes
3e84d7bf11 Merge #4738 (savagebread neon skins + skin-persistence fix) into stage 2026-06-30 22:21:28 +00:00
nesquena-hermes
78ca4d43a1 Merge #5005 (futureworld678 Shift+Enter) into stage 2026-06-30 21:51:42 +00:00
nesquena-hermes
8a5172275d Merge #5002 (per-project quick-create, opt-in) into stage 2026-06-30 20:56:05 +00:00
nesquena-hermes
ba401d3784 Merge #5079 (OpenAI TTS + SSRF hardening) into stage 2026-06-30 20:39:04 +00:00
nesquena-hermes
027699b570 Merge #4881 (rodboev Kanban modal fields) into stage 2026-06-30 19:50:46 +00:00
nesquena-hermes
b098e5c658 feat(#4676): gate per-project quick-create buttons behind opt-in setting
Add a default-OFF 'project_quick_create_buttons' setting that gates the
per-project '+' quick-create buttons on sidebar project chips. The chip
render site only calls _attachProjectQuickCreateButton when the new
window._projectQuickCreate runtime flag is enabled, so the buttons do
not appear unless the user opts in via Settings.

Mirrors the large_text_paste_as_attachment opt-in plumbing (defaults,
allowlist, settings collect/restore/save, window hydration at both sites,
checkbox row, i18n across all locales).
2026-06-30 19:49:07 +00:00
savage_bread
2bd1f68805 Add Neon Soft and Neon Paint skins
- Neon Soft: lighter dark-mode surfaces with the same neon purple/cyan palette
  as the existing Neon skin.
- Neon Paint: cyberpunk-themed skin with hot pink + electric cyan + warm amber
  on a deep purple-gray background.

Both skins include full light and dark mode variable sets plus component-level
overrides (selection, titlebar, sidebar, inputs, buttons, scrollbars, tool
cards, tree nodes, session badges, diff blocks, and links). Skin registration
is updated in boot.js and the inline skin whitelist in index.html
2026-06-30 18:28:33 +00:00
Lester
b1c11939aa Add Shift+Enter send key option 2026-06-30 18:04:29 +00:00
nesquena-hermes
121059b384 Merge #5161 (pr/5144-busy-composer-placeholder-hint) into stage-w5 2026-06-30 17:25:03 +00:00
Maude Bot
5263b9acf7 feat: add Webhooks sidebar filter parity
Co-authored-by: Kilian Tyler <kilian@kil.dev>
2026-06-30 17:06:31 +00:00
nesquena-hermes
3a12ac6bb8 Merge #5264 (fix/large-text-paste-toggle-upstream) into stage-w2 2026-06-30 16:35:55 +00:00
Stacey2911
6b0d8eaadf fix(webui): make large text paste-as-attachment toggleable via settings
Add a user setting 'large_text_paste_as_attachment' (default: on) that lets
users paste large text inline instead of having it auto-converted to .md file
attachments. The toggle lives in Control Center → Chat tab and persists via
settings.json, following the established upstream pattern for boolean settings.
2026-06-30 14:25:53 +10:00
Rod Boev
761ffc5d96 fix(#5250): let settings search results escape the menu clip 2026-06-29 22:23:00 -04:00
Rod Boev
aaabd32c92 feat(#4470): expose skills, max_runtime_seconds, and parents in Kanban create modal 2026-06-29 15:27:16 -04:00
nesquena-hermes
fae5c45395 Merge #5163 into stage/p1a 2026-06-29 04:07:21 +00:00
nesquena-hermes
ca06796764 fix(i18n): restore default-off quota-chip copy (Codex gate #4657)
Codex found leftover quota-chip residue: #4657 had rewritten settings_desc_quota_chip
to 'Shown by default...' (and regressed several locales to English), contradicting
the reverted default-OFF behavior. Restored master's localized 'Default off / wide
displays only' copy across all 14 locales + index.html. The composer_mobile_quota
strings (mobile config panel, fit-related) are kept.

Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
2026-06-29 02:06:15 +00:00
nesquena-hermes
c6370235be feat(composer): fit footer controls by overflow (#4657, rebased onto master)
Rebased Paladin173's #4657 onto current master. The per-control visibility
toggles half already shipped via his own #4598, so this rebase carries only the
net-new JS fit-by-overflow engine (_fitComposerFooter: ResizeObserver +
MutationObserver measure .composer-left overflow, step full -> cf-icons ->
cf-burger). Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
2026-06-29 01:19:02 +00:00
nesquena-hermes
863dc4e041 Splash logo: drop app-icon tile, free-breathing mark with per-theme glow
The new-conversation splash mark was wrapped in a glossy navy rounded-rect
that reads as a macOS app-launcher tile ('this is an app') rather than a
brand mark. Major AI chat UIs (Claude, ChatGPT, Gemini) all render their
empty-state mark bare. Remove the tile, scale the winged caduceus up
(~88px), and back it with a soft cyan/blue radial 'spirit' bloom.

Light and dark get separately tuned marks: dark keeps the bright
cyan->blue gradient + luminous bloom; light uses a deeper #0AA6CC->#1E63D6
gradient + cooler restrained bloom so the mark holds contrast on the pale
background instead of washing out. Entrance is gated behind
prefers-reduced-motion.
2026-06-28 23:42:02 +00:00