hermes - 💡(How to fix) Fix fix(dashboard): ChatPage clears header action buttons on ALL pages, not just Sessions

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Root Cause

ChatPage is mounted persistently outside <Routes> (so the PTY / WebSocket survive tab switches). Even when the user is on /cron, /models, or /sessions, ChatPage is still in the React tree with isActive=false.

Its useEffect for the header button actively calls setEnd(null) on every render when !isActive, clobbering the action buttons that the current page had just placed in the same slot.

The trigger for the ~3–4 second timing is the plugin load delay: usePlugins() fetches manifests asynchronously; once the promise settles, the entire tree re-renders, ChatPage's effect fires, and the header slot is wiped.

Fix Action

Fix

Change the effect to only register when Chat should own the slot, and let cleanup run only when it actually did:

// Before (buggy): actively clears other pages' content
useEffect(() => {
  if (!isActive) {
    setEnd(null);  // ← clears Cron CREATE button!
    return;
  }
  if (!narrow) {
    setEnd(null);  // ← also clears!
    return;
  }
  setEnd(<Button ... />);
  return () => setEnd(null);
}, [...]);

// After (fixed): only cleanup on unmount / when Chat owned the slot
useEffect(() => {
  if (!isActive || !narrow) return;  // just don't register
  setEnd(<Button ... />);
  return () => setEnd(null);  // cleanup only when Chat owned the slot
}, [...]);

PageHeaderProvider already clears all slots on pathname change via useLayoutEffect, so ChatPage's active clearing is redundant and harmful.

Code Example

// Before (buggy): actively clears other pages' content
useEffect(() => {
  if (!isActive) {
    setEnd(null);  // ← clears Cron CREATE button!
    return;
  }
  if (!narrow) {
    setEnd(null);  // ← also clears!
    return;
  }
  setEnd(<Button ... />);
  return () => setEnd(null);
}, [...]);

// After (fixed): only cleanup on unmount / when Chat owned the slot
useEffect(() => {
  if (!isActive || !narrow) return;  // just don't register
  setEnd(<Button ... />);
  return () => setEnd(null);  // cleanup only when Chat owned the slot
}, [...]);
RAW_BUFFERClick to expand / collapse

Problem

When the dashboard is started with embedded TUI chat enabled (hermes dashboard --tui), navigating to any non-chat page causes the page-specific header action buttons to disappear after a few seconds.

This affects at least:

  • Cron page — the "CREATE" button disappears
  • Models page — the "7D / 30D / 90D" filter buttons disappears

Screenshots

Cron page — normal vs buggy

Normal

<img width="1667" height="60" alt="Image" src="https://github.com/user-attachments/assets/913e8c61-7161-43b8-b230-a2146a9ca0fe" />

Buggy

<img width="1672" height="98" alt="Image" src="https://github.com/user-attachments/assets/e5426dcb-e556-4303-9755-8c1208294081" />

Models page — normal vs buggy

Normal

<img width="1673" height="58" alt="Image" src="https://github.com/user-attachments/assets/1dd15817-620f-47a4-b7cc-5d03fde27978" />

Buggy

<img width="1657" height="48" alt="Image" src="https://github.com/user-attachments/assets/0eed3d47-d99f-4ebb-8939-1d55fe0c2127" />

Root Cause

ChatPage is mounted persistently outside <Routes> (so the PTY / WebSocket survive tab switches). Even when the user is on /cron, /models, or /sessions, ChatPage is still in the React tree with isActive=false.

Its useEffect for the header button actively calls setEnd(null) on every render when !isActive, clobbering the action buttons that the current page had just placed in the same slot.

The trigger for the ~3–4 second timing is the plugin load delay: usePlugins() fetches manifests asynchronously; once the promise settles, the entire tree re-renders, ChatPage's effect fires, and the header slot is wiped.

Fix

Change the effect to only register when Chat should own the slot, and let cleanup run only when it actually did:

// Before (buggy): actively clears other pages' content
useEffect(() => {
  if (!isActive) {
    setEnd(null);  // ← clears Cron CREATE button!
    return;
  }
  if (!narrow) {
    setEnd(null);  // ← also clears!
    return;
  }
  setEnd(<Button ... />);
  return () => setEnd(null);
}, [...]);

// After (fixed): only cleanup on unmount / when Chat owned the slot
useEffect(() => {
  if (!isActive || !narrow) return;  // just don't register
  setEnd(<Button ... />);
  return () => setEnd(null);  // cleanup only when Chat owned the slot
}, [...]);

PageHeaderProvider already clears all slots on pathname change via useLayoutEffect, so ChatPage's active clearing is redundant and harmful.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING