diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index 1dfa2275..c387bfa7 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -2062,7 +2062,7 @@ export const Header: React.FC = ({ void shareCurrentSession()}>{t('sessions.sidebar.session.menu.share')} )} void exportCurrentSession()}>{t('sessions.sidebar.session.menu.exportMarkdown')} - {!isVSCode && currentSession && !currentSession.parentId ? ( + {!isVSCode && !isChatContext && currentSession && !currentSession.parentId ? ( diff --git a/packages/ui/src/components/session/SessionSidebar.tsx b/packages/ui/src/components/session/SessionSidebar.tsx index 0e2d9c8a..fcb8461f 100644 --- a/packages/ui/src/components/session/SessionSidebar.tsx +++ b/packages/ui/src/components/session/SessionSidebar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { isChatDirectoryForHome, isChatDirectoryPath } from '@/lib/chatDirectories'; +import { getChatsRootForHome, getChatsRootFromDirectory, isChatDirectoryForHome, isChatDirectoryPath } from '@/lib/chatDirectories'; import { mergeSidebarSessionSources } from './sidebar/sidebarSessionSources'; import type { Session } from '@opencode-ai/sdk/v2'; import { toast } from '@/components/ui'; @@ -41,7 +41,7 @@ import { UpdateDialog } from '@/components/ui/UpdateDialog'; import { SessionGroupSection } from './sidebar/SessionGroupSection'; import { SidebarHeader } from './sidebar/SidebarHeader'; import { SidebarNav } from './sidebar/SidebarNav'; -import { SidebarActivitySections } from './sidebar/SidebarActivitySections'; +import { SidebarActivitySections, type ActivityItem } from './sidebar/SidebarActivitySections'; import { SidebarFooter } from './sidebar/SidebarFooter'; import { SidebarProjectsList } from './sidebar/SidebarProjectsList'; import { SessionNodeItem } from './sidebar/SessionNodeItem'; @@ -1730,6 +1730,8 @@ const SessionSidebarComponent: React.FC = ({ normalizedSessionSearchQuery, groupSearchDataByGroup, visibleSessionCountByGroup, + isSingleProjectMode, + sessionGroupingMode, collapsedGroups, hideDirectoryControls, collapsedFolderIds, @@ -1773,6 +1775,36 @@ const SessionSidebarComponent: React.FC = ({ openNewSessionDraft(); }, [mobileVariant, openNewSessionDraft, setActiveMainTab, setSessionSwitcherOpen]); + const renderChatsSection = React.useCallback((items: ActivityItem[]) => { + const chatsRoot = getChatsRootForHome(homeDirectory) + ?? items.map((item) => getChatsRootFromDirectory(item.node.session.directory)).find(Boolean) + ?? null; + if (!chatsRoot) return items.map((item) => renderSessionNode(item.node, 0, item.groupDirectory)); + + const folderDirectories = [ + chatsRoot, + ...items.map((item) => normalizePath(item.node.session.directory ?? null)).filter((directory): directory is string => Boolean(directory)), + ]; + const folderScopes = Array.from(new Set(folderDirectories)).map((directory) => ({ + scopeKey: directory, + directory, + })); + const group: SessionGroup = { + id: 'managed-chats', + label: '', + branch: null, + description: null, + isMain: true, + worktree: null, + directory: chatsRoot, + folderScopeKey: chatsRoot, + folderScopes, + draftTarget: 'chat', + sessions: items.map((item) => item.node), + }; + return renderGroupSessions(group, 'managed-chats', null, true); + }, [homeDirectory, renderGroupSessions, renderSessionNode]); + const topContent = React.useMemo( () => (!isVSCode && !hasSessionSearchQuery && hasActivitySectionItems) ? ( = ({ isDesktopShellRuntime={isDesktopShellRuntime} onNewChat={handleOpenNewSessionDraftFromHeader} alwaysShowActions={alwaysShowSidebarActions} + renderChatsSection={renderChatsSection} /> ) : null, - [activitySections, alwaysShowSidebarActions, editingId, handleOpenNewSessionDraftFromHeader, hasActivitySectionItems, hasSessionSearchQuery, isDesktopShellRuntime, isVSCode, openSidebarMenuKey, recentExpandedParents, renderSessionNode], + [activitySections, alwaysShowSidebarActions, editingId, handleOpenNewSessionDraftFromHeader, hasActivitySectionItems, hasSessionSearchQuery, isDesktopShellRuntime, isVSCode, openSidebarMenuKey, recentExpandedParents, renderChatsSection, renderSessionNode], ); const isInlineEditing = Boolean(renamingFolderId || editingId || editingProjectDialogId); diff --git a/packages/ui/src/components/session/sidebar/DOCUMENTATION.md b/packages/ui/src/components/session/sidebar/DOCUMENTATION.md index 044dc3d6..12c2b1ce 100644 --- a/packages/ui/src/components/session/sidebar/DOCUMENTATION.md +++ b/packages/ui/src/components/session/sidebar/DOCUMENTATION.md @@ -14,6 +14,10 @@ - Scheduled tasks (`ScheduledTasksDialog`, now a full-page surface on web/desktop) and per-project worktree management (`WorktreesView`, opened from the project menu) render as overlays inside `
` in `MainLayout`; the sidebar no longer mounts them. - Group-level PR-status polling/indicators and worktree-group drag-to-reorder were removed together with the worktree grouping level; `oc.sessions.groupOrder` is no longer read or written. Worktree PR/branch context lives in the Worktrees surface. - Root session menus can quickly create a worktree from the session directory's current branch and move the full session subtree there while idle. +- Managed Chats never offer the worktree-move action in either the sidebar row menu or the active-session header menu because their directories are not project repositories. +- Managed Chats use the shared Chats root as their folder scope. Their activity section renders the normal folder tree, and sessions created from a Chats folder are assigned back to that root-scoped folder after their date/session directory materializes. Per-session folder scopes created by older builds remain visible for compatibility. +- The New session keyboard command inherits the active materialized session directory. Explicit sidebar entry points, including the top New session row and the Chats `+`, open a fresh managed Chat draft instead. +- The new-worktree keyboard command is a silent no-op while a managed Chat draft is open. It must not retarget that draft to the active project or show a Git/worktree error because Chats never participate in worktrees. - Directory loading is demand-driven: the sidebar publishes one complete priority plan for all known project/worktree directories, while the sync layer owns bounded execution. - When multiple configured projects are checkouts of the same Git repository, exactly one project owns the shared worktree topology: the configured canonical primary root when present, otherwise the first configured source for that repository. Any worktree path that is also a configured project is omitted from subordinate worktree groups, so every directory has one sidebar location while remaining part of bootstrap demand. diff --git a/packages/ui/src/components/session/sidebar/SessionGroupSection.tsx b/packages/ui/src/components/session/sidebar/SessionGroupSection.tsx index 752c393c..9e6f2679 100644 --- a/packages/ui/src/components/session/sidebar/SessionGroupSection.tsx +++ b/packages/ui/src/components/session/sidebar/SessionGroupSection.tsx @@ -85,7 +85,7 @@ type Props = { setActiveProjectIdOnly: (id: string) => void; setActiveMainTab: (tab: MainTab) => void; setSessionSwitcherOpen: (open: boolean) => void; - openNewSessionDraft: (options?: { selectedProjectId?: string | null; directoryOverride?: string | null; targetFolderId?: string }) => void; + openNewSessionDraft: (options?: { selectedProjectId?: string | null; directoryOverride?: string | null; targetFolderId?: string; target?: 'chat' | 'project' }) => void; addSessionToFolder: (scopeKey: string, folderId: string, sessionId: string) => void; createFolderAndStartRename: (scopeKey: string, parentId?: string | null) => { id: string } | null; renamingFolderId: string | null; @@ -881,7 +881,12 @@ function SessionGroupSectionBase(props: Props): React.ReactNode { if (projectId && projectId !== activeProjectId) setActiveProjectIdOnly(projectId); setActiveMainTab('chat'); if (mobileVariant) setSessionSwitcherOpen(false); - openNewSessionDraft({ selectedProjectId: projectId, directoryOverride: scopeDirectory ?? group.directory, targetFolderId: folder.id }); + openNewSessionDraft({ + selectedProjectId: projectId, + directoryOverride: scopeDirectory ?? group.directory, + targetFolderId: folder.id, + target: group.draftTarget, + }); }} hideActions={false} archivedBucket={group.isArchivedBucket === true} diff --git a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx index ce216419..b1151f3b 100644 --- a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx +++ b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx @@ -43,6 +43,7 @@ import { getSessionGoal } from '@/lib/sessionGoalMetadata'; import { sessionGoalStatusColor, sessionGoalStatusLabelKey } from '@/lib/sessionGoalPresentation'; import { getRuntimeBearerTokenSync } from '@/lib/runtime-auth'; import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch'; +import { getChatsRootFromDirectory, isChatDirectoryPath } from '@/lib/chatDirectories'; import { parseMultiRunSessionTitle } from '@/lib/multirun/title'; import { MultiRunFusionDialog } from '@/components/multirun/MultiRunFusionDialog'; import { FusionIcon } from '@/components/icons/FusionIcon'; @@ -957,7 +958,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { {t('sessions.sidebar.session.menu.exportMarkdown')} - {!isSubtaskSession && !archivedBucket && !isVSCode ? ( + {!isSubtaskSession && !archivedBucket && !isVSCode && !isChatDirectoryPath(sessionDirectory) ? ( @@ -1015,6 +1016,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { .forEach((worktree) => pushScope(worktree.path)); } } + pushScope(getChatsRootFromDirectory(sessionDirectory)); pushScope(sessionDirectory); const folderEntries = scopes.flatMap((scope) => getFoldersForScope(scope).map((folder) => ({ scope, folder }))); diff --git a/packages/ui/src/components/session/sidebar/SidebarActivitySections.tsx b/packages/ui/src/components/session/sidebar/SidebarActivitySections.tsx index d61b66b3..8c11daaa 100644 --- a/packages/ui/src/components/session/sidebar/SidebarActivitySections.tsx +++ b/packages/ui/src/components/session/sidebar/SidebarActivitySections.tsx @@ -12,7 +12,7 @@ import { import type { SessionNodeRenderExtras } from './sessionNodeItemUtils'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -type ActivityItem = { +export type ActivityItem = { node: SessionNode; projectId: string | null; groupDirectory: string | null; @@ -49,6 +49,7 @@ type Props = { isDesktopShellRuntime: boolean; onNewChat?: () => void; alwaysShowActions?: boolean; + renderChatsSection?: (items: ActivityItem[]) => React.ReactNode; }; type RenderExtras = SessionNodeRenderExtras; @@ -151,7 +152,8 @@ export function SidebarActivitySections(props: Props): React.ReactNode { ); const visibleItems = section.items.slice(0, visibleLimit); const remainingCount = section.items.length - visibleItems.length; - const canShowFewer = !flatVariant && section.items.length > initialVisibleCount && remainingCount === 0; + const usesCustomRenderer = section.key === 'chats' && Boolean(props.renderChatsSection); + const canShowFewer = !usesCustomRenderer && !flatVariant && section.items.length > initialVisibleCount && remainingCount === 0; const getRenderExtras = buildRenderExtras(visibleItems.map((item) => item.node)); const renderItem = (item: ActivityItem) => renderSessionNode( item.node, @@ -240,8 +242,10 @@ export function SidebarActivitySections(props: Props): React.ReactNode { {!isCollapsed ? (
- {visibleItems.map(renderItem)} - {remainingCount > 0 ? ( + {section.key === 'chats' && props.renderChatsSection + ? props.renderChatsSection(section.items) + : visibleItems.map(renderItem)} + {!usesCustomRenderer && remainingCount > 0 ? (