diff --git a/CLAUDE.md b/CLAUDE.md index cbaeeb4f..629953b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -596,6 +596,8 @@ SidePanel 折叠 UI:`ChatView` 把 `SidePanel` 包到 `
) : sessions.map((item, index) => { + const timeText = showSessionTime ? relativeTime(item.updated_at || item.created_at) : ''; const selectableIndex = files.length + index; const active = selectableIndex === selected; return ( @@ -315,6 +317,11 @@ export function PathReferenceDropdown({ {item.workspaceName} + {timeText && ( + + {timeText} + + )} ); diff --git a/web/src/components/Sidebar.jsx b/web/src/components/Sidebar.jsx index 29069847..732bcc6f 100644 --- a/web/src/components/Sidebar.jsx +++ b/web/src/components/Sidebar.jsx @@ -1,4 +1,5 @@ import { isDismissShortcut } from '../lib/shortcutBindings.js'; +import { Fragment } from 'react'; // Sidebar(200px):workspace 分组 → session list。 // // 数据源: @@ -59,6 +60,7 @@ import { SessionTitleText } from './SessionTitleText.jsx'; import { pushTrayMenu } from '../lib/desktopTrayMenu.js'; import { desktopTaskbarBadge, desktopTaskbarBadgeAvailable } from '../lib/desktopTaskbarBadge.js'; import { usePreference } from '../lib/usePreference.js'; +import { sidebarNodeKey, sidebarSessionRoots, sidebarTreeDropTarget } from '../lib/sidebarSessionTree.js'; import { createWorkspaceFolderOrderController } from '../lib/workspaceFolderOrder.js'; import { useWorkspaceFolderReorder } from '../lib/useWorkspaceFolderReorder.js'; import '../styles/workspaceFolderReorder.css'; @@ -88,7 +90,6 @@ import { expandedSessionListsAfterWorkspaceCollapseAll, expandedSessionListsAfterWorkspaceDisclosure, nextRemoteControlSurgeRequest, - reorderSidebarWorkspaceSession, remoteControlSurgeTargetKey, sessionListNeedsRevealExpansion, sessionMatchesRevealTarget, @@ -340,16 +341,26 @@ function pinnedDropTargetForPointer(clientY) { })); } -function workspaceDropTargetForPointer(clientY, workspaceHash) { +function workspaceDropTargetForPointer(clientY, workspaceHash, sourceId, clientX) { if (typeof document === 'undefined') return null; const workspace = String(workspaceHash || ''); const rows = Array.from(document.querySelectorAll('[data-sidebar-workspace-session-key]')) .filter((row) => row.dataset.sidebarWorkspaceSessionWorkspace === workspace); - return sidebarDropTargetForPointer(clientY, rows, (row) => ({ - targetWorkspaceHash: workspace, - targetId: row.dataset.sidebarWorkspaceSessionId || '', - targetKey: row.dataset.sidebarWorkspaceSessionKey || '', - })); + const entries = rows.map((row) => { + const rect = row.getBoundingClientRect(); + return { + key: row.dataset.sidebarTreeKey, + parent: row.dataset.sidebarTreeParent || '', + childCount: Number(row.dataset.sidebarTreeChildren) || 0, + scheduled: row.dataset.sidebarTreeScheduled === 'true', + id: row.dataset.sidebarWorkspaceSessionId, + dragKey: row.dataset.sidebarWorkspaceSessionKey, + top: rect.top, bottom: rect.bottom, left: rect.left, right: rect.right, + }; + }); + if (entries.length && clientX > Math.max(...entries.map(row => row.right)) + 10) return null; + const target = sidebarTreeDropTarget(entries, entries.find(row => row.id === sourceId), clientX, clientY); + return target ? { ...target, targetWorkspaceHash: workspace, targetId: target.id, targetKey: target.dragKey } : null; } function pinnedOrderItemsFromDom() { @@ -430,7 +441,7 @@ function startSidebarSessionPointerDrag({ timeText: drag.timeText, }); updateDragScroll(moveEvent.clientY); - updateDragTarget(moveEvent.clientY); + updateDragTarget(moveEvent.clientY, moveEvent.clientX); } function onUp(upEvent) { @@ -791,6 +802,8 @@ function SessionRow({ importHighlighted = false, remoteControlSurgeRequest = null, onRemoteControlSurgeCompleted, + treeExpanded = false, + onToggleTree, }) { const attention = s.attention_state || s.read_state || 'read'; const meta = attentionMeta(attention); @@ -807,6 +820,12 @@ function SessionRow({ const title = sessionDisplayTitle(s, s.name || ''); const titleAnimation = useSessionTitleAnimation(s, title); const sessionMarker = sidebarSessionMarker(s); + const hasTreeChildren = !pinned && Number(s.sidebar_child_count) > 0; + const showTreeArrow = hasTreeChildren && attention !== 'in_progress' && attention !== 'unread'; + const selectRow = () => { + onSelect?.(s); + if (hasTreeChildren) onToggleTree?.(s); + }; const remoteControlBound = Boolean(s.remote_control_bound ?? s.remoteControlBound); // 被 QQ / Telegram 会话绑定的会话显示同一个电脑图标;/rc 的专属行为(换绑动画等)仍只看 // remoteControlBound(openspec add-desktop-im-channels design D12)。 @@ -998,7 +1017,11 @@ function SessionRow({ data-sidebar-pinned-workspace={pinned ? pinScope || undefined : undefined} data-sidebar-workspace-session-key={workspaceReorderable ? rowKey || undefined : undefined} data-sidebar-workspace-session-id={workspaceReorderable ? s.id || undefined : undefined} - data-sidebar-workspace-session-workspace={workspaceReorderable ? workspaceHash || undefined : undefined} + data-sidebar-workspace-session-workspace={workspaceReorderable ? pinScope || undefined : undefined} + data-sidebar-tree-key={!pinned ? sidebarNodeKey(s) : undefined} + data-sidebar-tree-parent={!pinned ? s.sidebar_parent_key || '' : undefined} + data-sidebar-tree-children={!pinned ? s.sidebar_child_count || 0 : undefined} + data-sidebar-tree-scheduled={!pinned ? sessionMarker === 'loop' : undefined} aria-describedby={hoverCardVisible ? hoverCardId : undefined} className={clsx( 'ace-sidebar-session-row ace-sidebar-tree-row-grid ace-sidebar-primary-text group grid grid-cols-[24px_minmax(0,1fr)_auto] items-center gap-x-[7px] ml-1.5 mr-0 my-px pl-[3px] pr-[1px] rounded-md text-[14px] transition', @@ -1007,6 +1030,10 @@ function SessionRow({ dragging && 'is-dragging', dropPlacement === 'before' && 'is-drop-before', dropPlacement === 'after' && 'is-drop-after', + dropPlacement === 'inside' && 'is-drop-inside', + !pinned && s.sidebar_parent_key && 'ace-sidebar-session-tree-child', + active && 'is-tree-active', + hasTreeChildren && 'has-tree-children', !pinned && importHighlighted && 'is-opencode-import-highlight', remoteControlBound && 'is-remote-control-bound', active @@ -1052,23 +1079,22 @@ function SessionRow({ }); } }} - onBlurCapture={hoverDetails ? (event) => { + onBlurCapture={(event) => { if (!event.currentTarget.contains(event.relatedTarget)) { - sessionHoverLifecycle.dispatch({ + if (hoverDetails) sessionHoverLifecycle.dispatch({ type: SESSION_HOVER_LIFECYCLE_ACTIONS.KEYBOARD_LEAVE, owner: hoverCardId, }); } - } : undefined} - onPointerDown={pinned - ? (event) => onPinnedPointerDown?.(event, s, title) - : workspaceReorderable - ? (event) => onWorkspacePointerDown?.(event, s, title) - : undefined} + }} + onPointerDown={(event) => { + if (pinned) onPinnedPointerDown?.(event, s, title); + else if (workspaceReorderable) onWorkspacePointerDown?.(event, s, title); + }} onClick={(event) => { if (event.defaultPrevented) return; if (event.target?.closest?.('[data-sidebar-row-control="true"], input, textarea, select')) return; - onSelect?.(s); + selectRow(); }} > {remoteControlSurging && ( @@ -1078,8 +1104,11 @@ function SessionRow({ onAnimationEnd={() => finishRemoteControlSurge(activeRemoteControlSurgeSequenceRef.current)} /> )} - -