Commit graph

4820 commits

Author SHA1 Message Date
nesquena-hermes
749f95d01c docs(#4671): clarify dead-assignment backstop + add four-guard contract comment (Opus polish)
Opus ship-gate non-blocking observations (no MUST-FIX), applied to lower future-maintainer
risk on the crown-jewel profile-switch surface:
- clarified the belt-and-suspenders _sessionListSkeletonActive=false at the real-paint site
- added a FOUR-GUARD CONTRACT doc-comment at the top of switchToProfile naming the four
  interacting state pieces (embargo / invalidation / skeleton-flag / _wsTreeGen + _switchGen)
  so a future edit touching one without the others doesn't silently reopen a window.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-23 01:38:25 +00:00
nesquena-hermes
710f2c6934 test(#4671): parse full switchToProfile body instead of a fixed char window
Codex re-gate 5: the only remaining finding — my race-guard work grew switchToProfile
past the 6500-char window (applyBotName now at offset ~6609). Instead of bumping the
window again (it'll drift), bound the slice on the next top-level function
(openProfileCreate) so the test reads the whole body and can't break on growth.
No race/logic findings this pass — the profile-switch races are converged clean.
2026-06-23 01:27:11 +00:00
nesquena-hermes
1b0c64a604 fix(#4671): embargo also guards the fetch-failure path + setup moved inside try (Codex re-gate 4)
Two more edge cases from re-gate 4:
(1) SILENT: a mid-switch /api/sessions that FAILS bypassed the embargo (the drop-check
was only on the success path), so its catch cleared the skeleton + rendered old-profile
cache. Added 'if(_profileSwitchListEmbargo) return;' after the gen guard in the catch too.
(2) CORE: the embargo was set BEFORE the try, so a throw in the synchronous setup could
leak it (the guarded finally only covered the try). Moved the invalidate/embargo/skeleton/
ws-gen setup INSIDE the try so the _switchGen-guarded finally always lifts the embargo.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-23 01:18:58 +00:00
nesquena-hermes
ce0ef36ab1 fix(#4671): embargo session-list renders during the switch window (Codex re-gate 3)
Codex re-gate 3 found a third clobber variant: point-in-time invalidation isn't enough
because a renderSessionList() can START after the skeleton shows but BEFORE
/api/profile/switch returns. Since the new-profile cookie is only set by the switch
response, that GET fetches the OLD profile's rows, passes the (now-current) generation
guard, and clobbers the skeleton.

Fix: a profile-switch session-list EMBARGO. _setProfileSwitchListEmbargo(true) before
showSessionListSkeleton(); _runRenderSessionListRefresh drops ALL payloads (after the
gen guard, before apply) while embargoed; the switch lifts it immediately before its own
authoritative renderSessionList() (both branches), in the failure-restore, and in a
_switchGen-guarded finally as a safety net so it can't freeze the sidebar on an
early-return/throw. + regression test.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-23 01:11:24 +00:00
nesquena-hermes
0a64d7d7c6 fix(#4671): invalidate in-flight session-list renders at switch start (Codex re-gate 2)
Codex re-gate 2 found a remaining CORE clobber variant: a renderSessionList() already
in flight BEFORE the switch (background poll fetching the OLD profile's /api/sessions)
would resolve after the skeleton appears but before the switch's own renderSessionList()
bumped _renderSessionListGen — so it passed the generation guard, cleared the skeleton
flag in _applySessionListPayload, and painted stale rows.

Fix: _invalidateSessionListRenders() bumps _renderSessionListGen + clears
_pendingSessionListPayload / _renderSessionListQueuedRequest, called at switch start
BEFORE showSessionListSkeleton(). Any outstanding pre-switch response is now stale
(its captured _gen < current) and discarded by the existing guard; deferred/queued
applies are dropped. + regression test asserting the helper exists and runs before the
skeleton.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-23 00:56:30 +00:00
nesquena-hermes
66786143a8 fix(#4671): close 2 profile-switch render races via skeleton-flag + ws generation token
Reworked from the Codex re-gate (rebased onto fresh master so #4730's gateway-poll
work is preserved, not reverted). Two CORE races + a test-window break:

(1) Skeleton-clobber (sessions.js): renderSessionListFromCache() now bails while
_sessionListSkeletonActive — _allSessions still holds the PREVIOUS profile until
/api/sessions resolves, so a gateway-poll/SSE/timer/cache render mid-switch would
repaint the wrong profile's rows over the skeleton. _applySessionListPayload clears
the flag right before painting (fresh data in hand; superseded responses already
discarded by the generation guard); the fetch-failure path and switch-failure catch
clear it too so it can't strand. NO signature change (kept renderSessionListFromCache()
and _applySessionListPayload(sessData,projData) so the ~10 static tests that anchor on
those markers stay green).

(2) Workspace wrong-profile tree (workspace.js + panels.js): an empty-session switch
reuses the same session_id, so loadDir()'s id guard can't reject a stale pre-switch
/api/list. Added _wsTreeGen generation token + bumpWorkspaceTreeGen(), bumped
UNCONDITIONALLY at switch start (even panel-closed, since loadDir('.') still runs),
captured in loadDir() and re-checked after both awaited /api/list points.

(3) test_issue1116 proximity window 5000->6500 (applyBotName now ~char 5618, still fires).

+ regression tests: skeleton-flag bail/clear lifecycle, ws generation token, failure
restore clears flag; updated #1785 stale-session guard test for the compound condition.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-23 00:46:14 +00:00
nesquena-hermes
a8805bb658 stage #4671 (nesquena-hermes, nesquena+rodboev reviewed): profile-switch loading skeletons (#4662 phase 1)
# Conflicts:
#	CHANGELOG.md
2026-06-23 00:17:17 +00:00
nesquena-hermes
3d2cd8bce5
Merge pull request #4734 from nesquena/stage-4730b-release
Release v0.51.593 — Release UZ (gateway-poll listener cleanup, #4730 follow-up)
2026-06-22 16:53:22 -07:00
nesquena-hermes
a32c3a3f1e Release v0.51.593 — Release UZ (gateway-poll visibilitychange listener cleanup, #4730 follow-up)
Co-authored-by: akrhin <akrhin@users.noreply.github.com>
2026-06-22 23:50:09 +00:00
nesquena-hermes
9430bda43d
Merge pull request #4733 from nesquena/stage-4730-release
Release v0.51.592 — Release UY (gateway-poll skip + smd re-init, #4730)
2026-06-22 16:46:18 -07:00
nesquena-hermes
938ae25697 Release v0.51.592 — Release UY (skip gateway poll when hidden/streaming + smd re-init, #4730) 2026-06-22 23:42:30 +00:00
nesquena-hermes
20c70c3505 fix(#4730): clear assistantBody before smd re-init to prevent duplicate live text
Codex gate found: when window.smd becomes available after a segment already
rendered through the renderMd() fallback, _smdNewParser resets _smdWrittenText
to '' but does NOT clear the element, so the following _smdWrite(displayText)
appends the full accumulated segment on top of the existing fallback render —
duplicating the live assistant text. Clear assistantBody.innerHTML before re-init.

Co-authored-by: akrhin <akrhin@users.noreply.github.com>
2026-06-22 23:27:23 +00:00
nesquena-hermes
e571590c9b stage #4730 (akrhin): skip gateway poll when hidden/streaming + smd re-init before innerHTML fallback 2026-06-22 23:22:23 +00:00
sintez
f2abe50b7c perf: skip gateway poll when hidden/streaming, re-init smd parser before innerHTML fallback 2026-06-23 02:13:34 +03:00
nesquena-hermes
3bf0e194f5
Merge pull request #4726 from nesquena/stage-4721-release
Release v0.51.591 — Release UX (stop transcript jumping to first message, #4721/#4720)
2026-06-22 14:31:35 -07:00
nesquena-hermes
35412fd3ef Release v0.51.591 — Release UX (stop transcript jumping to first message, #4721/#4720) 2026-06-22 21:27:57 +00:00
nesquena-hermes
44d840d0a1 stage #4721 (nesquena-hermes, reviewed by nesquena): reset _oldestIdx on done to stop transcript jumping to first message (fixes #4720)
# Conflicts:
#	CHANGELOG.md
2026-06-22 21:17:12 +00:00
nesquena-hermes
acae87c3cb
Merge pull request #4725 from nesquena/stage-4544-release
Release v0.51.590 — Release UW (close cross-profile credential leak, #4544/#3961)
2026-06-22 14:11:18 -07:00
nesquena-hermes
4ae41c6d7a fix(#3961): add Anthropic OAuth tokens to fail-closed floor (agent-absent CI/test env)
The registry-derived scrub union is best-effort and returns empty when
hermes_cli.auth can't be imported (WebUI-only CI/test env). That made
ANTHROPIC_TOKEN / CLAUDE_CODE_OAUTH_TOKEN scrubbing depend on the agent being
installed, so the quota-subprocess regression failed on CI (agent absent) while
passing locally (agent present). Add both token names to the fail-closed floor
set so the scrub covers them regardless of the registry import — strictly more
correct (defense-in-depth) and deterministic across environments.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 21:07:54 +00:00
nesquena-hermes
c55ba5df11 Release v0.51.590 — Release UW (close cross-profile credential leak, #4544 / #3961) 2026-06-22 20:53:58 +00:00
nesquena-hermes
d2ee837796 fix(#3961): resolve Opus ship-gate findings (startup NameError + reload cache poisoning)
Opus final gate reproduced two issues in the config-expansion hardening:

MUST-FIX — import-order forward reference. _expand_env_vars (config.py top) calls
_thread_local_env_value, but that fn + _thread_ctx were defined ~6900 lines later,
AFTER the import-time reload_config(). A ${VAR} in a startup config.yaml raised a
(swallowed) NameError, leaving import-time config consumers with empty config.
Fix: relocate _thread_ctx + _thread_local_env_value above the config-file section
(they depend only on os + threading). Verified: startup ${VAR} now expands, no
NameError.

SHOULD-FIX — reload_config baked block-scoped expansion into the process-global
_cfg_cache. If a reload fired while a readonly/worker scope was active (profile
alternation resolves _get_config_path to the named profile), under-expanded
literal ${VAR}s were written to the shared cache, starving concurrent readers of
the `cfg` alias. Fix: reload_config now expands the global cache with the
block flag forced off (process-env view), since the cache is process-global;
per-request scoped reads still go through _thread_local_env_value at their own
sites. Verified: global cache holds the process-expanded value, not the literal.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 20:18:01 +00:00
nesquena-hermes
d60ad140b1 fix(#3961): scrub Azure managed-identity (MSI/IMDS) credential-source env vars
Final credential-surface item: agent/azure_identity_adapter.py treats
IDENTITY_ENDPOINT / MSI_ENDPOINT as ManagedIdentityCredential sources, so an
empty named profile could inherit the host's managed identity on the quota +
detached-worker paths. Add IDENTITY_ENDPOINT / IDENTITY_HEADER / MSI_ENDPOINT /
MSI_SECRET to the non-registry scrub set (config var AZURE_FOUNDRY_BASE_URL kept).
Detached-worker regression extended.

This completes the agent's enumerable credential-env read surface (verified from
source). Decision (with maintainer): stop the Codex deep-gate here — the
remaining theoretical surface is cloud-SDK-internal credential-source endpoints
on pre-existing infra, not vectors introduced by this PR; validated via Opus +
full suite. The PR's #3961 scope (os.environ mirror race on /api/providers +
/api/models) is fully closed plus the broader same-class hardening.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 19:58:44 +00:00
nesquena-hermes
51dd69b4f2 fix(#3961): route config ${VAR} expansion through the profile-aware env lookup
Codex re-gate found a config-template leak vector distinct from the env-name
scrub: _expand_env_vars() expanded ${VAR} references in config values via raw
os.environ, so a profile config.yaml of e.g. `api_key: ${ANTHROPIC_TOKEN}` let
_get_provider_api_key reconstruct the server-process token and pass it through as
the API key argument — even when the child-env scrub stripped the env var. This
bypassed the thread-local channel entirely.

_expand_env_vars now substitutes via _thread_local_env_value (the same
thread-local-first accessor with the block_process_env_fallback guard the PR
introduced): a ${VAR} resolves to the profile's own value, and refuses the
process-env fallback when a profile-scoped readonly/background scope set the block
flag (leaving the reference unexpanded). Cache integrity preserved — only the RAW
unexpanded dict is cached; expansion re-runs per read against the current thread's
scope. Normal (no-scope) behavior unchanged. + regression that fails without the fix.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 19:49:44 +00:00
Nathan Esquenazi
66ee1d6c81 fix(review): unbreak test_session_rotate_url_sync (PR shard-2 CI was red)
The #4720 done-handler comment (9 lines) pushed the tab-anchor localStorage
sync (`hermes-webui-session` setItem + _setActiveSessionUrl) past the 800-char
proximity window that test_session_rotate_url_sync asserts the #3018 session-id
sync lives within — so the PR's `test (3.x, 2)` shards were failing on master's
own regression guard. The full pytest suite caught it; the PR's gate slice did
not run it.

Fix (behavior unchanged — the _oldestIdx reset stays in the same place, before
the re-filter, guarded):
- Trim the inline comment to one line (the full rationale is in the #4720 commit
  + issue), so the completion block stops overflowing the guard window.
- The reset statement legitimately grew the completion block even bare, so widen
  test_session_rotate_url_sync's proximity window 800 -> 1000 (still well within
  the single done/settled handler; the guard's intent is preserved).

test_session_rotate_url_sync + test_issue4720_* both pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:44:18 -07:00
nesquena-hermes
bf548a255d fix(#3961): complete cloud-credential scrub set (AWS container FULL_URI + Azure identity family)
Round-3 gate found two more credential env vars the agent resolves cross-profile
that the scrub set missed, verified against the installed agent source:
- AWS_CONTAINER_CREDENTIALS_FULL_URI — read by model_switch._has_aws_creds:1360
  (botocore container-credentials provider; RELATIVE_URI was present, FULL_URI
  was not).
- Azure identity family — AZURE_CLIENT_ID / AZURE_CLIENT_SECRET / AZURE_TENANT_ID
  / AZURE_FEDERATED_TOKEN_FILE (agent/azure_identity_adapter.py service-principal
  + workload-identity model auth) and AZURE_FOUNDRY_API_KEY
  (runtime_provider.py).

Enumerated the agent's full credential-env read surface from source to close the
whole class in one pass rather than one var per gate round. Deliberately EXCLUDES
config/region vars (AWS_REGION, AWS_DEFAULT_REGION, AZURE_FOUNDRY_BASE_URL) — the
child probe may legitimately need those. Regressions extended to pin FULL_URI +
the Azure family.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 19:27:00 +00:00
nesquena-hermes
ab71f4b906 fix: reset _oldestIdx on stream done so transcript stops jumping to first message (#4720)
After a turn completed on a long conversation that loaded truncated, the
viewport jumped to the first message. The done SSE handler replaced S.messages
with the full transcript payload and updated _messagesTruncated, but did NOT
reset _oldestIdx the way the canonical full-load paths do (sessions.js
_ensureMessagesLoaded, ui.js loadSession).

The read-position rework (#4613) keys the scroll anchor on an absolute index
(sessionIdx = _oldestIdx + rawIdx; rows tagged data-session-msg-idx). On done
the handler also expands the render window from the tail window (50) to all
messages, shifting every rawIdx. With _oldestIdx left stale, the captured
anchor resolved to an earlier row and the fallback used the raw scrollTop
against the now-taller full transcript, landing near the first message.

Reset _oldestIdx from d.session._messages_offset||0 alongside _messagesTruncated.
The done payload (_session_payload_with_full_messages) embeds the full transcript
and carries no offset, so this resets to 0 and keeps the absolute anchor aligned
with the rebuilt rows. No-op for non-truncated sessions; pinned-at-bottom readers
still follow to bottom (shouldFollowOnDone path unchanged).

Both gates clean (Codex SAFE TO SHIP, Opus ship-it) — verified the done payload
is always a full transcript with no offset, and every other _oldestIdx consumer
becomes more consistent.
2026-06-22 19:26:07 +00:00
nesquena-hermes
4eb069be97 fix(#3961): complete credential scrub with non-registry agent creds (CUSTOM_API_KEY, AWS/Bedrock)
Codex re-gate found the registry-derived scrub still missed credential env vars
the agent resolves via raw os.getenv() that aren't in PROVIDER_REGISTRY:
CUSTOM_API_KEY (hermes_cli/models.py), AWS_BEARER_TOKEN_BEDROCK
(hermes_cli/model_switch.py), AZURE_ANTHROPIC_KEY (runtime_provider.py), and the
boto3 AWS_* credential family (agent/bedrock_adapter.py). Same cross-profile leak
class on custom/AWS-backed provider quota + detached-worker paths.

- Add _NON_REGISTRY_AGENT_CREDENTIAL_ENV_NAMES fail-closed set, union into
  _profile_secret_env_names so BOTH leak sites pick it up.
- _account_usage_subprocess_env now relies on the centralized scrub set (WebUI-
  only fallback only if the api.profiles import fails), dropping the partial
  local AWS pair.
- 2 more regressions (quota subprocess + detached worker) covering CUSTOM_API_KEY
  + AWS family, fail without the fix.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 19:19:56 +00:00
nesquena-hermes
737b112f30 fix(#4662 review): suppress spurious switch timeout toast, dim group-label fade, re-guard in-progress branch
Addresses @rodboev + greptile review of #4671:
- panels.js switchToProfile(): pass timeoutToast:false to the /api/profile/switch
  POST so a superseded or transient-but-successful switch no longer pops api()'s
  generic 'Request timed out' error toast; the generation-guarded catch handler is
  now the single source of truth for switch failure.
- panels.js: re-check _switchGen after the awaited renderSessionList() in the
  in-progress-session branch so a rapid second switch can't have its workspace
  skeleton cleared / a stale toast popped by a slower earlier switch (mirrors the
  no-messages branch guard).
- style.css: group labels now fade in via skeletonFadeInDim (ends at opacity:.5)
  instead of the shared skeletonFadeIn (ends at opacity:1 with fill-mode:both),
  which was overriding their intended resting 50% opacity.
- 4 new static-source regression assertions locking the three fixes; CHANGELOG
  Unreleased entry updated (fixed-shape skeleton note + the three review fixes).
2026-06-22 19:10:08 +00:00
nesquena-hermes
1293f0301e fix(#3961): scrub agent-registry OAuth token env vars in profile credential scope
Codex re-gate of #4544 reproduced a residual cross-profile leak: the PR closed
OPENROUTER_API_KEY but ANTHROPIC_TOKEN / CLAUDE_CODE_OAUTH_TOKEN still leaked the
server-process credential to an empty named profile on /api/provider/quota
(anthropic) and the detached-worker model-rebuild path, because the scrub set was
built only from the WebUI _PROVIDER_ENV_VAR map (which omits OAuth/token-flow
provider env vars).

Widen the shared _profile_secret_env_names() scrub source to union in the agent
registry's api_key_env_vars (hermes_cli.auth.PROVIDER_REGISTRY) so every
credential env var the agent runtime can resolve via raw os.getenv() is scrubbed
in profile-scoped reads. Fixes both leak sites at once (both route through
_profile_secret_env_names). + 2 regressions that fail without the fix.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 19:09:59 +00:00
nesquena-hermes
239529f9bd stage #4544 (rodboev): route provider/model credential reads through thread-local profile channel (fixes #3961) 2026-06-22 18:47:59 +00:00
nesquena-hermes
80d6514c9d
Merge pull request #4716 from nesquena/stage-4695-release
Release v0.51.589 — Release UV (guard state.db replay after compressed anchors, #4695)
2026-06-22 11:41:07 -07:00
nesquena-hermes
c4569ab906 Release v0.51.589 — Release UV (guard state.db replay after compressed anchors, #4695) 2026-06-22 18:37:05 +00:00
Frank Song
0567b056c0 fix(compression): guard state replay after compacted anchors 2026-06-22 18:37:05 +00:00
nesquena-hermes
1e47baf0f0
Merge pull request #4715 from nesquena/stage-4711-release
Release v0.51.588 — Release UU (preserve Thinking detail scroll, #4711)
2026-06-22 11:25:01 -07:00
nesquena-hermes
423fb6ae13 Release v0.51.588 — Release UU (preserve Thinking detail scroll across rebuilds, #4711) 2026-06-22 18:20:44 +00:00
nesquena-hermes
42d1fbc728 Phase 1 (#4662): address remaining Greptile review (P2 + stagger)
Builds on Nathan's switch-failure workspace-skeleton-clear fix (which already
covered Greptile P1). Remaining two:

- [P2] Removed dead _clearSessionListSkeletonFlag() — the flag is reset inline in
  renderSessionListFromCache(); the helper had no call site.
- [stagger] The .skeleton-row:nth-child(N) fade-in delays skipped most rows
  because group-label siblings are interleaved with rows in the DOM (positions
  shift: rows land at child 2, 4-6, 8-11, so 9/10/11 got no delay). Replaced the
  CSS :nth-child rules with an inline per-row animationDelay set in JS (rowIndex
  * 0.025s, capped 0.2s) so every row gets its correct stagger regardless of
  group structure.

Tests: +1 behavioural (every row carries a real inline stagger delay; last row
not skipped). 26 pass.
2026-06-22 18:12:55 +00:00
Nathan Esquenazi
ef0b4ba37a fix(#4662): clear workspace skeleton on switch-failure no-workspace path
The success path clears a stranded workspace-tree skeleton when the new
profile has no bound workspace (both switch branches), but the catch block
only restored the tree `if (S.session && S.session.workspace)` — with no
else. So a profile-switch failure while the workspace panel is open and the
(still-current) previous session has no workspace left the up-front skeleton
shimmering forever: the same skeleton-strand class the Codex/Opus gates
targeted, in the one path they didn't cover. Mirror the success-path
no-workspace handling in the failure path. Adds a static regression
assertion that the catch block clears the workspace skeleton.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:12:55 +00:00
nesquena-hermes
e505543650 Phase 1 (#4662): CHANGELOG entry for profile-switch loading states 2026-06-22 18:12:55 +00:00
nesquena-hermes
7e731ab22b Phase 1 (#4662): fix virtualized-sidebar skeleton clobber (Codex gate round 2)
On long sidebars over the virtualization threshold, showSessionListSkeleton()
cleared the DOM but left the data-session-virtual-* window state + the queued
scroll RAF active. A pending _scheduleSessionVirtualizedRender() could then
repaint the PREVIOUS profile's cached rows over the skeleton before the new
/api/sessions response landed (SILENT regression Codex caught).

Fix:
- showSessionListSkeleton(): cancelAnimationFrame(_sessionVirtualScrollRaf) and
  delete the data-session-virtual-{total,start,end,filter,activeAnchor} markers
  so no scroll-driven render targets the skeleton; the real render rebuilds them.
- _scheduleSessionVirtualizedRender(): early-return while
  _sessionListSkeletonActive is true.

Tests: +2 static assertions; driver mock gains dataset + RAF stubs. 24 pass.
2026-06-22 18:12:25 +00:00
nesquena-hermes
adcd06ff76 Phase 1 (#4662): address Codex + Opus gate findings
Codex (CORE regression): the parallelized loadDir('.') started BEFORE the
_switchGen stale-switch guard; empty-session switches reuse the session id, so a
superseded switch's /api/list could paint the wrong profile's tree. Reverted to
guard-before-workspace-render ordering (the up-front skeletons already give the
immediate cross-surface feedback; the parallel gain was marginal).

Opus findings:
- [blocker] Workspace skeleton stranded permanently when the new profile has no
  bound workspace (loadDir skipped → nothing clears #fileTree). Added
  clearWorkspaceTreeSkeleton() (only empties when a skeleton is present) and call
  it on the no-workspace path in BOTH switch branches.
- [low-med] Session-list skeleton stranded if an inline-rename input or row
  action menu was open (renderSessionListFromCache early-returns before the DOM
  swap). Dismiss _renamingSid + closeSessionActionMenu() before showing the
  skeleton.
- [low] No-op self-switch flashed skeleton→restore (activateCurrentProfile
  doesn't pre-check). Added an early-return when name === S.activeProfile.

Tests: +4 static assertions for the self-switch guard, rename/menu dismissal,
no-workspace skeleton clear, and the clear-helper definition. 22 pass.
2026-06-22 18:12:25 +00:00
nesquena-hermes
4abdb54ede Phase 1 (#4662): tests for profile-switch loading skeletons
- behaviour: node-driven, runs the actual showSessionListSkeleton() /
  showWorkspaceTreeSkeleton() builders against a mock DOM and asserts the
  produced structure (3 group labels + 8 single-line rows with title+stamp;
  8 tree rows with glyph+name, 7 file rows with a size bar, 1 dir without).
- static: asserts switchToProfile() shows both skeletons up front before the
  /api/profile/switch POST, parallelizes the list+workspace refresh, restores
  real content on failure; renderSessionListFromCache clears the skeleton flag;
  style.css has the classes, sheen+fade keyframes, reduced-motion disable, and
  light+dark theme tokens.
2026-06-22 18:12:25 +00:00
nesquena-hermes
bafd7ddfd2 Phase 1 (#4662): strengthen skeleton shimmer contrast for perceptible motion
The initial sheen (15% sheen / 9% base, 1.4s) was too subtle to read as
animation. Bump to a 5-stop gradient (22-24% sheen / 8% base, 1.25s), define
explicit light-mode --skeleton-base/--skeleton-sheen tokens (was relying on
fallbacks only), and give the tree glyph the same shimmering gradient so it
animates with the bars. prefers-reduced-motion still disables the sweep.
2026-06-22 18:12:25 +00:00
nesquena-hermes
b084535b24 Phase 1 (#4662): profile-switch loading skeletons (sidebar + workspace)
Show polished loading skeletons the instant a profile switch begins instead of
leaving the previous profile's conversation list and workspace file tree visible
until their fetches resolve (~1s).

- static/style.css: theme-token skeleton system (single-line session rows with
  group labels + timestamp bar; tree rows with glyph + name + size bar); subtle
  shimmer sweep; dark-mode tokens; prefers-reduced-motion fallback.
- static/sessions.js: showSessionListSkeleton() mirrors real sidebar anatomy
  (group headers + single-line rows); _sessionListSkeletonActive flag cleared on
  real render.
- static/workspace.js: showWorkspaceTreeSkeleton() mirrors the file-tree row.
- static/panels.js switchToProfile(): clear stale content + show both skeletons
  up front; parallelize the independent list + workspace refreshes (Promise.all);
  restore real content from cache on switch failure so a skeleton never strands.

Verified live: switch fires -> skeleton instant (0 stale rows) -> resolves to new
profile's real rows. Polished across 1920/1440/1280/390 + dark mode (screenshots
pending Nathan's visual review before gate+PR).
2026-06-22 18:12:25 +00:00
nesquena-hermes
c346c9f786 stage #4711 (franksong2702): preserve Thinking detail scroll across render rebuilds (fixes #4707) 2026-06-22 18:05:13 +00:00
nesquena-hermes
d61a67b258
docs: weekly contributor tally refresh (20260622) (#4710) 2026-06-22 10:32:31 -07:00
Frank Song
87ab74d1b9 fix(ui): preserve thinking detail scroll on rebuild 2026-06-23 00:11:51 +08:00
nesquena-hermes
c5bb1fcaa0
stage #4688 (franksong2702): running-first session ordering in sidebar (#4699)
Overlays live pending_started_at/updated_at/last_message_at onto cached /api/sessions rows
(when newer), sorts active/running sessions first then by freshest effective timestamp, applies
the same running-first order in the frontend sidebar renderer, and groups date buckets with the
same runtime-aware timestamp (no stale header on resumed running sessions). routes.py +
sessions.js + 81-line test. Composes with #4680 cache-freeze + #4597 (98 session-list tests pass).

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-22 03:52:19 -07:00
nesquena-hermes
148222b40a
stage #4696 (rodboev): eliminate iOS tap delay on interactive buttons (fixes #4693) (#4698)
1-line CSS: adds touch-action:manipulation + -webkit-tap-highlight-color:transparent to
button/.icon-btn/.send-btn/.approval-btn/[onclick] inside the touch/compact media block
(@media max-width:1399.98px), removing iOS Safari's ~300ms double-tap-zoom delay so taps
fire on first touch. Added a regression test pinning the rule.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 03:21:07 -07:00
nesquena-hermes
ef0cdbf9f4
stage #4689 (franksong2702) v3: suppress duplicate live process echoes — authoritative head 14e8fba6 (#4697)
Module-level _compact_for_echo_compare (strips ALL whitespace, \s+ -> '') so _is_visible_output_echo's
endswith() suffix-match detects interim_assistant prose that only differs from visible token prose by
line-break/paragraph formatting. Hoisted to module scope (importable) + behavior-based test (real
token vs interim text equality). Codex SAFE (exact suffix match, not substring). 103 echo/journal tests.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-22 03:04:55 -07:00
nesquena-hermes
a66913ac26
stage #4680 (nesquena-hermes): freeze /api/sessions cache during streaming (fixes #4672) (#4690)
Freezes the session-list cache source-stamp on the SET of active stream ids (not per-write
state) so mid-stream sidebar polls hold the cache instead of rebuilding all_sessions() every
poll and contending for the global LOCK the streaming worker holds (the ~2 tok/s + 15s
/api/sessions cause). Structural mutations still invalidate via publish_session_list_changed;
only a streaming session's own title/message_count lags <=TTL. routes.py + 109-line test.
99 session-list/sidebar/cache tests pass.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-22 02:11:04 -07:00