fix(client): keep flat sessions ordered by recency
This commit is contained in:
@@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user