Skip to content

fix(web): chat load timeouts/retry, Show all sessions, deterministic All Chats search (#170, #150, #145, #275) - #455

Merged
edspencer merged 7 commits into
mainfrom
fix/web-bug-cluster
Aug 13, 2026
Merged

fix(web): chat load timeouts/retry, Show all sessions, deterministic All Chats search (#170, #150, #145, #275)#455
edspencer merged 7 commits into
mainfrom
fix/web-bug-cluster

Conversation

@edspencer

@edspencer edspencer commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Fixes a cluster of long-open @herdctl/web bugs. One commit per issue, each independently reviewable.

Commit Issue
fix(web): time out, retry, and correctly empty-state chat loads #170
fix(web): expand a directory group beyond the first 10 sessions #150
test(web): lock in the collapse-during-search fix #145
fix(web): make the All Chats no-results state deterministic #275
fix(web): attach WebSocket listeners before awaiting fleet status #275
fix(web): give UI-test agents a non-temp working directory #275

#170 — ChatView had no timeout, no retry, and a misleading empty state

All three gaps in the issue were still present. Fixed as proposed, plus the ad hoc session path which had the same problems:

  • fetchChatMessages and fetchAdhocChatMessages now run under a 15s AbortController and report Timed out loading messages after 15s. Previously a hung call left the feed on "Loading messages..." forever.
  • MessageFeed takes a sessionId. An existing session with zero messages now shows "No messages found for this session" instead of "Send a message to start the conversation"; genuinely new chats are unchanged.
  • Retry in the error banner and in the empty state, in both ChatView and AdhocChatView. On failure the active session is kept so retry knows what to re-fetch.

#150 — "Show all" never expanded past 10 sessions

Still a live bug on main. Adds the showAll flag from #161, with two corrections that PR needed:

  • fix: Show All button in DirectoryGroup now properly expands sessions #161 hid the button as soon as showAll was set, which caps a group at the first server page (a 100-session group would stop at 60). The button now stays as Load more (N remaining) while the server has more.
  • loadMoreGroupSessions is awaited with a disabled/loading button, so the click isn't a no-op-looking dead control.

#161 can't be applied as-is (the file moved its search helper to lib/session-utils in #160), so its approach is reimplemented and credited in the commit message. Recommend closing #161 as superseded.

#145 — groups re-expanded on toggle during search

Already fixed on mainexpandedGroups was removed from the effect deps and a biome-ignore added. Nothing locked it in, so this adds a component test that searches, collapses a group, and asserts it stays collapsed. Verified it fails (infinite effect loop) when the dep is restored.

#275 — two CI-fragile test-ui specs

Both turned out to have real causes. Neither is fixed by loosening assertions; both specs are un-skipped and now pass in CI (28/28, no skips).

1. Raw ws probe — a server bug, not a flaky test. WebSocketHandler.handleConnection awaited getFleetStatus() before calling socket.on("message", ...), so any frame a client sent the instant its socket opened arrived at a listener-less socket and was silently dropped (ws does not buffer). On a cold or loaded server the window is wide enough to hit for real, and it affects any early client frame — the dashboard keepalive ping, subscribe, chat:send — not just the probe. Listeners are now attached synchronously before the first await. pong may consequently arrive before fleet:status, so the probe waits for both rather than assuming an ordering. It now passes in CI in ~525ms (previously timed out at 30s).

2. All Chats no-results — two layered causes.

Product: groupMatchesQuery kept a group whose directory path or agent name matched, but DirectoryGroup then filtered its sessions by session fields only — so the group survived with zero rows and rendered its own "No sessions match your search". Searching a directory name produced a list of empty groups instead of results. Unified in lib/session-utils: a group whose header matches shows all its sessions; a group matching only via sessions shows just those; a group matching neither is dropped entirely, so the single top-level "No matching sessions" is the only no-results message.

Test harness (found by pushing this PR and reading the CI failure): core's session discovery deliberately skips temp paths — isTempDirectory in state/session-discovery.ts filters /tmp/, /private/tmp/, /var/folders/ and os.tmpdir() — and the harness put every agent working directory under mkdtemp(tmpdir()). A harness-seeded session could therefore never appear in the All Chats listing, on any machine. That is the real source of the environment divergence:

  • a completed chat appears in the All Chats directory listing asserted on the text talker, which also renders in the layout sidebar. It passed everywhere while proving nothing.
  • The no-results spec passed on a dev machine only because the developer's own ~/.claude groups were present and all filtered out by the impossible query. On a clean CI runner there were no groups at all, so the page sat on its base empty state.

Only the agent workspaces move out of the temp root (to an alphanumeric-only dir under $HOME, removed on teardown); config and state stay in tmpRoot. The harness now exposes agentEncodedPath (core's exact grouping key) and agentDisplayWorkdir (the lossy decode the UI renders) so specs assert on a group precisely instead of on an ambiguous agent name.


Verification

  • pnpm typecheck ✅ · pnpm build ✅ · Lint & Format ✅ · Unit Tests ✅ · Build + Smoke ✅ · Web UI Tests ✅ (28 passed, 0 skipped)
  • New regression tests: DirectoryGroup.test.tsx, AllChatsPage.test.tsx, MessageFeed.test.tsx, chat-slice.test.ts, plus a ws-handler test that sends a message while the fleet-status promise is still pending. @herdctl/web is at 195 passing unit tests (was 141).
  • Two CI reruns were needed for unrelated pre-existing flakes in @herdctl/core / herdctl (5s-timeout tests in hooks/__tests__/hook-executor.test.ts, scheduler/__tests__/scheduler.test.ts, cli/src/__tests__/smoke.test.ts — different tests each run). Nothing in this PR touches those packages; worth a separate look at test timeouts on the CI runner.

Changeset: @herdctl/web patch.

🤖 Generated with Claude Code

HomeLab Agent and others added 6 commits August 13, 2026 08:45
Loading an existing chat session had three gaps:

- `fetchChatMessages` (and the ad hoc `fetchSessionByPath` equivalent) had no
  timeout or abort, so a hung API call — proxy timeout, huge transcript, dead
  server — left the feed on "Loading messages..." forever, with a page reload
  as the only escape. Both now run under a 15s AbortController and report a
  clear "Timed out loading messages" error.
- An *existing* session that loaded zero messages showed "Send a message to
  start the conversation", which reads as "this chat is new" when in fact the
  transcript could not be read. `MessageFeed` now takes the session id and
  shows "No messages found for this session" for that case, while genuinely
  new chats keep the original prompt.
- There was no way to retry a failed load. Both the error banner and the
  empty state now offer a Retry that re-runs the fetch in place.

Closes #170

Co-Authored-By: Claude <noreply@anthropic.com>
`sessionsToShow` was hardcoded to `filteredSessions.slice(0,
INITIAL_SESSIONS_SHOWN)` with no state tracking whether the user had asked for
more. Clicking "Show all" called `loadMoreGroupSessions`, which fetched the
next page from the server and updated the store — but the render slice never
moved, so a directory group could never display more than 10 sessions no
matter how many were loaded.

Add a local `showAll` flag: the click reveals every already-loaded session
immediately and only hits the server when the group is partially loaded. The
button stays visible (as "Load more (N remaining)") while the server still has
more, so groups larger than one page can be paged through, and is disabled
while a fetch is in flight.

Builds on the approach in #161 by @edspencer, which could not be applied
directly: the file has since moved its search helper to lib/session-utils, and
that version hid the button as soon as `showAll` was set, capping a group at
the first server page.

Closes #150

Co-Authored-By: Claude <noreply@anthropic.com>
The reported bug — collapsing a directory group while a search was active
immediately re-expanded it, because `expandedGroups` sat in the auto-expand
effect's dependency array and `toggleAllChatsGroup` always produces a fresh
`Set` — was already fixed on main, but nothing stopped it regressing.

Add a component test that searches, collapses a group, and asserts it stays
collapsed. Verified it fails (in fact, renders an infinite effect loop) when
`expandedGroups` is put back in the dependency array.

Closes #145

Co-Authored-By: Claude <noreply@anthropic.com>
The "no results" journey rendered one of two different messages depending on
the host machine's session history, which is why the Playwright spec had to
accept either and was ultimately skipped in CI.

The cause is a real inconsistency, not just a test problem. `groupMatchesQuery`
kept a group whose *directory path or agent name* matched the query, but
`DirectoryGroup` then filtered that group's sessions by session fields only —
so the group survived with zero rows and rendered its own "No sessions match
your search". Searching for a directory name therefore produced a list of
empty groups instead of results.

Resolve it in one place:

- Move `groupMatchesQuery` into lib/session-utils alongside a new
  `groupHeaderMatchesQuery`, so the page and the group agree on what matched.
- A group whose header matched shows ALL of its sessions (they are all
  relevant); a group that matched only via its sessions shows just those.
- A group that matches neither is dropped entirely, so the single top-level
  "No matching sessions" state is the only no-results message users can see.

The spec is un-skipped in CI and now waits for the seeded session to appear in
`/api/chat/all` before searching. It previously waited on the text "talker",
which also renders in the layout sidebar — so it could proceed with an empty
group list and never render any no-results message at all.

Refs #275

Co-Authored-By: Claude <noreply@anthropic.com>
The raw ping/pong browser probe timed out consistently in CI even at 30s while
every higher-level WebSocket journey passed. That was a server bug, not a
flaky test.

`WebSocketHandler.handleConnection` awaited `fleetManager.getFleetStatus()` to
send the initial snapshot, and only afterwards called `socket.on("message",
...)`. Any frame a client sent the instant its socket opened arrived at a
socket with no "message" listener and was silently dropped. On a cold or
loaded server — a freshly booted fleet, a CI runner — that window is wide
enough to hit for real, and it affects any early client frame (the dashboard's
keepalive ping, subscribe, chat:send), not just the probe.

Attach the message/close/error listeners synchronously, before the first
await. A consequence is that `pong` may now arrive before `fleet:status`, so
the probe waits for both instead of assuming an ordering, retries its
connection, and is un-skipped in CI.

Refs #275

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

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@edspencer, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d4e05563-6d1b-4526-ac0a-9212f93af47f

📥 Commits

Reviewing files that changed from the base of the PR and between 03b4ccc and 2879b05.

📒 Files selected for processing (18)
  • .changeset/lucky-pugs-attack.md
  • packages/web/src/client/src/components/all-chats/AllChatsPage.tsx
  • packages/web/src/client/src/components/all-chats/DirectoryGroup.tsx
  • packages/web/src/client/src/components/all-chats/__tests__/AllChatsPage.test.tsx
  • packages/web/src/client/src/components/all-chats/__tests__/DirectoryGroup.test.tsx
  • packages/web/src/client/src/components/chat/AdhocChatView.tsx
  • packages/web/src/client/src/components/chat/ChatView.tsx
  • packages/web/src/client/src/components/chat/MessageFeed.tsx
  • packages/web/src/client/src/components/chat/__tests__/MessageFeed.test.tsx
  • packages/web/src/client/src/lib/api.ts
  • packages/web/src/client/src/lib/session-utils.ts
  • packages/web/src/client/src/store/__tests__/chat-slice.test.ts
  • packages/web/src/client/src/store/chat-slice.ts
  • packages/web/src/server/__tests__/ws-handler.test.ts
  • packages/web/src/server/ws/handler.ts
  • packages/web/test-ui/harness.ts
  • packages/web/test-ui/tests/00-ws-probe.spec.ts
  • packages/web/test-ui/tests/07-sessions-and-errors.spec.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 13, 2026

Copy link
Copy Markdown

Deploying herdctl with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2879b05
Status: ✅  Deploy successful!
Preview URL: https://240b76ee.herdctl.pages.dev
Branch Preview URL: https://fix-web-bug-cluster.herdctl.pages.dev

View logs

CI proved the All Chats specs were never testing what they claimed. Core's
session discovery deliberately skips temp paths — `isTempDirectory` in
`state/session-discovery.ts` filters `/tmp/`, `/private/tmp/`, `/var/folders/`
and `os.tmpdir()` — and the harness put every agent's working directory under
`mkdtemp(tmpdir())`. A harness-seeded session therefore could NEVER appear in
the machine-wide All Chats listing, on any machine.

That is the real reason the two specs diverged between environments:

- "a completed chat appears in the All Chats directory listing" asserted on the
  text "talker", which also renders in the layout sidebar. It passed everywhere
  while proving nothing about the session list.
- The no-results spec passed on a developer machine only because the developer's
  own ~/.claude session groups were present and all filtered out by the
  impossible query. On a clean CI runner there were no groups at all, so the
  page sat on its base empty state and rendered no no-results message.

Move only the agent workspaces out of the temp root, into an alphanumeric-only
directory under $HOME (removed on teardown). Config, state and scratch stay in
tmpRoot. The names avoid non-alphanumerics because `encodePathForCli` maps every
such character to "-" and the listing decodes it back by turning every "-" into
"/", so anything else round-trips lossily.

The harness now also exposes `agentEncodedPath` (core's exact grouping key) and
`agentDisplayWorkdir` (the lossy decode the UI actually renders), so the specs
can assert on a group precisely instead of on an ambiguous agent name.

Refs #275

Co-Authored-By: Claude <noreply@anthropic.com>
@edspencer
edspencer merged commit 81e9c72 into main Aug 13, 2026
12 of 13 checks passed
@edspencer
edspencer deleted the fix/web-bug-cluster branch August 13, 2026 13:08
@github-actions github-actions Bot mentioned this pull request Aug 13, 2026
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.

1 participant