feat: update SessionSidebar to include new session creation and worktree handling

This commit is contained in:
Bohdan Triapitsyn
2026-01-08 00:29:20 +02:00
parent 29ba318f0f
commit d6f74087c4
2 changed files with 79 additions and 27 deletions
@@ -37,7 +37,6 @@ import {
RiErrorWarningLine, RiErrorWarningLine,
RiFileCopyLine, RiFileCopyLine,
RiGitBranchLine, RiGitBranchLine,
RiGitRepositoryLine,
RiLinkUnlinkM, RiLinkUnlinkM,
RiMore2Line, RiMore2Line,
RiPencilAiLine, RiPencilAiLine,
@@ -129,7 +128,7 @@ interface SortableProjectItemProps {
mobileVariant: boolean; mobileVariant: boolean;
onToggle: () => void; onToggle: () => void;
onHoverChange: (hovered: boolean) => void; onHoverChange: (hovered: boolean) => void;
onOpenWorktreeManager: () => void; onNewSession: () => void;
onOpenMultiRunLauncher: () => void; onOpenMultiRunLauncher: () => void;
onClose: () => void; onClose: () => void;
sentinelRef: (el: HTMLDivElement | null) => void; sentinelRef: (el: HTMLDivElement | null) => void;
@@ -150,7 +149,7 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
mobileVariant, mobileVariant,
onToggle, onToggle,
onHoverChange, onHoverChange,
onOpenWorktreeManager, onNewSession,
onOpenMultiRunLauncher, onOpenMultiRunLauncher,
onClose, onClose,
sentinelRef, sentinelRef,
@@ -233,17 +232,15 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[180px]"> <DropdownMenuContent align="end" className="min-w-[180px]">
<DropdownMenuItem onClick={onNewSession}>
<RiAddLine className="mr-1.5 h-4 w-4" />
New Session
</DropdownMenuItem>
{isRepo && !hideDirectoryControls && ( {isRepo && !hideDirectoryControls && (
<> <DropdownMenuItem onClick={onOpenMultiRunLauncher}>
<DropdownMenuItem onClick={onOpenWorktreeManager}> <ArrowsMerge className="mr-1.5 h-4 w-4" />
<RiGitRepositoryLine className="mr-1.5 h-4 w-4" /> New Multi-Run
Manage Worktrees </DropdownMenuItem>
</DropdownMenuItem>
<DropdownMenuItem onClick={onOpenMultiRunLauncher}>
<ArrowsMerge className="mr-1.5 h-4 w-4" />
New Multi-Run
</DropdownMenuItem>
</>
)} )}
<DropdownMenuItem <DropdownMenuItem
onClick={onClose} onClick={onClose}
@@ -377,6 +374,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const worktreeMetadata = useSessionStore((state) => state.worktreeMetadata); const worktreeMetadata = useSessionStore((state) => state.worktreeMetadata);
const availableWorktreesByProject = useSessionStore((state) => state.availableWorktreesByProject); const availableWorktreesByProject = useSessionStore((state) => state.availableWorktreesByProject);
const getSessionsByDirectory = useSessionStore((state) => state.getSessionsByDirectory); const getSessionsByDirectory = useSessionStore((state) => state.getSessionsByDirectory);
const openNewSessionDraft = useSessionStore((state) => state.openNewSessionDraft);
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => { const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
@@ -691,8 +689,30 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
// Check if this is a worktree session - if so, show confirmation dialog // Check if this is a worktree session - if so, show confirmation dialog
const worktree = worktreeMetadata.get(session.id); const worktree = worktreeMetadata.get(session.id);
if (worktree) { if (worktree) {
// Find ALL sessions linked to this worktree (not just the triggered one)
const worktreePath = worktree.path;
const allWorktreeSessions: Session[] = [];
const sessionIdSet = new Set<string>();
// Iterate through all sessions and find those linked to the same worktree
sessions.forEach((s) => {
const meta = worktreeMetadata.get(s.id);
if (meta && meta.path === worktreePath && !sessionIdSet.has(s.id)) {
allWorktreeSessions.push(s);
sessionIdSet.add(s.id);
// Also collect descendants of each worktree session
const sessionDescendants = collectDescendants(s.id);
sessionDescendants.forEach((desc) => {
if (!sessionIdSet.has(desc.id)) {
allWorktreeSessions.push(desc);
sessionIdSet.add(desc.id);
}
});
}
});
sessionEvents.requestDelete({ sessionEvents.requestDelete({
sessions: [session, ...descendants], sessions: allWorktreeSessions,
mode: 'worktree', mode: 'worktree',
worktree, worktree,
}); });
@@ -719,17 +739,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
} }
} }
}, },
[collectDescendants, deleteSession, deleteSessions, worktreeMetadata], [collectDescendants, deleteSession, deleteSessions, worktreeMetadata, sessions],
);
const handleOpenWorktreeManager = React.useCallback(
(projectId?: string | null) => {
if (projectId && projectId !== activeProjectId) {
setActiveProjectIdOnly(projectId);
}
sessionEvents.requestCreate({ worktreeMode: 'create', projectId: projectId ?? null });
},
[activeProjectId, setActiveProjectIdOnly],
); );
const handleOpenDirectoryDialog = React.useCallback(() => { const handleOpenDirectoryDialog = React.useCallback(() => {
@@ -1250,6 +1260,24 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
</DropdownMenuItem> </DropdownMenuItem>
</> </>
)} )}
{node.worktree ? (
<DropdownMenuItem
onClick={() => {
if (projectId && projectId !== activeProjectId) {
setActiveProject(projectId);
}
setActiveMainTab('chat');
if (mobileVariant) {
setSessionSwitcherOpen(false);
}
openNewSessionDraft({ directoryOverride: node.worktree?.path ?? null });
}}
className="[&>svg]:mr-1"
>
<RiGitBranchLine className="mr-1 h-4 w-4" />
New session in worktree
</DropdownMenuItem>
) : null}
<DropdownMenuItem <DropdownMenuItem
className="text-destructive focus:text-destructive [&>svg]:mr-1" className="text-destructive focus:text-destructive [&>svg]:mr-1"
onClick={() => handleDeleteSession(session)} onClick={() => handleDeleteSession(session)}
@@ -1289,6 +1317,11 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
handleDeleteSession, handleDeleteSession,
copiedSessionId, copiedSessionId,
mobileVariant, mobileVariant,
activeProjectId,
setActiveProject,
setActiveMainTab,
setSessionSwitcherOpen,
openNewSessionDraft,
], ],
); );
@@ -1497,7 +1530,16 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
mobileVariant={mobileVariant} mobileVariant={mobileVariant}
onToggle={() => toggleProject(projectKey)} onToggle={() => toggleProject(projectKey)}
onHoverChange={(hovered) => setHoveredProjectId(hovered ? projectKey : null)} onHoverChange={(hovered) => setHoveredProjectId(hovered ? projectKey : null)}
onOpenWorktreeManager={() => handleOpenWorktreeManager(projectKey)} onNewSession={() => {
if (projectKey !== activeProjectId) {
setActiveProject(projectKey);
}
setActiveMainTab('chat');
if (mobileVariant) {
setSessionSwitcherOpen(false);
}
openNewSessionDraft({ directoryOverride: project.normalizedPath });
}}
onOpenMultiRunLauncher={() => { onOpenMultiRunLauncher={() => {
if (projectKey !== activeProjectId) { if (projectKey !== activeProjectId) {
setActiveProject(projectKey); setActiveProject(projectKey);
+11 -1
View File
@@ -14,6 +14,7 @@ import { usePermissionStore } from "./permissionStore";
import { opencodeClient } from "@/lib/opencode/client"; import { opencodeClient } from "@/lib/opencode/client";
import { useDirectoryStore } from "./useDirectoryStore"; import { useDirectoryStore } from "./useDirectoryStore";
import { useConfigStore } from "./useConfigStore"; import { useConfigStore } from "./useConfigStore";
import { useProjectsStore } from "./useProjectsStore";
import { EXECUTION_FORK_META_TEXT } from "@/lib/messages/executionMeta"; import { EXECUTION_FORK_META_TEXT } from "@/lib/messages/executionMeta";
import { flattenAssistantTextParts } from "@/lib/messages/messageText"; import { flattenAssistantTextParts } from "@/lib/messages/messageText";
@@ -114,10 +115,19 @@ export const useSessionStore = create<SessionStore>()(
loadSessions: () => useSessionManagementStore.getState().loadSessions(), loadSessions: () => useSessionManagementStore.getState().loadSessions(),
openNewSessionDraft: (options) => { openNewSessionDraft: (options) => {
// Use explicit directoryOverride if provided, otherwise use active project path
let directory: string | null = null;
if (options?.directoryOverride !== undefined) {
directory = options.directoryOverride;
} else {
const activeProject = useProjectsStore.getState().getActiveProject();
directory = activeProject?.path ?? null;
}
set({ set({
newSessionDraft: { newSessionDraft: {
open: true, open: true,
directoryOverride: options?.directoryOverride ?? null, directoryOverride: directory,
parentID: options?.parentID ?? null, parentID: options?.parentID ?? null,
title: options?.title, title: options?.title,
}, },