|
|
@@ -22,7 +22,9 @@ import type { WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-
|
|
|
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
|
|
import type { WorkspaceBrowserProps } from '../contract/slots.ts'
|
|
|
import type { SessionNode, SessionOrderBy } from '../tree.ts'
|
|
|
-import { deriveFlat, deriveGroups, deriveSearchResults, UNGROUPED_KEY } from '../tree.ts'
|
|
|
+import {
|
|
|
+ deriveFlat, deriveGroups, deriveSearchResults, owningGroupKey, UNGROUPED_KEY,
|
|
|
+} from '../tree.ts'
|
|
|
import { ProjectRowItem, SearchResultItem, SessionNodeItem } from './Rows.tsx'
|
|
|
import { FLAT_SESSION_ORDER_KEY } from '../stores.ts'
|
|
|
import { WorkspacePickFlow } from '../WorkspacePicker.tsx'
|
|
|
@@ -238,6 +240,8 @@ type SessionTreeProps = Pick<
|
|
|
/** Host account home for POSIX hover-path abbreviation. */
|
|
|
home?: string | undefined
|
|
|
workspaces: readonly WorkspaceView[]
|
|
|
+ /** Whether the current Workspace stream has a complete Host baseline. */
|
|
|
+ workspaceReady: boolean
|
|
|
/** Explicit persisted zero-or-five-session state by Workspace group. */
|
|
|
groupExpansion: Readonly<Record<string, boolean>>
|
|
|
/** Persist one Workspace group's zero-or-five-session state. */
|
|
|
@@ -262,19 +266,28 @@ type SessionTreeProps = Pick<
|
|
|
onSessionArchive: (sessionId: SessionNode['id']) => void
|
|
|
/** Session order behavior: fixed after edits, or additionally promoted by user activity. */
|
|
|
orderBy: SessionOrderBy
|
|
|
+ /** One Session chosen from search that must be exposed and scrolled into view. */
|
|
|
+ revealSessionId?: SessionId | undefined
|
|
|
+ /** Acknowledge that the chosen Session row has been revealed. */
|
|
|
+ onSessionRevealed: (sessionId: SessionId) => void
|
|
|
}
|
|
|
|
|
|
/** The scrolling session tree; unmounting drops the sessions subscription and expand-all state. */
|
|
|
function SessionTree({
|
|
|
useSessions, useSessionPendingInteraction, startSession, open, forkSession, workspaces, archivedSessionIds,
|
|
|
+ workspaceReady,
|
|
|
onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive,
|
|
|
insertWorkspaceBefore, insertSessionBefore, orderBy,
|
|
|
groupExpansion, setGroupExpanded,
|
|
|
sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, home, t,
|
|
|
+ revealSessionId, onSessionRevealed,
|
|
|
}: SessionTreeProps) {
|
|
|
const list = useSessions(s => s)
|
|
|
const pendingInteractions = useSessionPendingInteraction(s => s)
|
|
|
const current = list.current
|
|
|
+ const revealGroup = revealSessionId === undefined || !workspaceReady
|
|
|
+ ? undefined
|
|
|
+ : owningGroupKey(workspaces, revealSessionId)
|
|
|
const [expandedSessionGroups, setExpandedSessionGroups] = useState<string[]>([])
|
|
|
// Transient drag marker state; the selected mode owns the resulting order.
|
|
|
const [drag, setDrag] = useState<DragState | null>(null)
|
|
|
@@ -284,10 +297,9 @@ function SessionTree({
|
|
|
const previousOrderBy = useRef(orderBy)
|
|
|
const nativeDragActive = drag !== null || workspaceDrag !== null
|
|
|
useNativeDragAcceptance(nativeDragActive)
|
|
|
- const currentGroup = current === undefined
|
|
|
+ const currentGroup = current === undefined || !workspaceReady
|
|
|
? undefined
|
|
|
- : (workspaces.find(w => w.sessionIds.includes(current))?.workspaceId as string | undefined)
|
|
|
- ?? UNGROUPED_KEY
|
|
|
+ : owningGroupKey(workspaces, current)
|
|
|
useEffect(() => {
|
|
|
if (current === undefined || currentGroup === undefined || Object.hasOwn(groupExpansion, currentGroup)) return
|
|
|
setGroupExpanded(currentGroup, true)
|
|
|
@@ -347,6 +359,17 @@ function SessionTree({
|
|
|
}),
|
|
|
[list, orderedWorkspaces, archivedSessionIds, pendingInteractions, expandedGroups, sessionOrderByAccount],
|
|
|
)
|
|
|
+ useEffect(() => {
|
|
|
+ if (revealGroup === undefined || groupExpansion[revealGroup] === true) return
|
|
|
+ setGroupExpanded(revealGroup, true)
|
|
|
+ }, [groupExpansion, revealGroup, setGroupExpanded])
|
|
|
+ useEffect(() => {
|
|
|
+ if (revealSessionId === undefined || revealGroup === undefined) return
|
|
|
+ const group = groups.find(candidate => candidate.key === revealGroup)
|
|
|
+ if (group === undefined || !group.expanded || !group.sessions.some(row => row.id === revealSessionId)) return
|
|
|
+ if (collapsedSessionRows(group.sessions).rows.some(row => row.id === revealSessionId)) return
|
|
|
+ setExpandedSessionGroups(keys => keys.includes(revealGroup) ? keys : [...keys, revealGroup])
|
|
|
+ }, [groups, revealGroup, revealSessionId])
|
|
|
const now = Date.now()
|
|
|
const commitSessionDrag = (activeDrag: DragState, over: NonNullable<DragState['over']>): void => {
|
|
|
if (sessionDropCommitted.current) return
|
|
|
@@ -564,6 +587,9 @@ function SessionTree({
|
|
|
onRename={onSessionRename}
|
|
|
onFork={forkSession}
|
|
|
onArchive={onSessionArchive}
|
|
|
+ onReveal={node.id === revealSessionId && group.key === revealGroup
|
|
|
+ ? () => { onSessionRevealed(node.id) }
|
|
|
+ : undefined}
|
|
|
drag={dragProps}
|
|
|
t={t}
|
|
|
/>
|
|
|
@@ -594,7 +620,8 @@ function SessionTree({
|
|
|
function FlatList({
|
|
|
useSessions, useSessionPendingInteraction, open, forkSession, onSessionRename, onSessionArchive,
|
|
|
archivedSessionIds,
|
|
|
- orderBy, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, t,
|
|
|
+ orderBy, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder,
|
|
|
+ revealSessionId, onSessionRevealed, t,
|
|
|
}: Pick<
|
|
|
SessionTreeProps,
|
|
|
| 'useSessions'
|
|
|
@@ -609,6 +636,8 @@ function FlatList({
|
|
|
| 'sessionUpdatedAtByAccount'
|
|
|
| 'syncSessionOrderAccount'
|
|
|
| 'setSessionOrder'
|
|
|
+ | 'revealSessionId'
|
|
|
+ | 'onSessionRevealed'
|
|
|
| 't'
|
|
|
>) {
|
|
|
const list = useSessions(s => s)
|
|
|
@@ -683,6 +712,9 @@ function FlatList({
|
|
|
onRename={onSessionRename}
|
|
|
onFork={forkSession}
|
|
|
onArchive={onSessionArchive}
|
|
|
+ onReveal={node.id === revealSessionId
|
|
|
+ ? () => { onSessionRevealed(node.id) }
|
|
|
+ : undefined}
|
|
|
flat
|
|
|
drag={{
|
|
|
start: () => {
|
|
|
@@ -827,6 +859,7 @@ export function WorkspaceBrowser({
|
|
|
const home = useHostInfo(info => info.home)
|
|
|
const workspaces = useWorkspaces(state => state.items)
|
|
|
const workspacePhase = useWorkspaces(state => state.phase)
|
|
|
+ const workspaceStreamState = useWorkspaces(state => state.state)
|
|
|
const archivedSessionIds = useWorkspaces(state => state.archivedSessionIds)
|
|
|
// Live occupancy of this surface's directory-flow hole (the same source the
|
|
|
// flow reads): a composition without a picking affordance can add nothing.
|
|
|
@@ -841,9 +874,9 @@ export function WorkspaceBrowser({
|
|
|
return current !== undefined && state.byId[current]?.blank === true ? current : undefined
|
|
|
})
|
|
|
const currentBlankAccount = currentBlankSessionId === undefined
|
|
|
+ || workspacePhase !== 'ready'
|
|
|
? undefined
|
|
|
- : (workspaces.find(workspace => workspace.sessionIds.includes(currentBlankSessionId))
|
|
|
- ?.workspaceId as string | undefined) ?? UNGROUPED_KEY
|
|
|
+ : owningGroupKey(workspaces, currentBlankSessionId)
|
|
|
const promotedBlank = useRef<{ sessionId: SessionId; accountKey: string } | undefined>(undefined)
|
|
|
useEffect(() => {
|
|
|
if (currentBlankSessionId === undefined || currentBlankAccount === undefined) {
|
|
|
@@ -874,6 +907,7 @@ export function WorkspaceBrowser({
|
|
|
// does not silently drop an in-progress filter.
|
|
|
const [query, setQuery] = useState('')
|
|
|
const [searchExpanded, setSearchExpanded] = useState(false)
|
|
|
+ const [revealSessionId, setRevealSessionId] = useState<SessionId | undefined>(undefined)
|
|
|
const normalizedQuery = sanitizeSearchQuery(query).trim()
|
|
|
const [remoteSearch, setRemoteSearch] = useState<RemoteSearchState>({
|
|
|
query: '',
|
|
|
@@ -889,6 +923,19 @@ export function WorkspaceBrowser({
|
|
|
const wsPlusRef = useRef<HTMLButtonElement>(null)
|
|
|
const composingRef = useRef(false)
|
|
|
|
|
|
+ const openSearchResult = (sessionId: SessionId): void => {
|
|
|
+ setRevealSessionId(sessionId)
|
|
|
+ setQuery('')
|
|
|
+ setSearchExpanded(false)
|
|
|
+ open(sessionId)
|
|
|
+ }
|
|
|
+ const acknowledgeSessionReveal = (sessionId: SessionId): void => {
|
|
|
+ setRevealSessionId(current => current === sessionId ? undefined : current)
|
|
|
+ }
|
|
|
+ useEffect(() => {
|
|
|
+ if (normalizedQuery !== '') setRevealSessionId(undefined)
|
|
|
+ }, [normalizedQuery])
|
|
|
+
|
|
|
// Rail search = expand + land in the search box: the flag arms before the
|
|
|
// expand request; once the shell flips wide the input mounts and takes focus.
|
|
|
const [searchOnExpand, setSearchOnExpand] = useState(false)
|
|
|
@@ -1206,7 +1253,7 @@ export function WorkspaceBrowser({
|
|
|
<SearchResults
|
|
|
useSessions={useSessions}
|
|
|
useSessionPendingInteraction={useSessionPendingInteraction}
|
|
|
- open={open}
|
|
|
+ open={openSearchResult}
|
|
|
workspaces={workspaces}
|
|
|
archivedSessionIds={archivedSessionIds}
|
|
|
query={normalizedQuery}
|
|
|
@@ -1227,6 +1274,8 @@ export function WorkspaceBrowser({
|
|
|
sessionUpdatedAtByAccount={sessionUpdatedAtByAccount}
|
|
|
syncSessionOrderAccount={actions.syncSessionOrderAccount}
|
|
|
setSessionOrder={actions.setSessionOrder}
|
|
|
+ revealSessionId={revealSessionId}
|
|
|
+ onSessionRevealed={acknowledgeSessionReveal}
|
|
|
t={t}
|
|
|
/>
|
|
|
)
|
|
|
@@ -1238,6 +1287,7 @@ export function WorkspaceBrowser({
|
|
|
onSessionArchive={onSessionArchive}
|
|
|
forkSession={forkSession}
|
|
|
workspaces={workspaces}
|
|
|
+ workspaceReady={workspacePhase === 'ready' && workspaceStreamState !== 'loading'}
|
|
|
groupExpansion={groupExpansion}
|
|
|
setGroupExpanded={actions.setGroupExpanded}
|
|
|
sessionOrderByAccount={sessionOrderByAccount}
|
|
|
@@ -1250,6 +1300,8 @@ export function WorkspaceBrowser({
|
|
|
insertWorkspaceBefore={insertWorkspaceBefore}
|
|
|
insertSessionBefore={insertSessionBefore}
|
|
|
orderBy={orderBy}
|
|
|
+ revealSessionId={revealSessionId}
|
|
|
+ onSessionRevealed={acknowledgeSessionReveal}
|
|
|
home={home}
|
|
|
t={t}
|
|
|
onRenameRequest={(workspaceId, currentTitle) => {
|