Skip to content

feat(app): search sessions by title from the home list - #1783

Open
f-liva wants to merge 3 commits into
slopus:mainfrom
f-liva:pr/session-search
Open

f-liva wants to merge 3 commits into
slopus:mainfrom
f-liva:pr/session-search

Conversation

@f-liva

@f-liva f-liva commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

What

Search sessions by title from the home list. A search icon sits next to New session in the sidebar (and beside + in the phone/web header); it toggles a text box that filters the list live. Opening the search also pages in every session older than the 150 the app boots with, so the filter covers the whole account, not just the last week.

Why

The web app had no way to find a session other than scrolling. With a few hundred sessions the list is unusable as a lookup, and the boot fetch's 150-row cap (GET /v1/sessions, take: 150) hides anything older entirely: on one real account 685 of 835 sessions were unreachable from the UI.

Search has to be client-side: session metadata (where the title lives) is end-to-end encrypted — the CLI encrypts it before update-metadata, the server stores an opaque blob — so the server cannot index or match titles. The filter runs over rows the client has already decrypted.

Change

  • useVisibleSessionListViewData(searchQuery?) — case-insensitive substring match on the row title (session.name: agent summary or bot name) with the working path as fallback, because untitled sessions all render as "New chat" and only the path tells them apart. Applies to flat rows, project cards, bots and the active-sessions group; the existing archive rule still applies; emptied headers and cards drop out.
  • SessionSearchInput + sessionSearchStore (zustand) — the box, and open/query state shared between the icon and the list without a prop chain (same pattern as homeDockFocus). Closing clears the query so the list never stays filtered behind a hidden input. The archive toggle hides while a query is active (its click cannot bring anything back under a filter).
  • SessionsListWrapper owns the box across every list state (populated, empty account, no results). SessionsList accepts the query as a controlled prop and falls back to the store for direct mounts (MainView phone layout).
  • sync.loadAllSessionsForSearch() — on first open, pages /v2/sessions (cursor, 200/page) to the end and ingests only ids not already in the store; once per app run; a failed page lets the next open retry. The decrypt+apply path is extracted from fetchSessions into ingestSessions so both callers share it. The boot fetch is unchanged. A spinner sits in the box while history pages in.
  • i18n: sessionsFilter.searchPlaceholder / noResultsPlaceholder in all 10 locales.

Three commits: filter + box, header icon, on-demand history. pnpm typecheck clean.

Verified

  • 15 unit tests on the filter (useVisibleSessionListViewData.test.ts: title/path match, project cards, bots, headers, archive interaction) and 4 on the history loader (sync.searchHistory.test.ts: cursor to the end, skipping known ids, single run, retry after a failed page).
  • Built and deployed to a self-hosted web instance: icon placement, live filtering, no-results state, close-clears-query, and history load (server cap temporarily lowered to 1 to exercise the loader) all checked in the browser.

Note

Message content search is out of scope for the same E2EE reason and would require decrypting every session's history client-side. Rebases cleanly on top of #1772 (chunked ingest) if that lands first: ingestSessions is the natural seam.

🤖 Generated with Claude Code

Federico Liva and others added 3 commits September 16, 2026 10:51
Session metadata is E2EE, so the server cannot index titles; the filter
runs client-side over rows the app has already decrypted. Matches the
row title with the working path as fallback (untitled sessions all share
"New chat"), case-insensitive substring.

The search box is extracted into SessionSearchInput and stays mounted in
every list state — populated, empty account, no-results — with the query
owned by SessionsListWrapper and passed down as a controlled prop.
Archive toggle hides while a search is active (its click cannot bring
anything back under a filter).

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
The search box no longer sits permanently above the list. A search icon
next to "New session" (sidebar top controls) and beside "+" (phone/web
header) toggles it. State lives in a small zustand store so the icon and
the list share it without a prop chain; closing the box clears the query
so the list never stays filtered behind a hidden input.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
The boot fetch stops at the server's 150 most recent sessions, so the
search silently missed everything older (on a real account: 685 of 835
sessions, anything before the last week). Opening the search now pages
/v2/sessions to the end and merges the rows the boot fetch did not bring
in. The decrypt+apply path is extracted from fetchSessions into
ingestSessions so both callers share it.

Runs only on demand and once per app run — the list itself still boots
at 150. A spinner sits in the box while history pages in; a failed page
lets the next open retry instead of pinning the failure.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

This branch has not been deployed

No deployments
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