fix(client): keep flat sessions ordered by recency

This commit is contained in:
_Kerman
2026-08-11 15:24:32 +08:00
parent c4affa852c
commit 8005ab78bd
2 changed files with 122 additions and 121 deletions

View File

@@ -36,9 +36,6 @@ const SEARCH_DEBOUNCE_MS = 250
const SEARCH_QUERY_MAX_CODE_UNITS = 500 const SEARCH_QUERY_MAX_CODE_UNITS = 500
/** Session rows visible per Workspace before the local overflow control. */ /** Session rows visible per Workspace before the local overflow control. */
const COLLAPSED_SESSION_LIMIT = 5 const COLLAPSED_SESSION_LIMIT = 5
const EMPTY_WORKSPACE_EXPANSION: Readonly<Record<string, boolean>> = Object.freeze({})
const EMPTY_RECENT_SESSION_ORDER: Readonly<Record<string, readonly string[]>> = Object.freeze({})
const EMPTY_RECENT_SESSION_UPDATED_AT: Readonly<Record<string, Readonly<Record<string, number>>>> = Object.freeze({})
/** Keep controlled input and RPC payload inside the session.search wire contract. */ /** Keep controlled input and RPC payload inside the session.search wire contract. */
function sanitizeSearchQuery(value: string): string { function sanitizeSearchQuery(value: string): string {
@@ -245,7 +242,11 @@ function SessionTree({
nextOrder.sort((a, b) => compareSessionRecency(a, b, list.byId)) nextOrder.sort((a, b) => compareSessionRecency(a, b, list.byId))
} else { } else {
const promoted = sessionIds const promoted = sessionIds
.filter((id) => previousUpdatedAt[id] === undefined || list.byId[id]!.updatedAt > previousUpdatedAt[id]!) .filter((id) => {
const session = list.byId[id]
return session !== undefined
&& (previousUpdatedAt[id] === undefined || session.updatedAt > previousUpdatedAt[id])
})
.sort((a, b) => compareSessionRecency(a, b, list.byId)) .sort((a, b) => compareSessionRecency(a, b, list.byId))
if (promoted.length > 0) { if (promoted.length > 0) {
const promotedIds = new Set(promoted) const promotedIds = new Set(promoted)
@@ -253,7 +254,10 @@ function SessionTree({
} }
} }
const nextUpdatedAt: Record<string, number> = {} const nextUpdatedAt: Record<string, number> = {}
for (const id of sessionIds) nextUpdatedAt[id] = list.byId[id]!.updatedAt for (const id of sessionIds) {
const session = list.byId[id]
if (session !== undefined) nextUpdatedAt[id] = session.updatedAt
}
const orderChanged = previousOrder === undefined const orderChanged = previousOrder === undefined
|| nextOrder.length !== previousOrder.length || nextOrder.length !== previousOrder.length
|| nextOrder.some((id, index) => id !== previousOrder[index]) || nextOrder.some((id, index) => id !== previousOrder[index])
@@ -299,7 +303,7 @@ function SessionTree({
const nextOrder = account.sessionIds.filter(id => id !== activeDrag.sessionId) const nextOrder = account.sessionIds.filter(id => id !== activeDrag.sessionId)
const insertAt = anchor === undefined ? nextOrder.length : nextOrder.indexOf(anchor) const insertAt = anchor === undefined ? nextOrder.length : nextOrder.indexOf(anchor)
nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId) nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId)
setRecentSessionOrder(activeDrag.workspaceId as string, nextOrder.map(id => id as string)) setRecentSessionOrder(activeDrag.workspaceId, nextOrder.map(id => id as string))
return return
} }
insertSessionBefore(activeDrag.workspaceId, activeDrag.sessionId, anchor).catch((reason: unknown) => { insertSessionBefore(activeDrag.workspaceId, activeDrag.sessionId, anchor).catch((reason: unknown) => {
@@ -465,13 +469,13 @@ function SessionTree({
} }
/** The flat "In one list" body: every session a top-level row, newest-first. */ /** The flat "In one list" body: every session a top-level row, newest-first. */
function FlatList({ useSessions, open, forkSession, onSessionRename, onSessionArchive, archivedSessionIds, orderBy, t }: Pick< function FlatList({ useSessions, open, forkSession, onSessionRename, onSessionArchive, archivedSessionIds, t }: Pick<
SessionTreeProps, 'useSessions' | 'open' | 'forkSession' | 'onSessionRename' | 'onSessionArchive' | 'archivedSessionIds' | 'orderBy' | 't' SessionTreeProps, 'useSessions' | 'open' | 'forkSession' | 'onSessionRename' | 'onSessionArchive' | 'archivedSessionIds' | 't'
>) { >) {
const list = useSessions(s => s) const list = useSessions(s => s)
const rows = useMemo( const rows = useMemo(
() => deriveFlat(list, archivedSessionIds, orderBy), () => deriveFlat(list, archivedSessionIds),
[list, archivedSessionIds, orderBy], [list, archivedSessionIds],
) )
const now = Date.now() const now = Date.now()
return ( return (
@@ -604,16 +608,13 @@ export function WorkspaceBrowser({
// flow reads): a composition without a picking affordance can add nothing. // flow reads): a composition without a picking affordance can add nothing.
const directoryFlowAvailable = useDirectoryFlow(occupied => occupied) const directoryFlowAvailable = useDirectoryFlow(occupied => occupied)
const groupBy = useStore(s => s.groupBy) const groupBy = useStore(s => s.groupBy)
// A live HMR handoff can retain the pre-ordering store instance until the const orderBy = useStore(s => s.orderBy)
// slot is remounted; manual is the established Workspace order.
const orderBy = useStore(s => s.orderBy ?? 'manual')
// A flat list has no single Workspace account to drag. Keep the stored // A flat list has no single Workspace account to drag. Keep the stored
// grouped preference intact while presenting the flat list by recency. // grouped preference intact while presenting the flat list by recency.
const effectiveOrderBy = groupBy === 'flat' && orderBy === 'manual' ? 'updated' : orderBy const effectiveOrderBy = groupBy === 'flat' && orderBy === 'manual' ? 'updated' : orderBy
// HMR can retain the preceding view-store instance until the slot remounts. const workspaceExpansion = useStore(s => s.workspaceExpansion)
const workspaceExpansion = useStore(s => s.workspaceExpansion ?? EMPTY_WORKSPACE_EXPANSION) const recentSessionOrder = useStore(s => s.recentSessionOrder)
const recentSessionOrder = useStore(s => s.recentSessionOrder ?? EMPTY_RECENT_SESSION_ORDER) const recentSessionUpdatedAt = useStore(s => s.recentSessionUpdatedAt)
const recentSessionUpdatedAt = useStore(s => s.recentSessionUpdatedAt ?? EMPTY_RECENT_SESSION_UPDATED_AT)
// The query outlives the tree and the input (both wide-only) so collapsing // The query outlives the tree and the input (both wide-only) so collapsing
// does not silently drop an in-progress filter. // does not silently drop an in-progress filter.
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
@@ -958,7 +959,7 @@ export function WorkspaceBrowser({
<FlatList <FlatList
useSessions={useSessions} open={open} forkSession={forkSession} useSessions={useSessions} open={open} forkSession={forkSession}
onSessionRename={onSessionRename} onSessionArchive={onSessionArchive} onSessionRename={onSessionRename} onSessionArchive={onSessionArchive}
archivedSessionIds={archivedSessionIds} orderBy={effectiveOrderBy} t={t} archivedSessionIds={archivedSessionIds} t={t}
/> />
) )
: ( : (

View File

@@ -208,8 +208,8 @@ function sessionNode(
/** /**
* Derive the workspace browser groups with every session as a top-level row. * Derive the workspace browser groups with every session as a top-level row.
* *
* Every group shows; sessions populate under expanded groups, preserving * Every group shows; sessions populate under expanded groups in the selected
* Host account order. Blank sessions are excluded except for the selected * local order. Blank sessions are excluded except for the selected
* provisional New Session row; archived sessions are excluded everywhere. * provisional New Session row; archived sessions are excluded everywhere.
* Content search lives outside this derivation * Content search lives outside this derivation
* (see {@link deriveSearchResults}). * (see {@link deriveSearchResults}).
@@ -217,6 +217,7 @@ function sessionNode(
* @param workspaces - real workspaces in stable Host order. * @param workspaces - real workspaces in stable Host order.
* @param archivedSessionIds - registry-global archive set. * @param archivedSessionIds - registry-global archive set.
* @param view - local expansion arrays. * @param view - local expansion arrays.
* @param orderBy - local session ordering mode.
* @returns group sections in render order. * @returns group sections in render order.
*/ */
export function deriveGroups( export function deriveGroups(
@@ -263,7 +264,6 @@ export function deriveGroups(
export function deriveFlat( export function deriveFlat(
list: SessionListState, list: SessionListState,
archivedSessionIds: readonly SessionId[], archivedSessionIds: readonly SessionId[],
orderBy: SessionOrderBy = 'updated',
): SessionNode[] { ): SessionNode[] {
const archived = new Set(archivedSessionIds) const archived = new Set(archivedSessionIds)
const descendants = indexSubagentDescendants(list.byId) const descendants = indexSubagentDescendants(list.byId)
@@ -273,7 +273,7 @@ export function deriveFlat(
if (s === undefined || !sessionVisible(s, list.current, archived)) continue if (s === undefined || !sessionVisible(s, list.current, archived)) continue
rows.push(s) rows.push(s)
} }
sortSessions(rows, orderBy === 'manual' ? 'updated' : orderBy) sortSessions(rows)
return rows.map(session => sessionNode(session, descendants)) return rows.map(session => sessionNode(session, descendants))
} }