feat: add optional multi-run isolation
Adds an Isolate runs toggle for Git projects Allows Git multi-runs to share the same directory Keeps non-Git runs unisolated with clear footer info
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
@@ -105,6 +106,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
|||||||
const [setupCommands, setSetupCommands] = React.useState<string[]>([]);
|
const [setupCommands, setSetupCommands] = React.useState<string[]>([]);
|
||||||
const [isSetupCommandsOpen, setIsSetupCommandsOpen] = React.useState(false);
|
const [isSetupCommandsOpen, setIsSetupCommandsOpen] = React.useState(false);
|
||||||
const [isLoadingSetupCommands, setIsLoadingSetupCommands] = React.useState(false);
|
const [isLoadingSetupCommands, setIsLoadingSetupCommands] = React.useState(false);
|
||||||
|
const [isolateRuns, setIsolateRuns] = React.useState(true);
|
||||||
const [showFileMention, setShowFileMention] = React.useState(false);
|
const [showFileMention, setShowFileMention] = React.useState(false);
|
||||||
const [mentionQuery, setMentionQuery] = React.useState('');
|
const [mentionQuery, setMentionQuery] = React.useState('');
|
||||||
const [showCommandAutocomplete, setShowCommandAutocomplete] = React.useState(false);
|
const [showCommandAutocomplete, setShowCommandAutocomplete] = React.useState(false);
|
||||||
@@ -301,6 +303,22 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
|||||||
// Use the BranchSelector hook for branch state management
|
// Use the BranchSelector hook for branch state management
|
||||||
const [worktreeBaseBranch, setWorktreeBaseBranch] = React.useState<string>('');
|
const [worktreeBaseBranch, setWorktreeBaseBranch] = React.useState<string>('');
|
||||||
const { isLoading: isLoadingWorktreeBaseBranches, isGitRepository } = useBranchOptions(selectedProjectDirectory);
|
const { isLoading: isLoadingWorktreeBaseBranches, isGitRepository } = useBranchOptions(selectedProjectDirectory);
|
||||||
|
const wasIsolationDisabledByNonGitRef = React.useRef(false);
|
||||||
|
const canIsolateRuns = isGitRepository === true;
|
||||||
|
const effectiveIsolateRuns = canIsolateRuns && isolateRuns;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isGitRepository === false) {
|
||||||
|
wasIsolationDisabledByNonGitRef.current = true;
|
||||||
|
setIsolateRuns(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGitRepository === true && wasIsolationDisabledByNonGitRef.current) {
|
||||||
|
wasIsolationDisabledByNonGitRef.current = false;
|
||||||
|
setIsolateRuns(true);
|
||||||
|
}
|
||||||
|
}, [isGitRepository]);
|
||||||
|
|
||||||
const createMultiRun = useMultiRunStore((state) => state.createMultiRun);
|
const createMultiRun = useMultiRunStore((state) => state.createMultiRun);
|
||||||
const error = useMultiRunStore((state) => state.error);
|
const error = useMultiRunStore((state) => state.error);
|
||||||
@@ -567,7 +585,8 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
|||||||
prompt: prompt.trim(),
|
prompt: prompt.trim(),
|
||||||
models: modelsForStore,
|
models: modelsForStore,
|
||||||
agent: selectedAgent || undefined,
|
agent: selectedAgent || undefined,
|
||||||
worktreeBaseBranch,
|
worktreeBaseBranch: effectiveIsolateRuns ? worktreeBaseBranch : undefined,
|
||||||
|
isolateRuns: effectiveIsolateRuns,
|
||||||
files: filesForStore.length > 0 ? filesForStore : undefined,
|
files: filesForStore.length > 0 ? filesForStore : undefined,
|
||||||
setupCommands: commandsForStore.length > 0 ? commandsForStore : undefined,
|
setupCommands: commandsForStore.length > 0 ? commandsForStore : undefined,
|
||||||
};
|
};
|
||||||
@@ -592,7 +611,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
|||||||
selectedModels.length >= 2 &&
|
selectedModels.length >= 2 &&
|
||||||
selectedProjectDirectory &&
|
selectedProjectDirectory &&
|
||||||
!isLoadingWorktreeBaseBranches &&
|
!isLoadingWorktreeBaseBranches &&
|
||||||
(isGitRepository === false || (isGitRepository === true && worktreeBaseBranch))
|
(isGitRepository === false || !effectiveIsolateRuns || worktreeBaseBranch)
|
||||||
);
|
);
|
||||||
|
|
||||||
const configuredSetupCount = setupCommands.filter(cmd => cmd.trim()).length;
|
const configuredSetupCount = setupCommands.filter(cmd => cmd.trim()).length;
|
||||||
@@ -698,8 +717,18 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
|||||||
directory={selectedProjectDirectory}
|
directory={selectedProjectDirectory}
|
||||||
value={worktreeBaseBranch}
|
value={worktreeBaseBranch}
|
||||||
onChange={setWorktreeBaseBranch}
|
onChange={setWorktreeBaseBranch}
|
||||||
|
disabled={!effectiveIsolateRuns}
|
||||||
id="multirun-worktree-base-branch"
|
id="multirun-worktree-base-branch"
|
||||||
/>
|
/>
|
||||||
|
<label className="mt-1.5 flex w-fit items-center gap-1.5 typography-micro text-muted-foreground">
|
||||||
|
<Checkbox
|
||||||
|
checked={isolateRuns}
|
||||||
|
onChange={setIsolateRuns}
|
||||||
|
disabled={!canIsolateRuns}
|
||||||
|
ariaLabel={t('multirun.launcher.isolateRuns.label')}
|
||||||
|
/>
|
||||||
|
<span>{t('multirun.launcher.isolateRuns.label')}</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Agent */}
|
{/* Agent */}
|
||||||
@@ -934,7 +963,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
|||||||
{/* ── Fixed footer ── */}
|
{/* ── Fixed footer ── */}
|
||||||
<div className="shrink-0 px-4 sm:px-6 py-3">
|
<div className="shrink-0 px-4 sm:px-6 py-3">
|
||||||
<div className="mx-auto w-full max-w-2xl flex items-center justify-end gap-2">
|
<div className="mx-auto w-full max-w-2xl flex items-center justify-end gap-2">
|
||||||
{isGitRepository === false ? (
|
{!effectiveIsolateRuns && isGitRepository !== null ? (
|
||||||
<div className="mr-auto flex min-w-0 items-center gap-1.5 typography-micro text-muted-foreground">
|
<div className="mr-auto flex min-w-0 items-center gap-1.5 typography-micro text-muted-foreground">
|
||||||
<Icon name="information" className="h-3.5 w-3.5 shrink-0" />
|
<Icon name="information" className="h-3.5 w-3.5 shrink-0" />
|
||||||
<span className="truncate">{t('multirun.launcher.project.gitRequired')}</span>
|
<span className="truncate">{t('multirun.launcher.project.gitRequired')}</span>
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ export const dict = {
|
|||||||
'multirun.launcher.groupName.placeholder': 'feature-auth, bugfix-login',
|
'multirun.launcher.groupName.placeholder': 'feature-auth, bugfix-login',
|
||||||
'multirun.launcher.baseBranch.label': 'Base branch',
|
'multirun.launcher.baseBranch.label': 'Base branch',
|
||||||
'multirun.launcher.baseBranch.info': 'New branch created from this base per model',
|
'multirun.launcher.baseBranch.info': 'New branch created from this base per model',
|
||||||
|
'multirun.launcher.isolateRuns.label': 'Isolate runs',
|
||||||
'multirun.launcher.agent.label': 'Agent',
|
'multirun.launcher.agent.label': 'Agent',
|
||||||
'multirun.launcher.agent.info': 'Agent used for all runs. Defaults to your configured agent.',
|
'multirun.launcher.agent.info': 'Agent used for all runs. Defaults to your configured agent.',
|
||||||
'multirun.launcher.setupCommands.label': 'Setup commands',
|
'multirun.launcher.setupCommands.label': 'Setup commands',
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"multirun.launcher.groupName.placeholder": "feature-auth, bugfix-login",
|
"multirun.launcher.groupName.placeholder": "feature-auth, bugfix-login",
|
||||||
"multirun.launcher.baseBranch.label": "Rama base",
|
"multirun.launcher.baseBranch.label": "Rama base",
|
||||||
"multirun.launcher.baseBranch.info": "Nueva rama creada desde esta base por modelo",
|
"multirun.launcher.baseBranch.info": "Nueva rama creada desde esta base por modelo",
|
||||||
|
"multirun.launcher.isolateRuns.label": "Aislar ejecuciones",
|
||||||
"multirun.launcher.agent.label": "Agente",
|
"multirun.launcher.agent.label": "Agente",
|
||||||
"multirun.launcher.agent.info": "Agente usado para todas las ejecuciones. Por defecto es el agente configurado.",
|
"multirun.launcher.agent.info": "Agente usado para todas las ejecuciones. Por defecto es el agente configurado.",
|
||||||
"multirun.launcher.setupCommands.label": "Comandos de configuración",
|
"multirun.launcher.setupCommands.label": "Comandos de configuración",
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'multirun.launcher.groupName.placeholder': 'feature-auth, bugfix-login',
|
'multirun.launcher.groupName.placeholder': 'feature-auth, bugfix-login',
|
||||||
'multirun.launcher.baseBranch.label': '기본 브랜치',
|
'multirun.launcher.baseBranch.label': '기본 브랜치',
|
||||||
'multirun.launcher.baseBranch.info': '각 모델은 이 기본 브랜치에서 새 브랜치를 만듭니다',
|
'multirun.launcher.baseBranch.info': '각 모델은 이 기본 브랜치에서 새 브랜치를 만듭니다',
|
||||||
|
'multirun.launcher.isolateRuns.label': '실행 격리',
|
||||||
'multirun.launcher.agent.label': '에이전트',
|
'multirun.launcher.agent.label': '에이전트',
|
||||||
'multirun.launcher.agent.info': '모든 실행에 사용할 에이전트입니다. 설정된 기본 에이전트를 사용합니다.',
|
'multirun.launcher.agent.info': '모든 실행에 사용할 에이전트입니다. 설정된 기본 에이전트를 사용합니다.',
|
||||||
'multirun.launcher.setupCommands.label': '셋업 명령',
|
'multirun.launcher.setupCommands.label': '셋업 명령',
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'multirun.launcher.groupName.placeholder': 'feature-auth, bugfix-login',
|
'multirun.launcher.groupName.placeholder': 'feature-auth, bugfix-login',
|
||||||
'multirun.launcher.baseBranch.label': 'Gałąź bazowa',
|
'multirun.launcher.baseBranch.label': 'Gałąź bazowa',
|
||||||
'multirun.launcher.baseBranch.info': 'Nowa gałąź tworzona z tej bazy dla każdego modelu',
|
'multirun.launcher.baseBranch.info': 'Nowa gałąź tworzona z tej bazy dla każdego modelu',
|
||||||
|
'multirun.launcher.isolateRuns.label': 'Izoluj uruchomienia',
|
||||||
'multirun.launcher.agent.label': 'Agent',
|
'multirun.launcher.agent.label': 'Agent',
|
||||||
'multirun.launcher.agent.info': 'Agent używany dla wszystkich uruchomień. Domyślnie twój skonfigurowany agent.',
|
'multirun.launcher.agent.info': 'Agent używany dla wszystkich uruchomień. Domyślnie twój skonfigurowany agent.',
|
||||||
'multirun.launcher.setupCommands.label': 'Polecenia konfiguracji',
|
'multirun.launcher.setupCommands.label': 'Polecenia konfiguracji',
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"multirun.launcher.groupName.placeholder": "feature-auth, bugfix-login",
|
"multirun.launcher.groupName.placeholder": "feature-auth, bugfix-login",
|
||||||
"multirun.launcher.baseBranch.label": "Branch base",
|
"multirun.launcher.baseBranch.label": "Branch base",
|
||||||
"multirun.launcher.baseBranch.info": "Cria uma nova branch a partir desta base para cada modelo",
|
"multirun.launcher.baseBranch.info": "Cria uma nova branch a partir desta base para cada modelo",
|
||||||
|
"multirun.launcher.isolateRuns.label": "Isolar execuções",
|
||||||
"multirun.launcher.agent.label": "Agente",
|
"multirun.launcher.agent.label": "Agente",
|
||||||
"multirun.launcher.agent.info": "Agente usado para todas as execuções. Por padrão, usa o agente configurado.",
|
"multirun.launcher.agent.info": "Agente usado para todas as execuções. Por padrão, usa o agente configurado.",
|
||||||
"multirun.launcher.setupCommands.label": "Comandos de configuração",
|
"multirun.launcher.setupCommands.label": "Comandos de configuração",
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"multirun.launcher.groupName.placeholder": "feature-auth, bugfix-login",
|
"multirun.launcher.groupName.placeholder": "feature-auth, bugfix-login",
|
||||||
"multirun.launcher.baseBranch.label": "Базова гілка",
|
"multirun.launcher.baseBranch.label": "Базова гілка",
|
||||||
"multirun.launcher.baseBranch.info": "Нова гілка, створена з цієї бази для кожної моделі",
|
"multirun.launcher.baseBranch.info": "Нова гілка, створена з цієї бази для кожної моделі",
|
||||||
|
"multirun.launcher.isolateRuns.label": "Ізолювати запуски",
|
||||||
"multirun.launcher.agent.label": "Агент",
|
"multirun.launcher.agent.label": "Агент",
|
||||||
"multirun.launcher.agent.info": "Агент використовується для всіх запусків. За замовчуванням налаштований агент.",
|
"multirun.launcher.agent.info": "Агент використовується для всіх запусків. За замовчуванням налаштований агент.",
|
||||||
"multirun.launcher.setupCommands.label": "Команди налаштування",
|
"multirun.launcher.setupCommands.label": "Команди налаштування",
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'multirun.launcher.groupName.placeholder': 'feature-auth, bugfix-login',
|
'multirun.launcher.groupName.placeholder': 'feature-auth, bugfix-login',
|
||||||
'multirun.launcher.baseBranch.label': '基础分支',
|
'multirun.launcher.baseBranch.label': '基础分支',
|
||||||
'multirun.launcher.baseBranch.info': '每个模型都会基于此分支创建新分支',
|
'multirun.launcher.baseBranch.info': '每个模型都会基于此分支创建新分支',
|
||||||
|
'multirun.launcher.isolateRuns.label': '隔离运行',
|
||||||
'multirun.launcher.agent.label': '智能体',
|
'multirun.launcher.agent.label': '智能体',
|
||||||
'multirun.launcher.agent.info': '所有运行使用同一智能体。默认使用你当前配置的智能体。',
|
'multirun.launcher.agent.info': '所有运行使用同一智能体。默认使用你当前配置的智能体。',
|
||||||
'multirun.launcher.setupCommands.label': '设置命令',
|
'multirun.launcher.setupCommands.label': '设置命令',
|
||||||
|
|||||||
@@ -118,10 +118,11 @@ export const useMultiRunStore = create<MultiRunStore>()(
|
|||||||
const directory = project.path;
|
const directory = project.path;
|
||||||
|
|
||||||
const isGit = await checkIsGitRepository(directory);
|
const isGit = await checkIsGitRepository(directory);
|
||||||
|
const shouldIsolateRuns = isGit && params.isolateRuns !== false;
|
||||||
|
|
||||||
const groupSlug = toGitSafeSlug(groupName);
|
const groupSlug = toGitSafeSlug(groupName);
|
||||||
const rootBranch = isGit ? await getRootBranch(directory) : undefined;
|
const rootBranch = shouldIsolateRuns ? await getRootBranch(directory) : undefined;
|
||||||
const rootTrackingRemote = isGit ? await resolveRootTrackingRemote(directory) : null;
|
const rootTrackingRemote = shouldIsolateRuns ? await resolveRootTrackingRemote(directory) : null;
|
||||||
|
|
||||||
const createdRuns: Array<{
|
const createdRuns: Array<{
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -161,7 +162,7 @@ export const useMultiRunStore = create<MultiRunStore>()(
|
|||||||
? `${groupSlug}/${model.providerID}/${model.modelID}/${index}`
|
? `${groupSlug}/${model.providerID}/${model.modelID}/${index}`
|
||||||
: `${groupSlug}/${model.providerID}/${model.modelID}`;
|
: `${groupSlug}/${model.providerID}/${model.modelID}`;
|
||||||
|
|
||||||
if (!isGit) {
|
if (!shouldIsolateRuns) {
|
||||||
const session = await opencodeClient.withDirectory(
|
const session = await opencodeClient.withDirectory(
|
||||||
directory,
|
directory,
|
||||||
() => opencodeClient.createSession({ title: sessionTitle })
|
() => opencodeClient.createSession({ title: sessionTitle })
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export interface CreateMultiRunParams {
|
|||||||
agent?: string;
|
agent?: string;
|
||||||
/** Base branch for new branches (defaults to `HEAD`). */
|
/** Base branch for new branches (defaults to `HEAD`). */
|
||||||
worktreeBaseBranch?: string;
|
worktreeBaseBranch?: string;
|
||||||
|
/** Whether Git projects should isolate each run in its own worktree. */
|
||||||
|
isolateRuns?: boolean;
|
||||||
/** Files to attach to all runs */
|
/** Files to attach to all runs */
|
||||||
files?: MultiRunFileAttachment[];
|
files?: MultiRunFileAttachment[];
|
||||||
/** Setup commands to run in each new worktree after creation */
|
/** Setup commands to run in each new worktree after creation */
|
||||||
|
|||||||
Reference in New Issue
Block a user