From d1f2aeaa4476b82a308c8c86cd7899e93976912f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Thu, 17 Sep 2026 23:47:12 +0200 Subject: [PATCH] feat(sessions): show independent results in search mode Replace hierarchical search and worktree filtering with flat per-session results. Add a localized Show subsessions switch, disabled by default, so a matching child can appear without an unrelated parent. Each result keeps its own native directory, activity timestamp, worktree badge and selection identity; closing search restores normal browsing hierarchy. Consume native search result identities directly instead of hydrating ancestors just to render a child. Preserve request-authority and deletion fences, and exhaust normal pagination for unqueried flat filters. This supersedes the root-only filter adjustment from 3921ba08. Validated UI typecheck/build, 14 session-tree cases, 65 request-authority cases and a real-component browser regression covering toggle, sorts, combined filters, badges, selection and missing parents. Installed-renderer verification used an existing cross-worktree child and confirmed flat results plus hierarchy restoration without moving sessions. Documented the desktop search wait and retained large-catalogue latency limitation. --- AGENTS.md | 1 + dev-docs/WORKTREE_SESSION_PLACEMENT.md | 22 +++++ packages/ui/src/components/session-list.tsx | 38 ++++++--- .../ui/src/lib/i18n/messages/de/session.ts | 1 + .../ui/src/lib/i18n/messages/en/session.ts | 1 + .../ui/src/lib/i18n/messages/es/session.ts | 1 + .../ui/src/lib/i18n/messages/fr/session.ts | 1 + .../ui/src/lib/i18n/messages/he/session.ts | 1 + .../ui/src/lib/i18n/messages/ja/session.ts | 1 + .../ui/src/lib/i18n/messages/ne/session.ts | 1 + .../ui/src/lib/i18n/messages/ru/session.ts | 1 + .../ui/src/lib/i18n/messages/tr/session.ts | 1 + .../src/lib/i18n/messages/zh-Hans/session.ts | 1 + packages/ui/src/stores/session-api.ts | 12 --- .../stores/session-request-authority.test.ts | 6 +- packages/ui/src/stores/session-state.ts | 25 ++---- packages/ui/src/stores/session-tree.test.ts | 48 +++++++++++ packages/ui/src/stores/session-tree.ts | 19 ++++- packages/ui/src/stores/sessions.ts | 4 +- .../tests/browser/fixtures/session-search.tsx | 61 ++++++++++++++ .../ui/tests/browser/session-search.test.ts | 81 +++++++++++++++++++ 21 files changed, 276 insertions(+), 51 deletions(-) create mode 100644 packages/ui/tests/browser/fixtures/session-search.tsx create mode 100644 packages/ui/tests/browser/session-search.test.ts diff --git a/AGENTS.md b/AGENTS.md index 5c32a4fc9..37d83ae9c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,7 @@ - Keep agent, model, and thinking controls in the composer footer via `PromptContextControls`; adapt that footer with the named `prompt-composer` container rather than viewport-only breakpoints. - Session rows keep actions inline until their measured title, badges, and controls no longer fit. Keep responsive action styles in `styles/components/session-row-actions.css`; hidden inline controls remain measurable but inert, and an open overflow menu stays mounted until dismissal. - Session hierarchy geometry lives in `styles/components/session-tree.css`; connector axes follow the parent expander at every depth, including selection mode, RTL and touch layouts. +- Session search/filter mode uses flat per-session results with an optional subsession switch; filters, sorting, worktree badges and selection use each result's own identity. Normal browsing retains the session hierarchy. - Never use rounded corners in UI styling; keep corners square unless the user explicitly requests otherwise for a specific change. - Explicit round exceptions: Yolo and MCP switches (shared `styles/components/switches.css` geometry), overlay drawer navigation buttons, and floating message scroll buttons. Other chrome remains square. - Tags and numeric/context/token labels also use rounded geometry via `--chip-radius` (`--pill-radius` is an alias). Register badge variants in `styles/components/badges.css`; use `.badge-shape` for utility-styled labels rather than adding a local radius. diff --git a/dev-docs/WORKTREE_SESSION_PLACEMENT.md b/dev-docs/WORKTREE_SESSION_PLACEMENT.md index ab1052d38..d816c06e2 100644 --- a/dev-docs/WORKTREE_SESSION_PLACEMENT.md +++ b/dev-docs/WORKTREE_SESSION_PLACEMENT.md @@ -174,6 +174,28 @@ authorized workspace spelling before changing UI state. ### Final native/UI integration (2026-09-17) +Follow-up: search/filter mode presents independent flat rows. The "Show subsessions" +switch (off by default) includes children without requiring their parents in the +results. Text and worktree filters must match the same session; activity, names and +worktree labels also sort each session independently. Selection targets only the +displayed matches. Server search results no longer require ancestor hydration. +Closing search restores normal hierarchy and ignores its worktree filter. This +supersedes the temporary root-only worktree-filter fix in `3921ba08`. + +The real-component browser regression covers the switch, cross-worktree children, +text plus directory filters, independent badges, bulk selection, direct child +navigation, results without loaded ancestors and returning to normal hierarchy. + +The rebuilt UI was also loaded in the installed Windows Tauri renderer. An +existing child, "Analyze automation update", was verified through native reads +in `D:\CodeNomad` while its parent remained in `pr649-final`. With its title and +the Workspace filter selected, the child appeared alone only when subsessions +were enabled. Selecting the parent's checkout hid it; closing search restored +the hierarchy and preserved the active parent conversation. A first desktop wait +of 30 seconds expired; the completed repeat used a 90-second bound. Directory +search still traverses the local worktree catalogue, so this is not a latency +guarantee. No native session locations were changed by this check. + Merged `dev@e47e01c6` (PR #697) into this branch. Discovery and canonical `server.status()` adaptation are now inherited from that independently merged change. The native worktree/family fixture passed again against isolated official diff --git a/packages/ui/src/components/session-list.tsx b/packages/ui/src/components/session-list.tsx index c86414cc1..22ec22802 100644 --- a/packages/ui/src/components/session-list.tsx +++ b/packages/ui/src/components/session-list.tsx @@ -32,11 +32,11 @@ import { clearSessionSearch, fetchSessions, getSessionSearchQuery, - getSessionSearchThreads, + getSessionSearchSessions, isSessionSearchLoading, } from "../stores/sessions" import { getGitRepoStatus, getWorktreeSlugForParentSession, getWorktrees } from "../stores/worktrees" -import { collectSessionThreadIds, findSessionThread, flattenVisibleSessionThreads, projectSessionFamilies, sortSessionIdsDeepestFirst, type SessionFamilySort } from "../stores/session-tree" +import { collectSessionThreadIds, findSessionThread, flattenVisibleSessionThreads, projectSessionFamilies, projectSessionSearchResults, sortSessionIdsDeepestFirst, type SessionFamilySort } from "../stores/session-tree" import { normalizeSessionDirectory } from "../stores/session-list-options" import { getLogger } from "../lib/logger" import { copyToClipboard } from "../lib/clipboard" @@ -72,6 +72,7 @@ const SessionList: Component = (props) => { const [filterQuery, setFilterQuery] = createSignal("") const [sortBy, setSortBy] = createSignal("activity") const [worktreeDirectory, setWorktreeDirectory] = createSignal("") + const [includeSubsessions, setIncludeSubsessions] = createSignal(false) const normalizedQuery = createMemo(() => (props.enableFilterBar ? filterQuery().trim().toLowerCase() : "")) let failedSortExhaustion: string | undefined @@ -133,7 +134,7 @@ const SessionList: Component = (props) => { createEffect(() => { const sort = sortBy() const key = `${props.instanceId}:${sort}` - if (sort === "activity") { + if (sort === "activity" && !props.enableFilterBar) { failedSortExhaustion = undefined return } @@ -222,20 +223,26 @@ const SessionList: Component = (props) => { const filteredThreads = createMemo(() => { const query = normalizedQuery() - const searchThreads = query && getSessionSearchQuery(props.instanceId) === query && !isSessionSearchLoading(props.instanceId) - ? getSessionSearchThreads(props.instanceId) - : props.threads + const hasSearchResults = query && getSessionSearchQuery(props.instanceId) === query && !isSessionSearchLoading(props.instanceId) const worktrees = getWorktrees(props.instanceId) const getWorktreeLabel = (directory: string) => { const normalized = normalizeSessionDirectory(directory) const worktree = worktrees.find((candidate) => normalizeSessionDirectory(candidate.serviceDirectory ?? candidate.directory) === normalized) return worktree?.kind === "root" ? t("sessionList.worktree.workspace") : worktree?.label ?? worktree?.slug ?? directory } - return projectSessionFamilies(searchThreads, { + if (!props.enableFilterBar) return projectSessionFamilies(props.threads, { sort: sortBy(), getWorktreeLabel }) + const instanceSessions = sessionStateSessions().get(props.instanceId) + const candidates = hasSearchResults ? getSessionSearchSessions(props.instanceId) + : collectSessionThreadIds(props.threads).flatMap(id => { + const session = instanceSessions?.get(id) + return session ? [session] : [] + }) + return projectSessionSearchResults(candidates, { sort: sortBy(), worktreeDirectory: worktreeDirectory(), + includeSubsessions: includeSubsessions(), getWorktreeLabel, - ...(query && searchThreads === props.threads + ...(query && !hasSearchResults ? { matchesSession: (session) => sessionMatchesQuery(session.id, query) } : {}), }) @@ -557,6 +564,7 @@ const SessionList: Component = (props) => { }> = (rowProps) => { const sessionId = () => rowProps.session.id const isChild = () => rowProps.depth > 0 + const isSubsession = () => Boolean(rowProps.session.parentId) const worktreeSlug = createMemo(() => { if (isChild()) return "" @@ -686,7 +694,7 @@ const SessionList: Component = (props) => { return (
= (props) => { title={title()} aria-current={isActive() ? "true" : undefined} > -
+ + 0}>