feat(client): refine workspace sidebar interactions
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
/* Settings shell (figma 501:29904 mask context / 501:29947 panel): sidebar
|
/* Settings shell (figma 501:29904 mask context / 501:29947 panel): sidebar
|
||||||
foot trigger row + centered 1080x700 modal panel. The trigger uses the
|
foot trigger row + centered 1080x700 modal panel. The trigger uses the
|
||||||
sidebar's 38px wide row / 36px rail circle rhythm; the
|
sidebar's 34px compact row / 36px rail circle rhythm; the
|
||||||
panel is a two-column layout — 188px nav rail + content column with a
|
panel is a two-column layout — 188px nav rail + content column with a
|
||||||
54px header and the 24px-padded options area. */
|
54px header and the 24px-padded options area. */
|
||||||
|
|
||||||
@@ -10,10 +10,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: calc(100% - 8px);
|
width: calc(100% + 8px);
|
||||||
height: 38px;
|
height: 34px;
|
||||||
margin: 4px 4px 4px;
|
margin: 4px -4px 4px;
|
||||||
padding: 8px 2px 8px 10px;
|
padding: 6px 2px 6px 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|||||||
@@ -38,6 +38,19 @@
|
|||||||
background: var(--dsw-alias-interactive-bg-hover);
|
background: var(--dsw-alias-interactive-bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.viewOptionLabel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewOptionCheck {
|
||||||
|
flex: none;
|
||||||
|
color: var(--dsw-alias-label-primary);
|
||||||
|
}
|
||||||
|
|
||||||
/* Section header: title, an inline search control, and the two trailing
|
/* Section header: title, an inline search control, and the two trailing
|
||||||
actions. Expanding search collapses the action cluster and takes its room. */
|
actions. Expanding search collapses the action cluster and takes its room. */
|
||||||
.sectionHeader {
|
.sectionHeader {
|
||||||
@@ -56,6 +69,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.root:not(.rail) .sectionHeader {
|
.root:not(.rail) .sectionHeader {
|
||||||
|
margin-top: 2px;
|
||||||
margin-right: -4px;
|
margin-right: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +80,23 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transition:
|
||||||
|
max-width 180ms var(--ds-ease-in-out),
|
||||||
|
margin-right 180ms var(--ds-ease-in-out),
|
||||||
|
opacity 120ms var(--ds-ease-in-out),
|
||||||
|
transform 180ms var(--ds-ease-in-out),
|
||||||
|
visibility 0s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionLabelHidden {
|
||||||
|
max-width: 0;
|
||||||
|
margin-right: -4px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-4px);
|
||||||
|
visibility: hidden;
|
||||||
|
transition-delay: 0s, 0s, 0s, 0s, 180ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchSlot {
|
.searchSlot {
|
||||||
@@ -84,7 +115,7 @@
|
|||||||
|
|
||||||
.searchSlotExpanded {
|
.searchSlotExpanded {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding-left: 4px;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerActions {
|
.headerActions {
|
||||||
@@ -138,7 +169,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.searchExpanded {
|
.searchExpanded {
|
||||||
height: 26px;
|
width: calc(100% + 4px);
|
||||||
|
height: 34px;
|
||||||
|
margin-inline: -2px;
|
||||||
padding: 0 4px 0 0;
|
padding: 0 4px 0 0;
|
||||||
border: 1px solid var(--dsw-alias-border-l2);
|
border: 1px solid var(--dsw-alias-border-l2);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -162,8 +195,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.searchExpanded .searchButton {
|
.searchExpanded .searchButton {
|
||||||
width: 26px;
|
width: 28px;
|
||||||
height: 26px;
|
height: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchButton:hover {
|
.searchButton:hover {
|
||||||
@@ -204,8 +237,8 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 18px;
|
width: 24px;
|
||||||
height: 18px;
|
height: 24px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -214,6 +247,10 @@
|
|||||||
color: var(--dsw-alias-label-secondary);
|
color: var(--dsw-alias-label-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clearButton:hover {
|
||||||
|
background: var(--dsw-alias-interactive-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
/* Rail variant (own .rail class from the wide owner prop — the region never
|
/* Rail variant (own .rail class from the wide owner prop — the region never
|
||||||
reads the shell's class names): the two icon controls stack as 36x36
|
reads the shell's class names): the two icon controls stack as 36x36
|
||||||
circles matching the shell's rail rhythm. */
|
circles matching the shell's rail rhythm. */
|
||||||
@@ -367,7 +404,7 @@
|
|||||||
|
|
||||||
.sessionOverflowButton {
|
.sessionOverflowButton {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 30px;
|
height: 28px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 0 12px 0 28px;
|
padding: 0 12px 0 28px;
|
||||||
@@ -385,8 +422,6 @@
|
|||||||
.sessionOverflowButton:hover {
|
.sessionOverflowButton:hover {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--dsw-alias-label-secondary);
|
color: var(--dsw-alias-label-secondary);
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
@@ -438,6 +473,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search,
|
.search,
|
||||||
|
.sectionLabel,
|
||||||
.searchSlot,
|
.searchSlot,
|
||||||
.searchInput,
|
.searchInput,
|
||||||
.headerActions {
|
.headerActions {
|
||||||
|
|||||||
@@ -12,11 +12,11 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
Button, IconCloseFill14, IconPersonalizationOutline16,
|
Button, IconCheckOutline16, IconCloseFill14, IconPersonalizationOutline16,
|
||||||
IconProjectAddOutline16, IconSearchOutline16, Menu, Modal, Tooltip,
|
IconProjectAddOutline16, IconSearchOutline16, Menu, Modal, Tooltip,
|
||||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
import type {
|
import type {
|
||||||
SessionSearchResultItem, WorkspaceId, WorkspaceView,
|
SessionId, SessionListState, SessionSearchResultItem, WorkspaceId, WorkspaceView,
|
||||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||||
import type { WorkspaceBrowserProps } from './contract/slots.ts'
|
import type { WorkspaceBrowserProps } from './contract/slots.ts'
|
||||||
import type { SessionNode, SessionOrderBy } from './tree.ts'
|
import type { SessionNode, SessionOrderBy } from './tree.ts'
|
||||||
@@ -37,6 +37,8 @@ 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_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 {
|
||||||
@@ -54,6 +56,33 @@ function toggled(list: readonly string[], key: string): string[] {
|
|||||||
return list.includes(key) ? list.filter(k => k !== key) : [...list, key]
|
return list.includes(key) ? list.filter(k => k !== key) : [...list, key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Reconcile a stored view order with the Workspace's current session account. */
|
||||||
|
function reconciledSessionOrder(sessionIds: readonly SessionId[], stored: readonly string[] | undefined): SessionId[] {
|
||||||
|
if (stored === undefined) return [...sessionIds]
|
||||||
|
const byId = new Map(sessionIds.map(id => [id as string, id]))
|
||||||
|
const ordered: SessionId[] = []
|
||||||
|
const included = new Set<string>()
|
||||||
|
for (const key of stored) {
|
||||||
|
const id = byId.get(key)
|
||||||
|
if (id === undefined || included.has(key)) continue
|
||||||
|
ordered.push(id)
|
||||||
|
included.add(key)
|
||||||
|
}
|
||||||
|
for (const id of sessionIds) {
|
||||||
|
if (included.has(id)) continue
|
||||||
|
ordered.push(id)
|
||||||
|
}
|
||||||
|
return ordered
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Newest update first with stable Session identity as the tie-break. */
|
||||||
|
function compareSessionRecency(a: SessionId, b: SessionId, byId: SessionListState['byId']): number {
|
||||||
|
const aUpdatedAt = byId[a]?.updatedAt ?? Number.NEGATIVE_INFINITY
|
||||||
|
const bUpdatedAt = byId[b]?.updatedAt ?? Number.NEGATIVE_INFINITY
|
||||||
|
if (aUpdatedAt !== bUpdatedAt) return bUpdatedAt - aUpdatedAt
|
||||||
|
return a < b ? -1 : 1
|
||||||
|
}
|
||||||
|
|
||||||
/** Grouping and ordering menu; own open state so it resets with the wide chrome. */
|
/** Grouping and ordering menu; own open state so it resets with the wide chrome. */
|
||||||
function ViewOptionsMenu({ groupBy, orderBy, onGroupPick, onOrderPick, t }: {
|
function ViewOptionsMenu({ groupBy, orderBy, onGroupPick, onOrderPick, t }: {
|
||||||
groupBy: 'workspace' | 'flat'
|
groupBy: 'workspace' | 'flat'
|
||||||
@@ -63,23 +92,27 @@ function ViewOptionsMenu({ groupBy, orderBy, onGroupPick, onOrderPick, t }: {
|
|||||||
t: WorkspaceBrowserProps['t']
|
t: WorkspaceBrowserProps['t']
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
const optionLabel = (label: string, selected: boolean) => (
|
||||||
|
<span className={css.viewOptionLabel}>
|
||||||
|
<span>{label}</span>
|
||||||
|
{selected && <IconCheckOutline16 className={css.viewOptionCheck} />}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
open={open}
|
open={open}
|
||||||
onClose={() => { setOpen(false) }}
|
onClose={() => { setOpen(false) }}
|
||||||
items={[
|
items={[
|
||||||
{ type: 'label' as const, id: 'group-by', text: t('groupBy.label') },
|
{ type: 'label' as const, id: 'group-by', text: t('groupBy.label') },
|
||||||
{ id: 'workspace', label: t('groupBy.workspace') },
|
{ id: 'workspace', label: optionLabel(t('groupBy.workspace'), groupBy === 'workspace') },
|
||||||
{ id: 'flat', label: t('groupBy.flat') },
|
{ id: 'flat', label: optionLabel(t('groupBy.flat'), groupBy === 'flat') },
|
||||||
{ type: 'label' as const, id: 'order-by', text: t('orderBy.label') },
|
{ type: 'label' as const, id: 'order-by', text: t('orderBy.label') },
|
||||||
{ id: 'manual', label: t('orderBy.manual'), disabled: groupBy !== 'workspace' },
|
{ id: 'manual', label: optionLabel(t('orderBy.manual'), orderBy === 'manual'), disabled: groupBy !== 'workspace' },
|
||||||
{ id: 'created', label: t('orderBy.created') },
|
{ id: 'updated', label: optionLabel(t('orderBy.updated'), orderBy === 'updated') },
|
||||||
{ id: 'updated', label: t('orderBy.updated') },
|
|
||||||
]}
|
]}
|
||||||
selectedIds={[groupBy, orderBy]}
|
|
||||||
onSelect={(id) => {
|
onSelect={(id) => {
|
||||||
if (id === 'workspace' || id === 'flat') onGroupPick(id)
|
if (id === 'workspace' || id === 'flat') onGroupPick(id)
|
||||||
else if (id === 'manual' || id === 'created' || id === 'updated') onOrderPick(id)
|
else if (id === 'manual' || id === 'updated') onOrderPick(id)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
align="end"
|
align="end"
|
||||||
@@ -133,6 +166,14 @@ type SessionTreeProps = Pick<
|
|||||||
workspaceExpansion: Readonly<Record<string, boolean>>
|
workspaceExpansion: Readonly<Record<string, boolean>>
|
||||||
/** Persist one Workspace group's zero-or-five-session state. */
|
/** Persist one Workspace group's zero-or-five-session state. */
|
||||||
setWorkspaceExpanded: (key: string, expanded: boolean) => void
|
setWorkspaceExpanded: (key: string, expanded: boolean) => void
|
||||||
|
/** Editable orders used by recent-update mode. */
|
||||||
|
recentSessionOrder: Readonly<Record<string, readonly string[]>>
|
||||||
|
/** Last update timestamps observed by recent-update mode. */
|
||||||
|
recentSessionUpdatedAt: Readonly<Record<string, Readonly<Record<string, number>>>>
|
||||||
|
/** Replace one recent-mode order and its observed timestamps. */
|
||||||
|
syncRecentSessions: (workspaceKey: string, order: string[], updatedAt: Record<string, number>) => void
|
||||||
|
/** Apply a manual drag inside one recent-mode order. */
|
||||||
|
setRecentSessionOrder: (workspaceKey: string, order: string[]) => void
|
||||||
/** Registry-global archive set (hidden rows). */
|
/** Registry-global archive set (hidden rows). */
|
||||||
archivedSessionIds: readonly SessionNode['id'][]
|
archivedSessionIds: readonly SessionNode['id'][]
|
||||||
/** Open the browser-owned rename dialog for a real Workspace group. */
|
/** Open the browser-owned rename dialog for a real Workspace group. */
|
||||||
@@ -143,7 +184,7 @@ type SessionTreeProps = Pick<
|
|||||||
onSessionRename: (sessionId: SessionNode['id'], currentTitle: string) => void
|
onSessionRename: (sessionId: SessionNode['id'], currentTitle: string) => void
|
||||||
/** Archive a session (row menu action; the row disappears on the state echo). */
|
/** Archive a session (row menu action; the row disappears on the state echo). */
|
||||||
onSessionArchive: (sessionId: SessionNode['id']) => void
|
onSessionArchive: (sessionId: SessionNode['id']) => void
|
||||||
/** Session visual order; only manual mode exposes durable Session dragging. */
|
/** Session visual order; manual mode drags durable order, updated mode drags its view order. */
|
||||||
orderBy: SessionOrderBy
|
orderBy: SessionOrderBy
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,14 +193,34 @@ function SessionTree({
|
|||||||
useSessions, startSession, open, forkSession, workspaces, archivedSessionIds,
|
useSessions, startSession, open, forkSession, workspaces, archivedSessionIds,
|
||||||
onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive,
|
onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive,
|
||||||
insertWorkspaceBefore, insertSessionBefore, orderBy,
|
insertWorkspaceBefore, insertSessionBefore, orderBy,
|
||||||
workspaceExpansion, setWorkspaceExpanded, t,
|
workspaceExpansion, setWorkspaceExpanded,
|
||||||
|
recentSessionOrder, recentSessionUpdatedAt, syncRecentSessions, setRecentSessionOrder, t,
|
||||||
}: SessionTreeProps) {
|
}: SessionTreeProps) {
|
||||||
const list = useSessions(s => s)
|
const list = useSessions(s => s)
|
||||||
const current = list.current
|
const current = list.current
|
||||||
const [expandedSessionGroups, setExpandedSessionGroups] = useState<string[]>([])
|
const [expandedSessionGroups, setExpandedSessionGroups] = useState<string[]>([])
|
||||||
// Transient drag viewing state (never store-bound; order truth stays Host-side).
|
// Transient drag marker state; the selected mode owns the resulting order.
|
||||||
const [drag, setDrag] = useState<DragState | null>(null)
|
const [drag, setDrag] = useState<DragState | null>(null)
|
||||||
|
const sessionDropCommitted = useRef(false)
|
||||||
const [workspaceDrag, setWorkspaceDrag] = useState<WorkspaceDragState | null>(null)
|
const [workspaceDrag, setWorkspaceDrag] = useState<WorkspaceDragState | null>(null)
|
||||||
|
const sessionDragging = drag !== null
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sessionDragging) return
|
||||||
|
// Row hover still owns the insertion marker. Accept the native drag at
|
||||||
|
// document level so releasing outside the list is not rendered as a
|
||||||
|
// rejected drop before dragend commits that last marker.
|
||||||
|
const acceptDrag = (event: DragEvent): void => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (event.dataTransfer !== null) event.dataTransfer.dropEffect = 'move'
|
||||||
|
}
|
||||||
|
const acceptDrop = (event: DragEvent): void => { event.preventDefault() }
|
||||||
|
document.addEventListener('dragover', acceptDrag)
|
||||||
|
document.addEventListener('drop', acceptDrop)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('dragover', acceptDrag)
|
||||||
|
document.removeEventListener('drop', acceptDrop)
|
||||||
|
}
|
||||||
|
}, [sessionDragging])
|
||||||
const currentGroup = current === undefined
|
const currentGroup = current === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: (workspaces.find(w => w.sessionIds.includes(current))?.workspaceId as string | undefined)
|
: (workspaces.find(w => w.sessionIds.includes(current))?.workspaceId as string | undefined)
|
||||||
@@ -172,11 +233,79 @@ function SessionTree({
|
|||||||
() => Object.entries(workspaceExpansion).filter(([, expanded]) => expanded).map(([key]) => key),
|
() => Object.entries(workspaceExpansion).filter(([, expanded]) => expanded).map(([key]) => key),
|
||||||
[workspaceExpansion],
|
[workspaceExpansion],
|
||||||
)
|
)
|
||||||
|
useEffect(() => {
|
||||||
|
if (orderBy !== 'updated' || list.phase !== 'ready') return
|
||||||
|
for (const workspace of workspaces) {
|
||||||
|
const key = workspace.workspaceId as string
|
||||||
|
const sessionIds = workspace.sessionIds.filter(id => list.byId[id] !== undefined)
|
||||||
|
const previousOrder = recentSessionOrder[key]
|
||||||
|
const previousUpdatedAt = recentSessionUpdatedAt[key] ?? {}
|
||||||
|
let nextOrder = reconciledSessionOrder(sessionIds, previousOrder)
|
||||||
|
if (previousOrder === undefined) {
|
||||||
|
nextOrder.sort((a, b) => compareSessionRecency(a, b, list.byId))
|
||||||
|
} else {
|
||||||
|
const promoted = sessionIds
|
||||||
|
.filter((id) => previousUpdatedAt[id] === undefined || list.byId[id]!.updatedAt > previousUpdatedAt[id]!)
|
||||||
|
.sort((a, b) => compareSessionRecency(a, b, list.byId))
|
||||||
|
if (promoted.length > 0) {
|
||||||
|
const promotedIds = new Set(promoted)
|
||||||
|
nextOrder = [...promoted, ...nextOrder.filter(id => !promotedIds.has(id))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nextUpdatedAt: Record<string, number> = {}
|
||||||
|
for (const id of sessionIds) nextUpdatedAt[id] = list.byId[id]!.updatedAt
|
||||||
|
const orderChanged = previousOrder === undefined
|
||||||
|
|| nextOrder.length !== previousOrder.length
|
||||||
|
|| nextOrder.some((id, index) => id !== previousOrder[index])
|
||||||
|
const timestampsChanged = Object.keys(nextUpdatedAt).length !== Object.keys(previousUpdatedAt).length
|
||||||
|
|| Object.entries(nextUpdatedAt).some(([id, updatedAt]) => previousUpdatedAt[id] !== updatedAt)
|
||||||
|
if (orderChanged || timestampsChanged) {
|
||||||
|
syncRecentSessions(key, nextOrder.map(id => id as string), nextUpdatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [list, orderBy, recentSessionOrder, recentSessionUpdatedAt, syncRecentSessions, workspaces])
|
||||||
|
const orderedWorkspaces = useMemo(() => {
|
||||||
|
if (orderBy !== 'updated') return workspaces
|
||||||
|
return workspaces.map((workspace) => {
|
||||||
|
const stored = recentSessionOrder[workspace.workspaceId as string]
|
||||||
|
const sessionIds = reconciledSessionOrder(workspace.sessionIds, stored)
|
||||||
|
if (stored === undefined) sessionIds.sort((a, b) => compareSessionRecency(a, b, list.byId))
|
||||||
|
return { ...workspace, sessionIds }
|
||||||
|
})
|
||||||
|
}, [list.byId, orderBy, recentSessionOrder, workspaces])
|
||||||
const groups = useMemo(
|
const groups = useMemo(
|
||||||
() => deriveGroups(list, workspaces, archivedSessionIds, { expandedProjects }, orderBy),
|
() => deriveGroups(list, orderedWorkspaces, archivedSessionIds, { expandedProjects }, 'manual'),
|
||||||
[list, workspaces, archivedSessionIds, expandedProjects, orderBy],
|
[list, orderedWorkspaces, archivedSessionIds, expandedProjects],
|
||||||
)
|
)
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
const commitSessionDrag = (activeDrag: DragState, over: NonNullable<DragState['over']>): void => {
|
||||||
|
if (sessionDropCommitted.current) return
|
||||||
|
sessionDropCommitted.current = true
|
||||||
|
setDrag(null)
|
||||||
|
const group = groups.find(candidate => candidate.workspaceId === activeDrag.workspaceId)
|
||||||
|
if (group === undefined) return
|
||||||
|
const targetIndex = group.sessions.findIndex(session => session.id === over.id)
|
||||||
|
if (targetIndex === -1) return
|
||||||
|
const anchor = over.half === 'before' ? over.id : group.sessions[targetIndex + 1]?.id
|
||||||
|
if (anchor === activeDrag.sessionId) return
|
||||||
|
const sourceIndex = group.sessions.findIndex(session => session.id === activeDrag.sessionId)
|
||||||
|
const anchorIndex = anchor === undefined
|
||||||
|
? group.sessions.length
|
||||||
|
: group.sessions.findIndex(session => session.id === anchor)
|
||||||
|
if (sourceIndex !== -1 && (anchorIndex === sourceIndex || anchorIndex === sourceIndex + 1)) return
|
||||||
|
if (orderBy === 'updated') {
|
||||||
|
const account = orderedWorkspaces.find(workspace => workspace.workspaceId === activeDrag.workspaceId)
|
||||||
|
if (account === undefined) return
|
||||||
|
const nextOrder = account.sessionIds.filter(id => id !== activeDrag.sessionId)
|
||||||
|
const insertAt = anchor === undefined ? nextOrder.length : nextOrder.indexOf(anchor)
|
||||||
|
nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId)
|
||||||
|
setRecentSessionOrder(activeDrag.workspaceId as string, nextOrder.map(id => id as string))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
insertSessionBefore(activeDrag.workspaceId, activeDrag.sessionId, anchor).catch((reason: unknown) => {
|
||||||
|
console.warn('session reorder rejected:', reason)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(css.treeBody, css.wide)}>
|
<div className={clsx(css.treeBody, css.wide)}>
|
||||||
@@ -271,14 +400,15 @@ function SessionTree({
|
|||||||
{(expandedSessionGroups.includes(group.key)
|
{(expandedSessionGroups.includes(group.key)
|
||||||
? group.sessions
|
? group.sessions
|
||||||
: group.sessions.slice(0, COLLAPSED_SESSION_LIMIT)
|
: group.sessions.slice(0, COLLAPSED_SESSION_LIMIT)
|
||||||
).map((node, index) => {
|
).map((node) => {
|
||||||
// Draggable: real-workspace session rows. The drag
|
// Draggable: real-workspace session rows. The drag
|
||||||
// never leaves its group — rows of other groups show no markers
|
// never leaves its group — rows of other groups show no markers
|
||||||
// and reject drops (visual movement confined to this section).
|
// and reject drops (visual movement confined to this section).
|
||||||
const draggable = group.workspaceId !== undefined && orderBy === 'manual'
|
const draggable = group.workspaceId !== undefined
|
||||||
const sameGroupDrag = drag !== null && drag.workspaceId === group.workspaceId
|
const sameGroupDrag = drag !== null && drag.workspaceId === group.workspaceId
|
||||||
const dragProps = !draggable || group.workspaceId === undefined ? undefined : {
|
const dragProps = !draggable || group.workspaceId === undefined ? undefined : {
|
||||||
start: () => {
|
start: () => {
|
||||||
|
sessionDropCommitted.current = false
|
||||||
setDrag({ workspaceId: group.workspaceId as WorkspaceId, sessionId: node.id, over: null })
|
setDrag({ workspaceId: group.workspaceId as WorkspaceId, sessionId: node.id, over: null })
|
||||||
},
|
},
|
||||||
active: sameGroupDrag,
|
active: sameGroupDrag,
|
||||||
@@ -290,21 +420,13 @@ function SessionTree({
|
|||||||
drop: (half: 'before' | 'after') => {
|
drop: (half: 'before' | 'after') => {
|
||||||
/* v8 ignore next -- narrowing guard: Rows gates drop on `active`, which is false while the drag state is null. */
|
/* v8 ignore next -- narrowing guard: Rows gates drop on `active`, which is false while the drag state is null. */
|
||||||
if (drag === null) return
|
if (drag === null) return
|
||||||
const sessions = group.sessions
|
commitSessionDrag(drag, { id: node.id, half })
|
||||||
// Anchor = the row the insert line points at ('after' means
|
},
|
||||||
// the next root; end-of-list omits the anchor → append).
|
end: () => {
|
||||||
const anchor = half === 'before' ? node.id : sessions[index + 1]?.id
|
if (drag?.over !== null && drag?.over !== undefined) commitSessionDrag(drag, drag.over)
|
||||||
setDrag(null)
|
else setDrag(null)
|
||||||
if (anchor === drag.sessionId) return
|
sessionDropCommitted.current = false
|
||||||
// No-op when the drop lands back on the source position.
|
|
||||||
const sourceIndex = sessions.findIndex(r => r.id === drag.sessionId)
|
|
||||||
const anchorIndex = anchor === undefined ? sessions.length : sessions.findIndex(r => r.id === anchor)
|
|
||||||
if (sourceIndex !== -1 && (anchorIndex === sourceIndex || anchorIndex === sourceIndex + 1)) return
|
|
||||||
insertSessionBefore(drag.workspaceId, drag.sessionId, anchor).catch((reason: unknown) => {
|
|
||||||
console.warn('session reorder rejected:', reason)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
end: () => { setDrag(null) },
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<SessionNodeItem
|
<SessionNodeItem
|
||||||
@@ -490,6 +612,8 @@ export function WorkspaceBrowser({
|
|||||||
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.
|
// HMR can retain the preceding view-store instance until the slot remounts.
|
||||||
const workspaceExpansion = useStore(s => s.workspaceExpansion ?? EMPTY_WORKSPACE_EXPANSION)
|
const workspaceExpansion = useStore(s => s.workspaceExpansion ?? EMPTY_WORKSPACE_EXPANSION)
|
||||||
|
const recentSessionOrder = useStore(s => s.recentSessionOrder ?? EMPTY_RECENT_SESSION_ORDER)
|
||||||
|
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('')
|
||||||
@@ -532,12 +656,12 @@ export function WorkspaceBrowser({
|
|||||||
const onClick = (event: MouseEvent): void => {
|
const onClick = (event: MouseEvent): void => {
|
||||||
if (!(event.target instanceof Node) || searchRoot.current?.contains(event.target) === true) return
|
if (!(event.target instanceof Node) || searchRoot.current?.contains(event.target) === true) return
|
||||||
searchInput.current?.blur()
|
searchInput.current?.blur()
|
||||||
setQuery('')
|
if (query !== '') return
|
||||||
setSearchExpanded(false)
|
setSearchExpanded(false)
|
||||||
}
|
}
|
||||||
document.addEventListener('click', onClick)
|
document.addEventListener('click', onClick)
|
||||||
return () => { document.removeEventListener('click', onClick) }
|
return () => { document.removeEventListener('click', onClick) }
|
||||||
}, [wide, searchExpanded])
|
}, [query, wide, searchExpanded])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (normalizedQuery === '') {
|
if (normalizedQuery === '') {
|
||||||
@@ -686,7 +810,7 @@ export function WorkspaceBrowser({
|
|||||||
<div className={clsx(css.root, !wide && css.rail)}>
|
<div className={clsx(css.root, !wide && css.rail)}>
|
||||||
<div className={css.sectionHeader}>
|
<div className={css.sectionHeader}>
|
||||||
{wide && (
|
{wide && (
|
||||||
<span className={clsx(css.sectionLabel, css.wide)}>
|
<span className={clsx(css.sectionLabel, css.wide, searchExpanded && css.sectionLabelHidden)}>
|
||||||
{groupBy === 'flat' ? t('section.sessions') : t('section.workspaces')}
|
{groupBy === 'flat' ? t('section.sessions') : t('section.workspaces')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -846,6 +970,10 @@ export function WorkspaceBrowser({
|
|||||||
workspaces={workspaces}
|
workspaces={workspaces}
|
||||||
workspaceExpansion={workspaceExpansion}
|
workspaceExpansion={workspaceExpansion}
|
||||||
setWorkspaceExpanded={actions.setWorkspaceExpanded}
|
setWorkspaceExpanded={actions.setWorkspaceExpanded}
|
||||||
|
recentSessionOrder={recentSessionOrder}
|
||||||
|
recentSessionUpdatedAt={recentSessionUpdatedAt}
|
||||||
|
syncRecentSessions={actions.syncRecentSessions}
|
||||||
|
setRecentSessionOrder={actions.setRecentSessionOrder}
|
||||||
archivedSessionIds={archivedSessionIds}
|
archivedSessionIds={archivedSessionIds}
|
||||||
startSession={startSession}
|
startSession={startSession}
|
||||||
open={open}
|
open={open}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export const zh = {
|
|||||||
'groupBy.flat': '单列表',
|
'groupBy.flat': '单列表',
|
||||||
'orderBy.label': '排序方式',
|
'orderBy.label': '排序方式',
|
||||||
'orderBy.manual': '手动排序',
|
'orderBy.manual': '手动排序',
|
||||||
'orderBy.created': '创建时间',
|
|
||||||
'orderBy.updated': '最近更新',
|
'orderBy.updated': '最近更新',
|
||||||
'sessions.expand': '展开其余 {n} 个会话',
|
'sessions.expand': '展开其余 {n} 个会话',
|
||||||
'sessions.collapse': '收起',
|
'sessions.collapse': '收起',
|
||||||
@@ -84,7 +83,6 @@ export const en = {
|
|||||||
'groupBy.flat': 'In one list',
|
'groupBy.flat': 'In one list',
|
||||||
'orderBy.label': 'Order by',
|
'orderBy.label': 'Order by',
|
||||||
'orderBy.manual': 'Manual',
|
'orderBy.manual': 'Manual',
|
||||||
'orderBy.created': 'Date created',
|
|
||||||
'orderBy.updated': 'Last updated',
|
'orderBy.updated': 'Last updated',
|
||||||
'sessions.expand': 'Show {n} more sessions',
|
'sessions.expand': 'Show {n} more sessions',
|
||||||
'sessions.collapse': 'Show less',
|
'sessions.collapse': 'Show less',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* Tree rows (figma Cell set 14:3080): project 54px two-line, session 34px
|
/* Tree rows: project 34px, session 32px, radius 8, indent step 22px
|
||||||
single-line, radius 8, indent step 22px (16px slot + 6px gap). Hover swaps
|
(16px slot + 6px gap). Hover swaps
|
||||||
are pure CSS: project folder -> chevron + action buttons; session time ->
|
are pure CSS: project folder -> chevron + action buttons; session time ->
|
||||||
ellipsis button. */
|
ellipsis button. */
|
||||||
|
|
||||||
@@ -29,11 +29,11 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 62px;
|
min-height: 48px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 7px 8px;
|
padding: 4px 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -64,9 +64,16 @@
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.searchResultMeta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.searchResultWorkspace,
|
.searchResultWorkspace,
|
||||||
.searchResultSnippet {
|
.searchResultSnippet {
|
||||||
margin-left: 20px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -75,16 +82,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.searchResultWorkspace {
|
.searchResultWorkspace {
|
||||||
|
flex: none;
|
||||||
|
max-width: 40%;
|
||||||
color: var(--dsw-alias-label-tertiary);
|
color: var(--dsw-alias-label-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchResultSnippet {
|
.searchResultSnippet {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
color: var(--dsw-alias-label-secondary);
|
color: var(--dsw-alias-label-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compact one-line Workspace row after removing the session-count subtitle. */
|
/* Compact one-line Workspace row after removing the session-count subtitle. */
|
||||||
.projectRow {
|
.projectRow {
|
||||||
height: 36px;
|
height: 34px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -95,7 +106,7 @@
|
|||||||
|
|
||||||
/* Session cell (figma): pad 8, a 16px status slot, then a 4px title gap. */
|
/* Session cell (figma): pad 8, a 16px status slot, then a 4px title gap. */
|
||||||
.sessionRow {
|
.sessionRow {
|
||||||
height: 34px;
|
height: 32px;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
/* Mount fade: session rows appear by unfolding a group (or the tree
|
/* Mount fade: session rows appear by unfolding a group (or the tree
|
||||||
mounting). Stable row keys keep already-visible rows from replaying it. */
|
mounting). Stable row keys keep already-visible rows from replaying it. */
|
||||||
|
|||||||
@@ -317,10 +317,12 @@ export function SearchResultItem({ result, currentId, onOpen, t }: {
|
|||||||
</span>
|
</span>
|
||||||
<span className={css.searchResultTitle}>{result.title}</span>
|
<span className={css.searchResultTitle}>{result.title}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className={css.searchResultWorkspace}>{result.workspace}</span>
|
<span className={css.searchResultMeta}>
|
||||||
{result.snippet !== undefined && (
|
<span className={css.searchResultWorkspace}>{result.workspace}</span>
|
||||||
<span className={css.searchResultSnippet}>{result.snippet}</span>
|
{result.snippet !== undefined && (
|
||||||
)}
|
<span className={css.searchResultSnippet}>{result.snippet}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { defineStore, type EngineStoreHandle } from '@deepseek-ai/dsh-client-run
|
|||||||
|
|
||||||
/** Session-list grouping mode: workspace sections or one flat recency list. */
|
/** Session-list grouping mode: workspace sections or one flat recency list. */
|
||||||
export type WorkspaceGroupBy = 'workspace' | 'flat'
|
export type WorkspaceGroupBy = 'workspace' | 'flat'
|
||||||
/** Session order: durable Workspace order or a derived timestamp order. */
|
/** Session order: durable Workspace order or an activity-promoted editable order. */
|
||||||
export type WorkspaceOrderBy = 'manual' | 'created' | 'updated'
|
export type WorkspaceOrderBy = 'manual' | 'updated'
|
||||||
|
|
||||||
/** Workspace browser viewing state persisted across surface remounts and reloads. */
|
/** Workspace browser viewing state persisted across surface remounts and reloads. */
|
||||||
type WorkspaceViewState = {
|
type WorkspaceViewState = {
|
||||||
@@ -18,6 +18,10 @@ type WorkspaceViewState = {
|
|||||||
orderBy: WorkspaceOrderBy
|
orderBy: WorkspaceOrderBy
|
||||||
/** Explicit zero-or-five-session state keyed by Workspace group identity. */
|
/** Explicit zero-or-five-session state keyed by Workspace group identity. */
|
||||||
workspaceExpansion: Record<string, boolean>
|
workspaceExpansion: Record<string, boolean>
|
||||||
|
/** Editable per-Workspace order used by recent-update mode. */
|
||||||
|
recentSessionOrder: Record<string, string[]>
|
||||||
|
/** Last observed update timestamps used to detect promotion events. */
|
||||||
|
recentSessionUpdatedAt: Record<string, Record<string, number>>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,6 +32,13 @@ type WorkspaceViewActions = {
|
|||||||
setGroupBy: (draft: WorkspaceViewState, mode: WorkspaceGroupBy) => void
|
setGroupBy: (draft: WorkspaceViewState, mode: WorkspaceGroupBy) => void
|
||||||
setOrderBy: (draft: WorkspaceViewState, mode: WorkspaceOrderBy) => void
|
setOrderBy: (draft: WorkspaceViewState, mode: WorkspaceOrderBy) => void
|
||||||
setWorkspaceExpanded: (draft: WorkspaceViewState, key: string, expanded: boolean) => void
|
setWorkspaceExpanded: (draft: WorkspaceViewState, key: string, expanded: boolean) => void
|
||||||
|
syncRecentSessions: (
|
||||||
|
draft: WorkspaceViewState,
|
||||||
|
workspaceKey: string,
|
||||||
|
order: string[],
|
||||||
|
updatedAt: Record<string, number>,
|
||||||
|
) => void
|
||||||
|
setRecentSessionOrder: (draft: WorkspaceViewState, workspaceKey: string, order: string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,12 +47,25 @@ type WorkspaceViewActions = {
|
|||||||
*/
|
*/
|
||||||
export function createWorkspaceViewStore(): EngineStoreHandle<WorkspaceViewState, WorkspaceViewActions> {
|
export function createWorkspaceViewStore(): EngineStoreHandle<WorkspaceViewState, WorkspaceViewActions> {
|
||||||
return defineStore({
|
return defineStore({
|
||||||
init: (): WorkspaceViewState => ({ groupBy: 'workspace', orderBy: 'manual', workspaceExpansion: {} }),
|
init: (): WorkspaceViewState => ({
|
||||||
persist: 'dsh.workspace.view.v3',
|
groupBy: 'workspace',
|
||||||
|
orderBy: 'manual',
|
||||||
|
workspaceExpansion: {},
|
||||||
|
recentSessionOrder: {},
|
||||||
|
recentSessionUpdatedAt: {},
|
||||||
|
}),
|
||||||
|
persist: 'dsh.workspace.view.v4',
|
||||||
actions: {
|
actions: {
|
||||||
setGroupBy: (d, mode: WorkspaceGroupBy) => { d.groupBy = mode },
|
setGroupBy: (d, mode: WorkspaceGroupBy) => { d.groupBy = mode },
|
||||||
setOrderBy: (d, mode: WorkspaceOrderBy) => { d.orderBy = mode },
|
setOrderBy: (d, mode: WorkspaceOrderBy) => { d.orderBy = mode },
|
||||||
setWorkspaceExpanded: (d, key: string, expanded: boolean) => { d.workspaceExpansion[key] = expanded },
|
setWorkspaceExpanded: (d, key: string, expanded: boolean) => { d.workspaceExpansion[key] = expanded },
|
||||||
|
syncRecentSessions: (d, workspaceKey: string, order: string[], updatedAt: Record<string, number>) => {
|
||||||
|
d.recentSessionOrder[workspaceKey] = order
|
||||||
|
d.recentSessionUpdatedAt[workspaceKey] = updatedAt
|
||||||
|
},
|
||||||
|
setRecentSessionOrder: (d, workspaceKey: string, order: string[]) => {
|
||||||
|
d.recentSessionOrder[workspaceKey] = order
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ export interface SessionNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Session order selected by the Workspace browser. */
|
/** Session order selected by the Workspace browser. */
|
||||||
export type SessionOrderBy = 'manual' | 'created' | 'updated'
|
export type SessionOrderBy = 'manual' | 'updated'
|
||||||
|
|
||||||
/** One workspace group section: header row facts + visible top-level session rows. */
|
/** One workspace group section: header row facts + visible top-level session rows. */
|
||||||
export interface GroupNode {
|
export interface GroupNode {
|
||||||
@@ -108,14 +108,8 @@ function byRecency(a: SessionSummary, b: SessionSummary): number {
|
|||||||
return a.id < b.id ? -1 : 1
|
return a.id < b.id ? -1 : 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Newest-created first, id as the deterministic tiebreak. */
|
function sortSessions(sessions: SessionSummary[]): void {
|
||||||
function byCreation(a: SessionSummary, b: SessionSummary): number {
|
sessions.sort(byRecency)
|
||||||
if (b.createdAt !== a.createdAt) return b.createdAt - a.createdAt
|
|
||||||
return a.id < b.id ? -1 : 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortSessions(sessions: SessionSummary[], orderBy: Exclude<SessionOrderBy, 'manual'>): void {
|
|
||||||
sessions.sort(orderBy === 'created' ? byCreation : byRecency)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,7 +144,7 @@ function buildGroup(
|
|||||||
orderBy: SessionOrderBy,
|
orderBy: SessionOrderBy,
|
||||||
): Group {
|
): Group {
|
||||||
const sessions = [...members]
|
const sessions = [...members]
|
||||||
if (orderBy !== 'manual') sortSessions(sessions, orderBy)
|
if (orderBy !== 'manual') sortSessions(sessions)
|
||||||
return { key, workspaceId, cwd, createdAt, label, sessions }
|
return { key, workspaceId, cwd, createdAt, label, sessions }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user