Skip to content

fix: Show All button in DirectoryGroup now properly expands sessions - #161

Open
edspencer wants to merge 2 commits into
mainfrom
fix/150-show-all-button
Open

fix: Show All button in DirectoryGroup now properly expands sessions#161
edspencer wants to merge 2 commits into
mainfrom
fix/150-show-all-button

Conversation

@edspencer

@edspencer edspencer commented Feb 26, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #150

  • Added showAll state to track whether user clicked "Show all" button
  • When showAll is true, component renders all filteredSessions instead of slicing to INITIAL_SESSIONS_SHOWN
  • Button now shows all locally-loaded sessions first, then fetches more from server if hasMoreOnServer is true
  • Button is hidden after being clicked (when showAll is true)

Changes

  1. Import useState from React
  2. Add showAll state (defaults to false)
  3. Conditionally slice filteredSessions based on showAll state
  4. Update handleShowAll to set state and conditionally fetch from server
  5. Hide button when showAll is true

Test plan

  • Verify "Show All" button appears for directory groups with >10 sessions
  • Click "Show All" and verify all locally-loaded sessions are displayed
  • Verify button disappears after clicking
  • Verify additional sessions are fetched from server when needed
  • Verify no TypeScript errors

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Fixed the "Show All" button in the directory to properly expand beyond the initial view and display all available sessions
    • Now correctly reveals all locally-loaded sessions and automatically fetches additional sessions from the server when needed
    • The button is hidden once all sessions have been displayed

edspencer and others added 2 commits February 26, 2026 02:54
Fixes #150

- Add showAll state to track whether user clicked "Show all"
- When showAll is true, render all filteredSessions instead of slicing to INITIAL_SESSIONS_SHOWN
- Button now shows all locally-loaded sessions first, then fetches more from server if needed
- Hide button after it's been clicked (when showAll is true)

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

coderabbitai Bot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a changeset file documenting a patch release and modifies DirectoryGroup component to fix the non-functional "Show All" button. Introduces local showAll state to track user action, conditionally rendering all filtered sessions instead of a hardcoded 10-session limit, and triggering server fetch for additional sessions when needed.

Changes

Cohort / File(s) Summary
Changeset Documentation
.changeset/fix-show-all-button.md
Adds changeset file documenting patch release for @herdctl/web with description of "Show All" button fix.
DirectoryGroup Component
packages/web/src/client/src/components/all-chats/DirectoryGroup.tsx
Introduces showAll state via useState hook to track expansion state. Updates sessionsToShow logic to display all filtered sessions when showAll is true. Conditionally renders "Show All" button only when not expanded. Preserves server fetch logic on button click.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A button that couldn't quite show, kept ten sessions and wouldn't grow
Now showAll state does the trick, expanding sessions slick and quick
From local cache to server's store, your sessions appear—ten and more! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main fix: the Show All button now properly expands sessions in DirectoryGroup, which directly matches the changeset modifications.
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from issue #150: adds showAll state, renders full filteredSessions when showAll is true, implements conditional server fetching, and hides the button after expansion.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing issue #150: the changeset file, useState import addition, showAll state logic, and rendering changes are all necessary for the fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/150-show-all-button

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying herdctl with  Cloudflare Pages  Cloudflare Pages

Latest commit: dfc37c8
Status: ✅  Deploy successful!
Preview URL: https://3bd151bf.herdctl.pages.dev
Branch Preview URL: https://fix-150-show-all-button.herdctl.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/web/src/client/src/components/all-chats/DirectoryGroup.tsx (1)

65-65: Add an explicit boolean type for showAll state.

Line 65 currently relies on inference; prefer explicit typing for consistency with strict TS conventions.

Suggested diff
-  const [showAll, setShowAll] = useState(false);
+  const [showAll, setShowAll] = useState<boolean>(false);

As per coding guidelines, **/*.{ts,tsx}: Use strict TypeScript with explicit types.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/client/src/components/all-chats/DirectoryGroup.tsx` at line
65, The state declaration for showAll uses type inference; update it to an
explicit boolean type to satisfy strict TS rules by changing the useState call
that defines showAll and setShowAll (the useState import or invocation in
DirectoryGroup.tsx) to declare useState<boolean>(...) so showAll is explicitly
typed as boolean while preserving the initial value and existing usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/src/client/src/components/all-chats/DirectoryGroup.tsx`:
- Around line 80-85: handleShowAll currently calls loadMoreGroupSessions once
(which fetches limit:50 in all-chats-slice.ts) then hides the "Load more" button
via setShowAll(true), so groups with >50 remaining sessions become inaccessible;
update handleShowAll in DirectoryGroup.tsx to either (a) loop/await
loadMoreGroupSessions(group.encodedPath) until hasMoreOnServer is false so all
pages are fetched before hiding the button, or (b) keep the load-more control
visible (do not call setShowAll(true) or hide the button) and let the user
repeatedly invoke loadMoreGroupSessions while hasMoreOnServer remains true;
reference the symbols handleShowAll, loadMoreGroupSessions, and hasMoreOnServer
when making the change.

---

Nitpick comments:
In `@packages/web/src/client/src/components/all-chats/DirectoryGroup.tsx`:
- Line 65: The state declaration for showAll uses type inference; update it to
an explicit boolean type to satisfy strict TS rules by changing the useState
call that defines showAll and setShowAll (the useState import or invocation in
DirectoryGroup.tsx) to declare useState<boolean>(...) so showAll is explicitly
typed as boolean while preserving the initial value and existing usage.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9649c2 and dfc37c8.

📒 Files selected for processing (2)
  • .changeset/fix-show-all-button.md
  • packages/web/src/client/src/components/all-chats/DirectoryGroup.tsx

Comment on lines +80 to +85
// First, show all locally-loaded sessions
setShowAll(true);
// Then fetch more from server if there are any
if (hasMoreOnServer) {
loadMoreGroupSessions(group.encodedPath);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Searching for loadMoreGroupSessions definition and usage =="
rg -nP --type=ts --type=tsx -C4 '\bloadMoreGroupSessions\b'

echo
echo "== Checking DirectoryGroup.tsx content around lines 80-85 and 154 =="
fd -e tsx -e ts "DirectoryGroup" | head -5

Repository: edspencer/herdctl

Length of output: 168


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Finding DirectoryGroup.tsx =="
fd "DirectoryGroup" -e tsx -e ts

echo
echo "== Searching for loadMoreGroupSessions without type restriction =="
rg -n 'loadMoreGroupSessions' --max-count=20

echo
echo "== Checking pagination-related patterns =="
rg -n 'hasMore|nextCursor|offset|limit|page' | head -30

Repository: edspencer/herdctl

Length of output: 2340


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Reading all-chats-slice.ts loadMoreGroupSessions implementation =="
cat -n packages/web/src/client/src/store/all-chats-slice.ts | sed -n '100,150p'

echo
echo "== Reading DirectoryGroup.tsx around lines 80-85 and 154 =="
cat -n packages/web/src/client/src/components/all-chats/DirectoryGroup.tsx | sed -n '60,100p'

echo
echo "== Reading DirectoryGroup.tsx around line 154 =="
cat -n packages/web/src/client/src/components/all-chats/DirectoryGroup.tsx | sed -n '145,165p'

Repository: edspencer/herdctl

Length of output: 4362


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Checking for any pagination continuation or hasMore tracking =="
grep -n "sessionCount\|sessions.length\|hasMore" packages/web/src/client/src/store/all-chats-slice.ts | head -20

echo
echo "== Check if there's re-fetching logic elsewhere =="
rg -n "loadMoreGroupSessions" packages/web/src/client/src/store/ -A5 -B5

echo
echo "== Check API response structure =="
rg -n "fetchDirectoryGroupSessions" packages/web/src/client/src/ -A10 | head -40

Repository: edspencer/herdctl

Length of output: 4916


Confirm pagination handles groups with >50 additional sessions.

loadMoreGroupSessions fetches with limit: 50 (all-chats-slice.ts:118), but handleShowAll (DirectoryGroup.tsx:79–85) calls it only once. If a group has more than 50 additional sessions beyond those already loaded, the button disappears after the first fetch (line 154), leaving remaining sessions inaccessible. Either loop until hasMoreOnServer is false, or preserve a visible "Load more" button when additional server data exists.

Also applies to: 154-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/client/src/components/all-chats/DirectoryGroup.tsx` around
lines 80 - 85, handleShowAll currently calls loadMoreGroupSessions once (which
fetches limit:50 in all-chats-slice.ts) then hides the "Load more" button via
setShowAll(true), so groups with >50 remaining sessions become inaccessible;
update handleShowAll in DirectoryGroup.tsx to either (a) loop/await
loadMoreGroupSessions(group.encodedPath) until hasMoreOnServer is false so all
pages are fetched before hiding the button, or (b) keep the load-more control
visible (do not call setShowAll(true) or hide the button) and let the user
repeatedly invoke loadMoreGroupSessions while hasMoreOnServer remains true;
reference the symbols handleShowAll, loadMoreGroupSessions, and hasMoreOnServer
when making the change.

@edspencer edspencer left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR fixes the "Show All" button in DirectoryGroup so that clicking it now renders all locally-loaded sessions (via new showAll state) instead of always slicing to the first 10 — resolving #150. It also fires a server fetch when more sessions exist remotely, and hides the button once showAll is set.

I read DirectoryGroup.tsx in full and traced loadMoreGroupSessions in store/all-chats-slice.ts (it fetches one page of limit: 50 at offset: group.sessions.length, merges results, and swallows errors internally so there's no unhandled-rejection risk from the un-awaited call). The local-display fix is correct. I found one correctness gap in how the button interacts with server pagination for large groups.


{/* Show more button */}
{(hasMoreLoaded || hasMoreOnServer) && (
{!showAll && (hasMoreLoaded || hasMoreOnServer) && (

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM · correctness — "Show all" cannot reveal all sessions when the group has more than one server page

loadMoreGroupSessions fetches only a single page of limit: 50 per call (see store/all-chats-slice.ts:117-120, offset: group.sessions.length). handleShowAll calls it exactly once and simultaneously sets showAll = true, and this line hides the button whenever showAll is true (!showAll && ...).

So for a group where sessionCount > sessions.length + 50 (e.g. 10 loaded locally, 200 total), one click fetches 50 more (60 visible) and then permanently hides the button — the remaining ~140 sessions can never be loaded, even though the button label promised "Show all 200 sessions". Groups with up to initial + 50 sessions work fine; larger ones don't fully expand.

One fix is to keep the button visible while the server still has more, so repeated clicks keep paging (each click just fetches the next 50). Since handleShowAll already only fetches when hasMoreOnServer, re-clicking after showAll is set simply loads the next page:

Suggested change
{!showAll && (hasMoreLoaded || hasMoreOnServer) && (
{((!showAll && hasMoreLoaded) || hasMoreOnServer) && (

@edspencer

Copy link
Copy Markdown
Owner Author

Reviewed 2 changed files (5 hunks); ran the verify pass; 1 finding.

@edspencer

Copy link
Copy Markdown
Owner Author

Assessment while working through #150 in #455.

The bug this PR fixes is still live on mainsessionsToShow is still filteredSessions.slice(0, INITIAL_SESSIONS_SHOWN) with no state, so a directory group can never render more than 10 sessions.

The approach here is right, and #455 uses it (local showAll flag; reveal locally-loaded sessions first, only fetch from the server when there is more). Two reasons it isn't cherry-picked as-is:

  1. It no longer applies. fix: deduplicate sessionMatchesQuery and add autoName search support #160 moved sessionMatchesQuery out of DirectoryGroup.tsx into lib/session-utils, so both hunks conflict on the import/context lines.
  2. Hiding the button on showAll caps the group at one server page. loadMoreGroupSessions fetches limit: 50 from offset: group.sessions.length, so a group with 100 sessions stops at 60 with no way to reach the rest. fix(web): chat load timeouts/retry, Show all sessions, deterministic All Chats search (#170, #150, #145, #275) #455 keeps the button visible as Load more (N remaining) while hasMoreOnServer, and disables it while a fetch is in flight.

#455 credits this PR in the commit message and adds regression tests (DirectoryGroup.test.tsx) covering both the local expansion and the server-paging path. Suggest closing this one as superseded once #455 lands.

edspencer added a commit that referenced this pull request Aug 13, 2026
…All Chats search (#170, #150, #145, #275) (#455)

* fix(web): time out, retry, and correctly empty-state chat loads (#170)

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>

* fix(web): expand a directory group beyond the first 10 sessions (#150)

`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>

* test(web): lock in the collapse-during-search fix (#145)

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>

* fix(web): make the All Chats no-results state deterministic (#275)

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>

* fix(web): attach WebSocket listeners before awaiting fleet status (#275)

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>

* chore: changeset for the @herdctl/web bug fixes

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

* fix(web): give UI-test agents a non-temp working directory (#275)

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>

---------

Co-authored-by: HomeLab Agent <homelab-infra@valfenda.net>
Co-authored-by: Claude <noreply@anthropic.com>
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.

"Show All" button in DirectoryGroup never expands beyond INITIAL_SESSIONS_SHOWN

1 participant