+
+ {/* Create worktree section */}
+
+
Create worktree
-
- Branch-specific directory under {WORKTREE_ROOT}.
+
+ Branch-specific directory under {WORKTREE_ROOT}
-
{
- setWorktreeCreateMode(value);
- setWorktreeError(null);
-
- if (value === 'existing' && !existingWorktreeBranch) {
- const firstLocal = availableWorktreeBaseBranches.find((option) => option.group === 'local')?.value ?? '';
- if (firstLocal) {
- setExistingWorktreeBranch(firstLocal);
+
+
+
+
+ {/* Branch name input - inline when "New branch" is selected */}
+ {worktreeCreateMode === 'new' && (
+ handleBranchInputChange(e.target.value)}
+ placeholder="feature/new-branch"
+ className="h-8 flex-1 min-w-0 typography-meta text-foreground placeholder:text-muted-foreground/70"
+ disabled={!isGitRepo || isCheckingGitRepository}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && !isCreatingWorktree) {
+ handleCreateWorktree();
+ }
+ }}
+ />
+ )}
+
+
+ {/* Base branch selector - only shown when "New branch" is selected */}
+ {worktreeCreateMode === 'new' && (
<>
-
-
-
- handleBranchInputChange(e.target.value)}
- placeholder="feature/new-branch"
- className="h-8 flex-1 typography-meta text-foreground placeholder:text-muted-foreground/70"
- disabled={!isGitRepo || isCheckingGitRepository}
- onKeyDown={(e) => {
- if (e.key === 'Enter' && !isCreatingWorktree) {
- handleCreateWorktree();
- }
- }}
- />
-
-
- >
- ) : (
- <>
-
-
-
-
-
- {!isLoadingWorktreeBaseBranches && !availableWorktreeBaseBranches.some((option) => option.group === 'local') ? (
-
- No local branches found. Fetch or create a branch first.
-
- ) : null}
>
)}
+ {/* Preview info */}
{worktreeTargetBranch ? (
{worktreeCreateMode === 'existing' ? (
<>
Uses branch{' '}
{worktreeTargetBranch}
- {' '}at{' '}
-
- {formatPathForDisplay(worktreePreviewPath, homeDirectory)}
-
>
) : (
<>
- Creates branch{' '}
+ Creates{' '}
{worktreeTargetBranch}
{' '}from{' '}
{selectedWorktreeBaseLabel}
- {' '}at{' '}
-
- {formatPathForDisplay(worktreePreviewPath, homeDirectory)}
-
>
)}
) : null}
+
+ {/* Create button */}
+
{worktreeError &&
{worktreeError}
}
@@ -963,50 +1086,100 @@ export const SessionDialogs: React.FC = () => {
)}
- {}
-
-
+ {/* Setup commands section */}
+
+
+
+ Setup commands
+ {setupCommands.filter(cmd => cmd.trim()).length > 0 && (
+
+ {' '}({setupCommands.filter(cmd => cmd.trim()).length} configured)
+
+ )}
+
+
+
+
+
+
+ Commands run in the new worktree. Use $ROOT_WORKTREE_PATH for project root.
+
+ {isLoadingSetupCommands ? (
+
Loading...
+ ) : (
+
+ {setupCommands.map((command, index) => (
+
+ {
+ const newCommands = [...setupCommands];
+ newCommands[index] = e.target.value;
+ setSetupCommands(newCommands);
+ }}
+ placeholder="e.g., bun install"
+ className="h-8 flex-1 font-mono text-xs"
+ />
+
+
+ ))}
+
+
+ )}
+
+
+
+
+ {/* Existing worktrees section */}
+
+
Existing worktrees
{isLoadingWorktrees ? (
Loading worktrees…
) : availableWorktrees.length === 0 ? (
- No worktrees found under {WORKTREE_ROOT}.
+ No worktrees found under {WORKTREE_ROOT}
) : (
-
- {availableWorktrees.map((worktree) => {
-
- const relativePath = worktree.relativePath
- || (worktree.path.startsWith(projectDirectory + '/')
- ? worktree.path.slice(projectDirectory.length + 1)
- : worktree.path);
- return (
-
+ {availableWorktrees.map((worktree) => (
+
+
+ {worktree.label || worktree.branch || 'Detached HEAD'}
+
+
- );
- })}
+
+
+
+ ))}
)}
@@ -1153,7 +1326,7 @@ export const SessionDialogs: React.FC = () => {
Worktree Manager
{worktreeManagerBody}
-
{worktreeManagerActions}
+
{worktreeManagerActions}
)}
diff --git a/packages/ui/src/components/session/SessionSidebar.tsx b/packages/ui/src/components/session/SessionSidebar.tsx
index d0b9ae83..7a115a9e 100644
--- a/packages/ui/src/components/session/SessionSidebar.tsx
+++ b/packages/ui/src/components/session/SessionSidebar.tsx
@@ -24,15 +24,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog';
-import { Button } from '@/components/ui/button';
+
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
import {
@@ -347,10 +339,7 @@ export const SessionSidebar: React.FC
= ({
const safeStorage = React.useMemo(() => getSafeStorage(), []);
const [collapsedGroups, setCollapsedGroups] = React.useState>(new Set());
const [collapsedProjects, setCollapsedProjects] = React.useState>(new Set());
- const [pendingProjectClose, setPendingProjectClose] = React.useState<{
- id: string;
- label: string;
- } | null>(null);
+
const [projectRepoStatus, setProjectRepoStatus] = React.useState
);
};
diff --git a/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx b/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx
index 627bfbfe..900fea5d 100644
--- a/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx
+++ b/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx
@@ -1,6 +1,8 @@
import React from 'react';
import {
RiAddCircleLine,
+ RiAddLine,
+ RiArrowDownSLine,
RiCloseLine,
RiFileImageLine,
RiFileLine,
@@ -12,11 +14,14 @@ import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
+import { useProjectsStore } from '@/stores/useProjectsStore';
import { ModelMultiSelect, generateInstanceId, type ModelSelectionWithId } from '@/components/multirun/ModelMultiSelect';
import { BranchSelector, useBranchOptions } from '@/components/multirun/BranchSelector';
import { AgentSelector } from '@/components/multirun/AgentSelector';
import { isIMECompositionEvent } from '@/lib/ime';
+import { getWorktreeSetupCommands } from '@/lib/openchamberConfig';
import type { CreateMultiRunParams, MultiRunFileAttachment } from '@/types/multirun';
/** Max file size in bytes (10MB) */
@@ -53,12 +58,82 @@ export const AgentManagerEmptyState: React.FC
= ({
const [baseBranch, setBaseBranch] = React.useState('HEAD');
const [attachedFiles, setAttachedFiles] = React.useState([]);
const [isSubmitting, setIsSubmitting] = React.useState(false);
+ const [setupCommands, setSetupCommands] = React.useState([]);
+ const [isSetupCommandsOpen, setIsSetupCommandsOpen] = React.useState(false);
+ const [isLoadingSetupCommands, setIsLoadingSetupCommands] = React.useState(false);
const fileInputRef = React.useRef(null);
const textareaRef = React.useRef(null);
const currentDirectory = useDirectoryStore((state) => state.currentDirectory ?? null);
const { isGitRepository, isLoading: isLoadingBranches } = useBranchOptions(currentDirectory);
+
+ const vscodeWorkspaceFolder = React.useMemo(() => {
+ if (typeof window === 'undefined') {
+ return null;
+ }
+ const folder = (window as unknown as { __VSCODE_CONFIG__?: { workspaceFolder?: unknown } }).__VSCODE_CONFIG__?.workspaceFolder;
+ return typeof folder === 'string' && folder.trim().length > 0 ? folder.trim() : null;
+ }, []);
+
+ const isVSCodeRuntime = React.useMemo(() => {
+ if (typeof window === 'undefined') {
+ return false;
+ }
+ const apis = (window as unknown as { __OPENCHAMBER_RUNTIME_APIS__?: { runtime?: { isVSCode?: boolean } } }).__OPENCHAMBER_RUNTIME_APIS__;
+ return Boolean(apis?.runtime?.isVSCode);
+ }, []);
+
+ // Get project directory for setup commands
+ const activeProjectId = useProjectsStore((state) => state.activeProjectId);
+ const projects = useProjectsStore((state) => state.projects);
+ const projectDirectory = React.useMemo(() => {
+ // VS Code panel should always use the current workspace root.
+ if (isVSCodeRuntime && vscodeWorkspaceFolder) {
+ return vscodeWorkspaceFolder;
+ }
+
+ if (activeProjectId) {
+ const project = projects.find((p) => p.id === activeProjectId);
+ if (project?.path) return project.path;
+ }
+
+ const base = currentDirectory ?? vscodeWorkspaceFolder;
+ if (!base) return null;
+
+
+ const normalized = base.replace(/\\/g, '/').replace(/\/+$/, '') || base;
+ const marker = '/.openchamber/';
+ const markerIndex = normalized.indexOf(marker);
+ if (markerIndex > 0) return normalized.slice(0, markerIndex);
+ if (normalized.endsWith('/.openchamber')) return normalized.slice(0, normalized.length - '/.openchamber'.length);
+ return normalized;
+ }, [activeProjectId, projects, currentDirectory, vscodeWorkspaceFolder]);
+
+ // Load setup commands from config
+ React.useEffect(() => {
+ if (!projectDirectory) return;
+
+ let cancelled = false;
+ setIsLoadingSetupCommands(true);
+
+ (async () => {
+ try {
+ const commands = await getWorktreeSetupCommands(projectDirectory);
+ if (!cancelled) {
+ setSetupCommands(commands);
+ }
+ } catch {
+ // Ignore errors, start with empty commands
+ } finally {
+ if (!cancelled) {
+ setIsLoadingSetupCommands(false);
+ }
+ }
+ })();
+
+ return () => { cancelled = true; };
+ }, [projectDirectory]);
const handleAddModel = React.useCallback((model: ModelSelectionWithId) => {
if (selectedModels.length >= MAX_MODELS) {
@@ -153,6 +228,9 @@ export const AgentManagerEmptyState: React.FC = ({
}))
: undefined;
+ // Filter setup commands
+ const commandsToRun = setupCommands.filter(cmd => cmd.trim().length > 0);
+
await onCreateGroup?.({
name: groupName.trim(),
prompt: prompt.trim(),
@@ -160,6 +238,7 @@ export const AgentManagerEmptyState: React.FC = ({
agent: selectedAgent || undefined,
worktreeBaseBranch: baseBranch,
files,
+ setupCommands: commandsToRun.length > 0 ? commandsToRun : undefined,
});
// Reset form on success - only after onCreateGroup completes
@@ -228,6 +307,70 @@ export const AgentManagerEmptyState: React.FC = ({
+ {/* Setup commands collapsible */}
+
+
+
+ Setup commands
+ {setupCommands.filter(cmd => cmd.trim()).length > 0 && (
+
+ {' '}({setupCommands.filter(cmd => cmd.trim()).length} configured)
+
+ )}
+
+
+
+
+
+
+ Commands run in each new worktree. Use $ROOT_WORKTREE_PATH for project root.
+
+ {isLoadingSetupCommands ? (
+
Loading...
+ ) : (
+
+ {setupCommands.map((command, index) => (
+
+ {
+ const newCommands = [...setupCommands];
+ newCommands[index] = e.target.value;
+ setSetupCommands(newCommands);
+ }}
+ placeholder="e.g., bun install"
+ className="h-8 flex-1 font-mono text-xs"
+ />
+ {
+ const newCommands = setupCommands.filter((_, i) => i !== index);
+ setSetupCommands(newCommands);
+ }}
+ className="flex-shrink-0 flex h-8 w-8 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"
+ >
+
+
+
+ ))}
+
setSetupCommands([...setupCommands, ''])}
+ className="flex items-center gap-1.5 typography-meta text-muted-foreground hover:text-foreground transition-colors"
+ >
+
+ Add command
+
+
+ )}
+
+
+
+
{/* Agent Selection */}