Skip to content

fix: persist compact activity disclosure state#1729

Closed
Michaelyklam wants to merge 1 commit intonesquena:masterfrom
Michaelyklam:fix/persist-activity-disclosure
Closed

fix: persist compact activity disclosure state#1729
Michaelyklam wants to merge 1 commit intonesquena:masterfrom
Michaelyklam:fix/persist-activity-disclosure

Conversation

@Michaelyklam
Copy link
Copy Markdown
Contributor

Thinking Path

  • Compact Activity is meant to keep tool/thinking details quiet until the user asks for them.
  • The current live tool path forced compact Activity open, and switching chats rebuilt Activity groups from defaults instead of the user’s last disclosure choice.
  • Disclosure state is UI-only and browser-local, so localStorage keyed by chat/session plus turn is the smallest durable shape.
  • Live stream disclosure state should also carry forward to the settled assistant turn when the response completes.
  • This PR keeps Activity collapsed by default, but restores the user’s explicit open/closed choice whenever the chat is revisited or re-rendered.

What Changed

  • Added session-scoped Activity disclosure persistence in static/ui.js using hermes-activity-disclosure:<session_id>:<turn_key> localStorage keys.
  • Routed Activity summary clicks through _toggleActivityGroup() so each toggle updates ARIA, the existing live expand-intent tracker, and persisted state together.
  • Keyed settled Activity groups as assistant:<message_index> and live Activity groups as live:<stream_id>.
  • Changed compact live tool Activity to default collapsed instead of force-open.
  • Copied a live turn’s saved disclosure state onto the settled assistant turn when the stream completes.
  • Documented the per-chat/per-turn persistence behavior in DESIGN.md.
  • Added regression coverage for session/turn-scoped persistence, live default collapsed behavior, and the updated Bug: Activity panel collapses during streaming and latest user message disappears after Stop #1298 click path.
  • Added UI evidence under docs/pr-media/activity-disclosure/.

Why It Matters

Users can collapse or expand compact Activity once, switch to another chat, and return without the Activity row snapping back to the wrong mode. This removes a small but annoying transcript-state reset while preserving the compact default and existing progressive-disclosure behavior.

UI media

Persisted collapsed state after re-render:

Persisted collapsed compact Activity row

Explicitly expanded Activity row:

Expanded compact Activity row

Verification

# RED before implementation
/home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_ui_tool_call_cleanup.py::TestToolCallGroupingStatic::test_activity_disclosure_state_is_session_and_turn_scoped tests/test_ui_tool_call_cleanup.py::TestToolCallGroupingStatic::test_live_tool_activity_defaults_collapsed_unless_saved_open -q
# Result before fix: 2 failed

# Focused + related suites
/home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_ui_tool_call_cleanup.py tests/test_regressions.py tests/test_parallel_session_switch.py -q
# Result: 99 passed in 7.26s

# Updated #1298 click-path regression + compact Activity tests
/home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_issue1298_cancel_and_activity.py::TestIssue1298ActivityGroupExpandPersistence::test_inline_onclick_records_user_intent tests/test_ui_tool_call_cleanup.py -q
# Result: 16 passed in 1.72s

node --check static/ui.js
node --check static/messages.js
npx -y @google/design.md lint DESIGN.md
git diff --check

# Full suite, isolated from live config/host env
env -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/ -q
# Result: 4542 passed, 2 skipped, 3 xpassed, 1 warning, 8 subtests passed in 437.56s

# Raw media URLs
curl -L -s -o /dev/null -w 'closed %{http_code} %{size_download}\n' https://raw.githubusercontent.com/Michaelyklam/hermes-webui/fix/persist-activity-disclosure/docs/pr-media/activity-disclosure/activity-persisted-closed.png
curl -L -s -o /dev/null -w 'expanded %{http_code} %{size_download}\n' https://raw.githubusercontent.com/Michaelyklam/hermes-webui/fix/persist-activity-disclosure/docs/pr-media/activity-disclosure/activity-expanded.png
# Result: closed 200 45015; expanded 200 51574

Manual browser verification:

  • Started an isolated WebUI server on 127.0.0.1:18793 with a temporary state dir.
  • Injected a synthetic session with one assistant turn, reasoning, and two tool calls.
  • Verified the Activity row defaults collapsed with key assistant:1.
  • Clicked to expand, verified localStorage['hermes-activity-disclosure:s-persist:assistant:1'] === 'open'.
  • Re-rendered the transcript and verified the Activity row restored open.
  • Clicked closed, re-rendered again, and verified the row restored collapsed with stored value closed.

Risks / Follow-ups

  • Disclosure state is intentionally browser-local UI state, not synced server-side across browsers/devices.
  • The localStorage entries are small and keyed by session/turn; no pruning is added in this PR.
  • The behavior uses assistant message index for settled turns, which matches the current persisted transcript shape and existing tool-call indexing.

Model Used

AI assisted.

  • Provider: OpenAI Codex
  • Model: gpt-5.5
  • Notable tool use: Hermes file/terminal/browser tools, pytest, Node syntax checks, DESIGN.md lint, isolated browser QA, git/gh.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Summary

Pulled the branch and read the full diff against master. The persistence shape here is right — localStorage['hermes-activity-disclosure:<sid>:<turn_key>'] keyed by session id and either assistant:<index> (settled) or live:<stream_id> (in-flight), with a _copyActivityDisclosureState() handoff when a live turn settles. That is the smallest reasonable durable representation for a UI-only toggle.

There is one thing to flag, though.

Merge collision with #1725

#1725 (open right now, also touching static/ui.js and the same ensureActivityGroup() template in particular) deletes the .tool-call-group-list and .tool-call-group-count spans entirely from the summary. This PR keeps them. After both merge in either order, whoever lands second will need to rebase, and the obvious mechanical resolution leaves dead spans in this PR's template.

Reading static/ui.js:3819 on this branch, the new template still includes:

group.innerHTML=`<button type="button" class="tool-call-group-summary" aria-expanded="${collapsed?'false':'true'}" onclick="_toggleActivityGroup(this)"><span class="tool-call-group-chevron">${li('chevron-right',12)}</span><span class="tool-call-group-label">Activity</span><span class="tool-call-group-list">tools / thinking</span><span class="tool-call-group-duration"></span><span class="tool-call-group-count">0</span></button><div class="tool-call-group-body"></div>`;

vs #1725's simplified version which drops .tool-call-group-list and .tool-call-group-count (and also strips the corresponding CSS rules + sync logic in _syncToolCallGroupSummary). The persistence work here is orthogonal to the summary-cleanup work there, so a coordinated rebase should be straightforward — the _toggleActivityGroup(this) onclick rewrite and the data-activity-disclosure-key attribute don't depend on those spans being present.

Code reference

The persistence path on static/ui.js:3766-3784:

const _activityDisclosureStoragePrefix='hermes-activity-disclosure:';
function _activityDisclosureStorageKey(activityKey){
  if(!activityKey||!S.session||!S.session.session_id) return null;
  return _activityDisclosureStoragePrefix+S.session.session_id+':'+activityKey;
}
function _readActivityDisclosureState(activityKey){
  const key=_activityDisclosureStorageKey(activityKey);
  if(!key) return null;
  try{
    const saved=localStorage.getItem(key);
    return saved==='open'||saved==='closed'?saved:null;
  }catch(_){return null;}
}

And the live-to-settled transfer in static/messages.js:929-932:

if(typeof _copyActivityDisclosureState==='function'&&lastAsst){
  const assistantIdx=S.messages.indexOf(lastAsst);
  if(assistantIdx>=0) _copyActivityDisclosureState('live:'+streamId, 'assistant:'+assistantIdx);
}

Diagnosis

Two minor observations beyond the #1725 collision:

  1. The _writeActivityDisclosureState writes lazily on user toggle. There is no proactive cleanup of orphan keys when sessions are deleted. Per the body, this is intentional ("no pruning is added in this PR") and the entries are tiny, but if someone runs Hermes for a year with hundreds of sessions, the localStorage namespace will grow unbounded. Worth noting in a follow-up tracking issue rather than blocking here.
  2. _activityKeyForLiveTurn() returns null when there is no S.activeStreamId. ensureActivityGroup() falls back to live:<stream_id> only via opts.activityKey. The two ensureActivityGroup({live:true, ...}) callers in appendLiveToolCard and appendThinking both already pass activityKey:_activityKeyForLiveTurn(), so no path silently drops the key — that's good.

Verification

The static source-shape tests in tests/test_ui_tool_call_cleanup.py:150-191 cover the right invariants (storage prefix presence, session-id scoping, key attribute, hydrate-before-default, click-persists, settled key shape, live key shape, and the _copyActivityDisclosureState call in messages.js). All three CI checks are green.

Recommendation: rebase against #1725 once one of the two lands (this PR or that one); the persistence logic and the summary cleanup are independent and should not need any logic changes when reconciled — only the template literal and the contents of _syncToolCallGroupSummary.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Thanks @Michaelyklam — this shipped in v0.51.8 (commit 85d0279) as part of a 7-PR full-sweep batch release. Stage rebased your branch onto current master, ran the full pre-release gate (4584 pytest, browser tests, Opus advisor verdict SHIP), and merged via release PR #1737.

GitHub didn't auto-close because the merge commit only references the squash-merged stage branch, not your fork's commit directly — closing manually for hygiene.

Live now on https://get-hermes.ai/ and on existing installs after git pull + restart.

Release notes: https://github.com/nesquena/hermes-webui/releases/tag/v0.51.8

pull Bot pushed a commit to soitun/hermes-webui that referenced this pull request May 6, 2026
pull Bot pushed a commit to soitun/hermes-webui that referenced this pull request May 6, 2026
…p + test-isolation fix

Constituent PRs:
- nesquena#1725 (@Michaelyklam) — simplify compact Activity row summary
- nesquena#1726 (@Michaelyklam) — delegate generic provider catalogs to Hermes CLI (slice of nesquena#1240)
- nesquena#1727 (@Michaelyklam) — link Claude Code OAuth in onboarding (closes nesquena#1362)
- nesquena#1728 (@starship-s) — preserve profile context when starting chats
- nesquena#1729 (@Michaelyklam) — persist compact Activity disclosure state
- nesquena#1730 (@Michaelyklam) — prevent sticky sidebar hover drag state
- nesquena#1732 (@Sanjays2402) — unpin scroll on small upward motion during streaming (closes nesquena#1731)

Plus 2 in-stage absorbed fixes:
- test-isolation fix: monkeypatch.setattr(config, 'cfg', X) survives PR nesquena#1728's
  path/mtime-aware get_config() reload. Mandatory before tag (Opus stage-302).
- Opus SHOULD-FIX #1: _lastScrollTop reset on session switch (nesquena#1732 follow-up).

Tests: 4537 → 4584 passing (+47). 0 regressions. Full suite ~128s. Stably green.

Pre-release verification:
- All 7 PRs CI-green individually + rebased onto master
- pytest 4584 passed, 0 failed (multiple runs)
- node -c clean on all 4 modified .js files
- 11/11 browser API endpoints PASS on isolated port 8789
- 20 QA tests via webui_qa_agent.sh PASS
- Opus advisor: SHIP, 5/5 verification clean, 0 MUST-FIX, 1 SHOULD-FIX absorbed
  (_lastScrollTop reset), 1 SHOULD-FIX deferred (nesquena#1736 — _clear_anthropic_env_values
  race, onboarding-time-only)

Closes nesquena#1362, nesquena#1731.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants