fix(web): align session search contracts
This commit is contained in:
@@ -213,6 +213,10 @@
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.searchTree > [role='treeitem'] + [role='treeitem'] {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.searchStatus,
|
||||
.searchWarning {
|
||||
padding: 10px 12px;
|
||||
|
||||
@@ -261,33 +261,37 @@ function SearchResults({
|
||||
workspaces,
|
||||
query,
|
||||
remote,
|
||||
resultLimit,
|
||||
}: Pick<SessionTreeProps, 'useSessions' | 'open'> & {
|
||||
workspaces: readonly WorkspaceView[]
|
||||
query: string
|
||||
remote: RemoteSearchState
|
||||
resultLimit: number
|
||||
}) {
|
||||
const list = useSessions((s) => s)
|
||||
const currentRemote = remote.query === query
|
||||
? remote
|
||||
: { query, status: 'loading' as const, items: [], hasMore: false }
|
||||
const results = useMemo(
|
||||
() => deriveSearchResults(list, workspaces, query, currentRemote),
|
||||
[list, workspaces, query, currentRemote],
|
||||
() => deriveSearchResults(list, workspaces, query, currentRemote, resultLimit),
|
||||
[list, workspaces, query, currentRemote, resultLimit],
|
||||
)
|
||||
const pending = currentRemote.status === 'loading'
|
||||
const failed = currentRemote.status === 'error'
|
||||
|
||||
return (
|
||||
<div className={clsx(css.treeBody, css.wide)}>
|
||||
<div className={css.list} role="tree" aria-label="Search results">
|
||||
{results.items.map(result => (
|
||||
<SearchResultItem
|
||||
key={result.id}
|
||||
result={result}
|
||||
currentId={list.current}
|
||||
onOpen={open}
|
||||
/>
|
||||
))}
|
||||
<div className={css.list}>
|
||||
<div className={css.searchTree} role="tree" aria-label="Search results">
|
||||
{results.items.map(result => (
|
||||
<SearchResultItem
|
||||
key={result.id}
|
||||
result={result}
|
||||
currentId={list.current}
|
||||
onOpen={open}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{pending && (
|
||||
<div className={css.searchStatus} role="status">Searching session history…</div>
|
||||
)}
|
||||
@@ -300,7 +304,9 @@ function SearchResults({
|
||||
<div className={css.empty}>No matching sessions</div>
|
||||
)}
|
||||
{results.hasMore && (
|
||||
<div className={css.searchStatus}>Showing the first 20 results. Narrow your search.</div>
|
||||
<div className={css.searchStatus}>
|
||||
Showing the first {resultLimit} results. Narrow your search.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className={css.fade} />
|
||||
@@ -327,6 +333,7 @@ export function WorkspaceBrowser({
|
||||
insertSessionBefore,
|
||||
createWorkspace,
|
||||
searchSessions,
|
||||
searchResultLimit,
|
||||
}: WorkspaceBrowserProps) {
|
||||
const workspaces = useWorkspaces(state => state.items)
|
||||
const groupBy = useStore(s => s.groupBy)
|
||||
@@ -544,6 +551,7 @@ export function WorkspaceBrowser({
|
||||
workspaces={workspaces}
|
||||
query={normalizedQuery}
|
||||
remote={remoteSearch}
|
||||
resultLimit={searchResultLimit}
|
||||
/>
|
||||
)
|
||||
: groupBy === 'flat'
|
||||
|
||||
@@ -40,6 +40,8 @@ export type WorkspaceBrowserInjected = {
|
||||
query: string,
|
||||
signal: AbortSignal,
|
||||
) => Promise<{ items: readonly SessionSearchResultItem[]; hasMore: boolean }>
|
||||
/** Maximum number of merged rows rendered for one search. */
|
||||
searchResultLimit: number
|
||||
/** Rename a Host Workspace (rejects on name conflict; resolves on durability). */
|
||||
renameWorkspace: (workspaceId: WorkspaceId, title: string) => Promise<void>
|
||||
/** Delete only a Host Workspace registration; directory and Session logs remain. */
|
||||
|
||||
@@ -44,6 +44,7 @@ export function apply(ctx: ClientContext): void {
|
||||
startSession: (workspaceId) => { ctx.workspaces.startSession(workspaceId) },
|
||||
open: (sessionId) => { ctx.sessions.open(sessionId) },
|
||||
searchSessions,
|
||||
searchResultLimit: ctx.sessions.searchResultLimit,
|
||||
renameWorkspace: async (workspaceId, title) => { await ctx.workspaces.rename(workspaceId, title) },
|
||||
deleteWorkspace: async (workspaceId) => { await ctx.workspaces.delete(workspaceId) },
|
||||
insertSessionBefore: async (workspaceId, sessionId, beforeSessionId) => {
|
||||
|
||||
@@ -278,9 +278,6 @@ export function deriveFlat(list: SessionListState): SessionNode[] {
|
||||
return rows.map(s => sessionNode(s, [], false, false))
|
||||
}
|
||||
|
||||
/** Maximum rows rendered by the basic search surface. */
|
||||
const SEARCH_RESULT_LIMIT = 20
|
||||
|
||||
/**
|
||||
* Merge immediate title/Workspace substring matches with ranked Host content
|
||||
* matches. Local rows lead newest-first, content-only rows retain backend
|
||||
@@ -289,13 +286,15 @@ const SEARCH_RESULT_LIMIT = 20
|
||||
* @param workspaces - Workspace membership and display labels.
|
||||
* @param query - caller text; surrounding whitespace is ignored.
|
||||
* @param content - ranked Host content-search page.
|
||||
* @returns at most 20 deduplicated flat rows and a refine-query hint bit.
|
||||
* @param limit - protocol-owned maximum merged row count.
|
||||
* @returns bounded deduplicated flat rows and a refine-query hint bit.
|
||||
*/
|
||||
export function deriveSearchResults(
|
||||
list: SessionListState,
|
||||
workspaces: readonly WorkspaceView[],
|
||||
query: string,
|
||||
content: { items: readonly SessionSearchResultItem[]; hasMore: boolean },
|
||||
limit: number,
|
||||
): SearchResultSet {
|
||||
const q = query.trim().toLowerCase()
|
||||
if (q === '') return { items: [], hasMore: false }
|
||||
@@ -340,7 +339,7 @@ export function deriveSearchResults(
|
||||
}
|
||||
|
||||
return {
|
||||
items: ordered.slice(0, SEARCH_RESULT_LIMIT).map((summary) => {
|
||||
items: ordered.slice(0, limit).map((summary) => {
|
||||
const match = contentBySession.get(summary.id)
|
||||
return {
|
||||
id: summary.id,
|
||||
@@ -350,7 +349,7 @@ export function deriveSearchResults(
|
||||
...match === undefined ? {} : { snippet: match.snippet },
|
||||
}
|
||||
}),
|
||||
hasMore: content.hasMore || ordered.length > SEARCH_RESULT_LIMIT,
|
||||
hasMore: content.hasMore || ordered.length > limit,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user