feat: implement worktree session creation and management
- Added `createWorktreeSession` utility to handle the creation of new sessions with auto-generated worktrees. - Introduced branch name generation utilities for creating unique and friendly branch names. - Updated `CommandPalette` to trigger worktree session creation via a command. - Refactored keyboard shortcuts to support new worktree session creation with Shift + Cmd/Ctrl + N. - Enhanced project settings to include worktree defaults (branch prefix, base branch, auto-create option). - Updated persistence logic to preserve worktree defaults when saving project settings. - Modified menu actions to initiate new worktree sessions. - Ensured proper handling of existing branches to avoid conflicts during worktree creation.
This commit is contained in:
@@ -3,6 +3,7 @@ import { OpenChamberVisualSettings } from './OpenChamberVisualSettings';
|
||||
import { AboutSettings } from './AboutSettings';
|
||||
import { SessionRetentionSettings } from './SessionRetentionSettings';
|
||||
import { DefaultsSettings } from './DefaultsSettings';
|
||||
import { WorktreeSectionContent } from './WorktreeSectionContent';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isWebRuntime } from '@/lib/desktop';
|
||||
@@ -50,6 +51,8 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
||||
return <ChatSectionContent />;
|
||||
case 'sessions':
|
||||
return <SessionsSectionContent />;
|
||||
case 'worktree':
|
||||
return <WorktreeSectionContent />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||
import { AboutSettings } from './AboutSettings';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type OpenChamberSection = 'visual' | 'chat' | 'sessions';
|
||||
export type OpenChamberSection = 'visual' | 'chat' | 'sessions' | 'worktree';
|
||||
|
||||
interface OpenChamberSidebarProps {
|
||||
selectedSection: OpenChamberSection;
|
||||
@@ -34,6 +34,11 @@ const OPENCHAMBER_SECTION_GROUPS: SectionGroup[] = [
|
||||
label: 'Sessions',
|
||||
items: ['Defaults', 'Retention'],
|
||||
},
|
||||
{
|
||||
id: 'worktree',
|
||||
label: 'Worktree',
|
||||
items: ['Branch', 'Setup'],
|
||||
},
|
||||
];
|
||||
|
||||
export const OpenChamberSidebar: React.FC<OpenChamberSidebarProps> = ({
|
||||
|
||||
@@ -0,0 +1,571 @@
|
||||
import React from 'react';
|
||||
import { RiAddLine, RiCloseLine, RiDeleteBinLine, RiInformationLine } from '@remixicon/react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
import { useGitBranches, useIsGitRepo } from '@/stores/useGitStore';
|
||||
import { checkIsGitRepository, getGitBranches } from '@/lib/gitApi';
|
||||
import { getWorktreeSetupCommands, saveWorktreeSetupCommands } from '@/lib/openchamberConfig';
|
||||
import { listWorktrees, mapWorktreeToMetadata } from '@/lib/git/worktreeService';
|
||||
import { sessionEvents } from '@/lib/sessionEvents';
|
||||
import type { WorktreeMetadata } from '@/types/worktree';
|
||||
|
||||
type BranchOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
group: 'special' | 'local' | 'remote';
|
||||
};
|
||||
|
||||
export const WorktreeSectionContent: React.FC = () => {
|
||||
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
||||
const updateWorktreeDefaults = useProjectsStore((state) => state.updateWorktreeDefaults);
|
||||
|
||||
const projectPath = activeProject?.path ?? null;
|
||||
const worktreeDefaults = activeProject?.worktreeDefaults;
|
||||
|
||||
const isGitRepoFromStore = useIsGitRepo(projectPath);
|
||||
const branchesFromStore = useGitBranches(projectPath);
|
||||
|
||||
const { sessions, getWorktreeMetadata } = useSessionStore();
|
||||
|
||||
const [branchPrefix, setBranchPrefix] = React.useState(worktreeDefaults?.branchPrefix ?? '');
|
||||
const [baseBranch, setBaseBranch] = React.useState(worktreeDefaults?.baseBranch ?? 'HEAD');
|
||||
const [setupCommands, setSetupCommands] = React.useState<string[]>([]);
|
||||
const [isLoadingCommands, setIsLoadingCommands] = React.useState(false);
|
||||
const [isLoadingGit, setIsLoadingGit] = React.useState(false);
|
||||
const [isGitRepoLocal, setIsGitRepoLocal] = React.useState<boolean | null>(null);
|
||||
const [branchesLocal, setBranchesLocal] = React.useState<{ all: string[]; current: string } | null>(null);
|
||||
const [availableWorktrees, setAvailableWorktrees] = React.useState<WorktreeMetadata[]>([]);
|
||||
const [isLoadingWorktrees, setIsLoadingWorktrees] = React.useState(false);
|
||||
|
||||
const WORKTREE_ROOT = '.openchamber';
|
||||
|
||||
const joinWorktreePath = React.useCallback((projectDirectory: string, slug: string): string => {
|
||||
const normalizedProject = projectDirectory.replace(/\\/g, '/').replace(/\/+$/, '');
|
||||
const base = !normalizedProject || normalizedProject === '/'
|
||||
? `/${WORKTREE_ROOT}`
|
||||
: `${normalizedProject}/${WORKTREE_ROOT}`;
|
||||
return slug ? `${base}/${slug}` : base;
|
||||
}, []);
|
||||
|
||||
const refreshWorktrees = React.useCallback(async () => {
|
||||
if (!projectPath || isGitRepoLocal === false) return;
|
||||
|
||||
try {
|
||||
const worktrees = await listWorktrees(projectPath);
|
||||
const mapped = worktrees.map((info) => mapWorktreeToMetadata(projectPath, info));
|
||||
const worktreeRoot = joinWorktreePath(projectPath, '');
|
||||
const worktreePrefix = `${worktreeRoot}/`;
|
||||
const filtered = mapped.filter((item) => item.path.startsWith(worktreePrefix));
|
||||
setAvailableWorktrees(filtered);
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}, [projectPath, isGitRepoLocal, joinWorktreePath]);
|
||||
|
||||
// Load git info when project changes
|
||||
React.useEffect(() => {
|
||||
if (!projectPath) return;
|
||||
|
||||
let cancelled = false;
|
||||
setIsLoadingGit(true);
|
||||
setIsLoadingWorktrees(true);
|
||||
setIsGitRepoLocal(null);
|
||||
setBranchesLocal(null);
|
||||
setAvailableWorktrees([]);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const repoStatus = await checkIsGitRepository(projectPath);
|
||||
if (cancelled) return;
|
||||
setIsGitRepoLocal(repoStatus);
|
||||
|
||||
if (repoStatus) {
|
||||
const [branchData, worktrees] = await Promise.all([
|
||||
getGitBranches(projectPath),
|
||||
listWorktrees(projectPath).catch(() => []),
|
||||
]);
|
||||
|
||||
if (!cancelled) {
|
||||
if (branchData) {
|
||||
setBranchesLocal({ all: branchData.all, current: branchData.current });
|
||||
}
|
||||
|
||||
// Filter worktrees to only show those under .openchamber
|
||||
const mapped = worktrees.map((info) => mapWorktreeToMetadata(projectPath, info));
|
||||
const worktreeRoot = `${projectPath.replace(/\\/g, '/').replace(/\/+$/, '')}/${WORKTREE_ROOT}`;
|
||||
const worktreePrefix = `${worktreeRoot}/`;
|
||||
const filtered = mapped.filter((item) => item.path.startsWith(worktreePrefix));
|
||||
setAvailableWorktrees(filtered);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsLoadingGit(false);
|
||||
setIsLoadingWorktrees(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [projectPath]);
|
||||
|
||||
// Load setup commands
|
||||
React.useEffect(() => {
|
||||
if (!projectPath) return;
|
||||
|
||||
let cancelled = false;
|
||||
setIsLoadingCommands(true);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const commands = await getWorktreeSetupCommands(projectPath);
|
||||
if (!cancelled) {
|
||||
setSetupCommands(commands.length > 0 ? commands : ['']);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setSetupCommands(['']);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsLoadingCommands(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [projectPath]);
|
||||
|
||||
// Sync local state with store when project changes
|
||||
React.useEffect(() => {
|
||||
setBranchPrefix(worktreeDefaults?.branchPrefix ?? '');
|
||||
setBaseBranch(worktreeDefaults?.baseBranch ?? 'HEAD');
|
||||
}, [worktreeDefaults]);
|
||||
|
||||
// Use local branches if available, otherwise fall back to store
|
||||
const branches = branchesLocal ?? branchesFromStore;
|
||||
const isGitRepo = isGitRepoLocal ?? isGitRepoFromStore;
|
||||
|
||||
const branchOptions = React.useMemo<BranchOption[]>(() => {
|
||||
const options: BranchOption[] = [];
|
||||
const headLabel = branches?.current
|
||||
? `Current (HEAD: ${branches.current})`
|
||||
: 'Current (HEAD)';
|
||||
options.push({ value: 'HEAD', label: headLabel, group: 'special' });
|
||||
|
||||
if (branches) {
|
||||
const localBranches = branches.all
|
||||
.filter((name: string) => !name.startsWith('remotes/'))
|
||||
.sort((a: string, b: string) => a.localeCompare(b));
|
||||
|
||||
localBranches.forEach((name: string) => {
|
||||
options.push({ value: name, label: name, group: 'local' });
|
||||
});
|
||||
|
||||
const remoteBranches = branches.all
|
||||
.filter((name: string) => name.startsWith('remotes/'))
|
||||
.map((name: string) => name.replace(/^remotes\//, ''))
|
||||
.sort((a: string, b: string) => a.localeCompare(b));
|
||||
|
||||
remoteBranches.forEach((name: string) => {
|
||||
options.push({ value: name, label: name, group: 'remote' });
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [branches]);
|
||||
|
||||
// Track pending changes for save-on-unmount
|
||||
const pendingBranchPrefixRef = React.useRef<string | null>(null);
|
||||
|
||||
const handleBranchPrefixChange = React.useCallback((value: string) => {
|
||||
setBranchPrefix(value);
|
||||
pendingBranchPrefixRef.current = value;
|
||||
}, []);
|
||||
|
||||
const saveBranchPrefix = React.useCallback((value: string) => {
|
||||
if (!activeProject?.id) return;
|
||||
updateWorktreeDefaults(activeProject.id, { branchPrefix: value });
|
||||
pendingBranchPrefixRef.current = null;
|
||||
}, [activeProject?.id, updateWorktreeDefaults]);
|
||||
|
||||
const handleBranchPrefixBlur = React.useCallback(() => {
|
||||
saveBranchPrefix(branchPrefix);
|
||||
}, [branchPrefix, saveBranchPrefix]);
|
||||
|
||||
// Save pending changes on unmount
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (pendingBranchPrefixRef.current !== null && activeProject?.id) {
|
||||
updateWorktreeDefaults(activeProject.id, { branchPrefix: pendingBranchPrefixRef.current });
|
||||
}
|
||||
};
|
||||
}, [activeProject?.id, updateWorktreeDefaults]);
|
||||
|
||||
const handleBaseBranchChange = React.useCallback((value: string) => {
|
||||
setBaseBranch(value);
|
||||
if (!activeProject?.id) return;
|
||||
updateWorktreeDefaults(activeProject.id, { baseBranch: value });
|
||||
}, [activeProject?.id, updateWorktreeDefaults]);
|
||||
|
||||
const handleSetupCommandChange = React.useCallback((index: number, value: string) => {
|
||||
setSetupCommands((prev) => {
|
||||
const next = [...prev];
|
||||
next[index] = value;
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleAddCommand = React.useCallback(() => {
|
||||
setSetupCommands((prev) => [...prev, '']);
|
||||
}, []);
|
||||
|
||||
const handleRemoveCommand = React.useCallback((index: number) => {
|
||||
setSetupCommands((prev) => prev.filter((_, i) => i !== index));
|
||||
}, []);
|
||||
|
||||
const saveSetupCommands = React.useCallback(async () => {
|
||||
if (!projectPath) return;
|
||||
const filtered = setupCommands.filter((cmd) => cmd.trim().length > 0);
|
||||
await saveWorktreeSetupCommands(projectPath, filtered);
|
||||
}, [projectPath, setupCommands]);
|
||||
|
||||
// Save setup commands on blur
|
||||
const handleCommandBlur = React.useCallback(() => {
|
||||
saveSetupCommands();
|
||||
}, [saveSetupCommands]);
|
||||
|
||||
// Delete worktree handler
|
||||
const handleDeleteWorktree = React.useCallback((worktree: WorktreeMetadata) => {
|
||||
const normalizedWorktreePath = worktree.path.replace(/\\/g, '/').replace(/\/+$/, '');
|
||||
|
||||
// Find sessions linked to this worktree by:
|
||||
// 1. Worktree metadata path match
|
||||
// 2. Session directory match
|
||||
const directSessions = sessions.filter((session) => {
|
||||
// Check worktree metadata
|
||||
const metadata = getWorktreeMetadata(session.id);
|
||||
if (metadata?.path === worktree.path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check session directory
|
||||
const sessionDir = (session as { directory?: string }).directory;
|
||||
if (sessionDir) {
|
||||
const normalizedSessionDir = sessionDir.replace(/\\/g, '/').replace(/\/+$/, '');
|
||||
if (normalizedSessionDir === normalizedWorktreePath) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// Build a set of session IDs that are directly linked
|
||||
const directSessionIds = new Set(directSessions.map((s) => s.id));
|
||||
|
||||
// Find all subsessions recursively
|
||||
const findSubsessions = (parentIds: Set<string>): typeof sessions => {
|
||||
const subsessions = sessions.filter((session) => {
|
||||
const parentID = (session as { parentID?: string | null }).parentID;
|
||||
return parentID && parentIds.has(parentID);
|
||||
});
|
||||
if (subsessions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const subsessionIds = new Set(subsessions.map((s) => s.id));
|
||||
return [...subsessions, ...findSubsessions(subsessionIds)];
|
||||
};
|
||||
|
||||
const allSubsessions = findSubsessions(directSessionIds);
|
||||
|
||||
// Dedupe sessions (in case same session matched both ways)
|
||||
const seenIds = new Set<string>();
|
||||
const allSessions = [...directSessions, ...allSubsessions].filter((session) => {
|
||||
if (seenIds.has(session.id)) {
|
||||
return false;
|
||||
}
|
||||
seenIds.add(session.id);
|
||||
return true;
|
||||
});
|
||||
|
||||
sessionEvents.requestDelete({
|
||||
sessions: allSessions,
|
||||
mode: 'worktree',
|
||||
worktree,
|
||||
});
|
||||
}, [sessions, getWorktreeMetadata]);
|
||||
|
||||
|
||||
|
||||
// Refresh worktrees when sessions change (after deletion)
|
||||
const sessionsKey = React.useMemo(() => sessions.map(s => s.id).join(','), [sessions]);
|
||||
React.useEffect(() => {
|
||||
if (isGitRepoLocal && projectPath) {
|
||||
refreshWorktrees();
|
||||
}
|
||||
}, [sessionsKey, isGitRepoLocal, projectPath, refreshWorktrees]);
|
||||
|
||||
if (!projectPath) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">Worktree settings</h3>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
Select a project to configure worktree defaults.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoadingGit) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">Worktree settings</h3>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
Loading...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isGitRepo === false) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">Worktree settings</h3>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
Worktree settings are only available for Git repositories.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Branch prefix */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">Branch prefix</h3>
|
||||
<Tooltip delayDuration={1000}>
|
||||
<TooltipTrigger asChild>
|
||||
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8} className="max-w-xs">
|
||||
Prefix for auto-generated branch names when creating new worktrees.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
e.g. feature, bugfix, wip (no trailing slash)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
value={branchPrefix}
|
||||
onChange={(e) => handleBranchPrefixChange(e.target.value)}
|
||||
onBlur={handleBranchPrefixBlur}
|
||||
placeholder="feature"
|
||||
className="max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Default base branch */}
|
||||
<div className="space-y-4 border-t border-border/40 pt-6">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">Base branch</h3>
|
||||
<Tooltip delayDuration={1000}>
|
||||
<TooltipTrigger asChild>
|
||||
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8} className="max-w-xs">
|
||||
Default branch to create new worktrees from.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
Default branch for new worktree branches
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Select value={baseBranch} onValueChange={handleBaseBranchChange}>
|
||||
<SelectTrigger className="w-auto max-w-xs typography-meta text-foreground">
|
||||
<SelectValue placeholder="Select a branch" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Default</SelectLabel>
|
||||
{branchOptions
|
||||
.filter((option) => option.group === 'special')
|
||||
.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
|
||||
{branchOptions.some((option) => option.group === 'local') && (
|
||||
<>
|
||||
<SelectSeparator />
|
||||
<SelectGroup>
|
||||
<SelectLabel>Local branches</SelectLabel>
|
||||
{branchOptions
|
||||
.filter((option) => option.group === 'local')
|
||||
.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
{branchOptions.some((option) => option.group === 'remote') && (
|
||||
<>
|
||||
<SelectSeparator />
|
||||
<SelectGroup>
|
||||
<SelectLabel>Remote branches</SelectLabel>
|
||||
{branchOptions
|
||||
.filter((option) => option.group === 'remote')
|
||||
.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Setup commands */}
|
||||
<div className="space-y-4 border-t border-border/40 pt-6">
|
||||
<div className="space-y-1">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">Setup commands</h3>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
Run automatically when a new worktree is created.
|
||||
Use <code className="font-mono text-xs bg-sidebar-accent/50 px-1 rounded">$ROOT_WORKTREE_PATH</code> for the project root.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoadingCommands ? (
|
||||
<p className="typography-meta text-muted-foreground">Loading...</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{setupCommands.map((command, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
value={command}
|
||||
onChange={(e) => handleSetupCommandChange(index, e.target.value)}
|
||||
onBlur={handleCommandBlur}
|
||||
placeholder="e.g., bun install"
|
||||
className="flex-1 font-mono text-xs"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleRemoveCommand(index);
|
||||
// Save after removing
|
||||
setTimeout(saveSetupCommands, 0);
|
||||
}}
|
||||
className="flex-shrink-0 flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
aria-label="Remove command"
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddCommand}
|
||||
className="flex items-center gap-1.5 typography-meta text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<RiAddLine className="h-3.5 w-3.5" />
|
||||
Add command
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Existing worktrees */}
|
||||
<div className="space-y-4 border-t border-border/40 pt-6">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">Existing worktrees</h3>
|
||||
<Tooltip delayDuration={1000}>
|
||||
<TooltipTrigger asChild>
|
||||
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8} className="max-w-xs">
|
||||
Worktrees created under <code className="font-mono text-xs">.openchamber</code> directory.
|
||||
Deleting a worktree will also remove any linked sessions.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
Manage worktrees for this project
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoadingWorktrees ? (
|
||||
<p className="typography-meta text-muted-foreground">Loading worktrees...</p>
|
||||
) : availableWorktrees.length === 0 ? (
|
||||
<p className="typography-meta text-muted-foreground/70">
|
||||
No worktrees found under <code className="font-mono text-xs">.openchamber</code>
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{availableWorktrees.map((worktree) => (
|
||||
<div
|
||||
key={worktree.path}
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-sidebar-accent/30 transition-colors group"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="typography-meta text-foreground truncate">
|
||||
{worktree.label || worktree.branch || 'Detached HEAD'}
|
||||
</p>
|
||||
<p className="typography-micro text-muted-foreground/60 truncate">
|
||||
{worktree.relativePath || worktree.path}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteWorktree(worktree)}
|
||||
className="flex-shrink-0 flex h-7 w-7 items-center justify-center rounded text-muted-foreground/50 hover:text-destructive hover:bg-destructive/10 opacity-0 group-hover:opacity-100 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
aria-label={`Delete worktree ${worktree.branch || worktree.label}`}
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,13 +17,13 @@ import { useDeviceInfo } from '@/lib/device';
|
||||
import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiGitBranchLine, RiLayoutLeftLine, RiMoonLine, RiQuestionLine, RiRestartLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine, RiTimeLine } from '@remixicon/react';
|
||||
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
|
||||
import { getModifierLabel } from '@/lib/utils';
|
||||
import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
|
||||
|
||||
export const CommandPalette: React.FC = () => {
|
||||
const {
|
||||
isCommandPaletteOpen,
|
||||
setCommandPaletteOpen,
|
||||
setHelpDialogOpen,
|
||||
setSessionCreateDialogOpen,
|
||||
setActiveMainTab,
|
||||
setSettingsDialogOpen,
|
||||
setSessionSwitcherOpen,
|
||||
@@ -66,9 +66,9 @@ export const CommandPalette: React.FC = () => {
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleOpenAdvancedSession = () => {
|
||||
setSessionCreateDialogOpen(true);
|
||||
const handleCreateWorktreeSession = () => {
|
||||
handleClose();
|
||||
createWorktreeSession();
|
||||
};
|
||||
|
||||
const { isMobile } = useDeviceInfo();
|
||||
@@ -135,7 +135,7 @@ export const CommandPalette: React.FC = () => {
|
||||
<span>New Session</span>
|
||||
<CommandShortcut>{getModifierLabel()} + N</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleOpenAdvancedSession}>
|
||||
<CommandItem onSelect={handleCreateWorktreeSession}>
|
||||
<RiGitBranchLine className="mr-2 h-4 w-4" />
|
||||
<span>New Session with Worktree</span>
|
||||
<CommandShortcut>Shift + {getModifierLabel()} + N</CommandShortcut>
|
||||
|
||||
Reference in New Issue
Block a user