mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
Merge v0.11.0rc into feat/mobile-improvements-contributor
Resolves merge conflicts by keeping both features: - enableAiCommitMessages (from our branch) - defaultFeatureModel (from v0.11.0rc) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -161,7 +161,7 @@ export function BoardHeader({
|
||||
<div className={controlContainerClass} data-testid="worktrees-toggle-container">
|
||||
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
||||
<Label htmlFor="worktrees-toggle" className="text-sm font-medium cursor-pointer">
|
||||
Worktrees
|
||||
Worktree Bar
|
||||
</Label>
|
||||
<Switch
|
||||
id="worktrees-toggle"
|
||||
|
||||
@@ -117,73 +117,90 @@ export function CardActions({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isCurrentAutoTask && feature.status === 'in_progress' && (
|
||||
<>
|
||||
{/* Approve Plan button - shows when plan is generated and waiting for approval */}
|
||||
{feature.planSpec?.status === 'generated' && onApprovePlan && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-purple-600 hover:bg-purple-700 text-white animate-pulse"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onApprovePlan();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`approve-plan-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
Approve Plan
|
||||
</Button>
|
||||
)}
|
||||
{feature.skipTests && onManualVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onManualVerify();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`manual-verify-${feature.id}`}
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
Verify
|
||||
</Button>
|
||||
) : onResume ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onResume();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`resume-feature-${feature.id}`}
|
||||
>
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
Resume
|
||||
</Button>
|
||||
) : null}
|
||||
{onViewOutput && !feature.skipTests && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-7 text-[11px] px-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewOutput();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`view-output-inprogress-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isCurrentAutoTask &&
|
||||
(feature.status === 'in_progress' ||
|
||||
(typeof feature.status === 'string' && feature.status.startsWith('pipeline_'))) && (
|
||||
<>
|
||||
{/* Approve Plan button - shows when plan is generated and waiting for approval */}
|
||||
{feature.planSpec?.status === 'generated' && onApprovePlan && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-purple-600 hover:bg-purple-700 text-white animate-pulse"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onApprovePlan();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`approve-plan-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
Approve Plan
|
||||
</Button>
|
||||
)}
|
||||
{feature.skipTests && onManualVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onManualVerify();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`manual-verify-${feature.id}`}
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
Verify
|
||||
</Button>
|
||||
) : onResume ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onResume();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`resume-feature-${feature.id}`}
|
||||
>
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
Resume
|
||||
</Button>
|
||||
) : onVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onVerify();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`verify-feature-${feature.id}`}
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
Verify
|
||||
</Button>
|
||||
) : null}
|
||||
{onViewOutput && !feature.skipTests && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-7 text-[11px] px-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewOutput();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`view-output-inprogress-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isCurrentAutoTask && feature.status === 'verified' && (
|
||||
<>
|
||||
{/* Logs button */}
|
||||
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
FeatureTextFilePath as DescriptionTextFilePath,
|
||||
ImagePreviewMap,
|
||||
} from '@/components/ui/description-image-dropzone';
|
||||
import { Play, Cpu, FolderKanban } from 'lucide-react';
|
||||
import { Play, Cpu, FolderKanban, Settings2 } from 'lucide-react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
@@ -33,7 +34,7 @@ import {
|
||||
PlanningMode,
|
||||
Feature,
|
||||
} from '@/store/app-store';
|
||||
import type { ReasoningEffort, PhaseModelEntry } from '@automaker/types';
|
||||
import type { ReasoningEffort, PhaseModelEntry, AgentModel } from '@automaker/types';
|
||||
import { supportsReasoningEffort, isClaudeModel } from '@automaker/types';
|
||||
import {
|
||||
TestingTabContent,
|
||||
@@ -122,7 +123,7 @@ interface AddFeatureDialogProps {
|
||||
selectedNonMainWorktreeBranch?: string;
|
||||
/**
|
||||
* When true, forces the dialog to default to 'current' work mode (work on current branch).
|
||||
* This is used when the "Use selected worktree branch" setting is disabled.
|
||||
* This is used when the "Default to worktree mode" setting is disabled.
|
||||
*/
|
||||
forceCurrentBranchMode?: boolean;
|
||||
}
|
||||
@@ -152,6 +153,7 @@ export function AddFeatureDialog({
|
||||
forceCurrentBranchMode,
|
||||
}: AddFeatureDialogProps) {
|
||||
const isSpawnMode = !!parentFeature;
|
||||
const navigate = useNavigate();
|
||||
const [workMode, setWorkMode] = useState<WorkMode>('current');
|
||||
|
||||
// Form state
|
||||
@@ -187,7 +189,8 @@ export function AddFeatureDialog({
|
||||
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Get defaults from store
|
||||
const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees } = useAppStore();
|
||||
const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees, defaultFeatureModel } =
|
||||
useAppStore();
|
||||
|
||||
// Track previous open state to detect when dialog opens
|
||||
const wasOpenRef = useRef(false);
|
||||
@@ -207,7 +210,7 @@ export function AddFeatureDialog({
|
||||
);
|
||||
setPlanningMode(defaultPlanningMode);
|
||||
setRequirePlanApproval(defaultRequirePlanApproval);
|
||||
setModelEntry({ model: 'opus' });
|
||||
setModelEntry(defaultFeatureModel);
|
||||
|
||||
// Initialize description history (empty for new feature)
|
||||
setDescriptionHistory([]);
|
||||
@@ -228,6 +231,7 @@ export function AddFeatureDialog({
|
||||
defaultBranch,
|
||||
defaultPlanningMode,
|
||||
defaultRequirePlanApproval,
|
||||
defaultFeatureModel,
|
||||
useWorktrees,
|
||||
selectedNonMainWorktreeBranch,
|
||||
forceCurrentBranchMode,
|
||||
@@ -318,7 +322,7 @@ export function AddFeatureDialog({
|
||||
// When a non-main worktree is selected, use its branch name for custom mode
|
||||
setBranchName(selectedNonMainWorktreeBranch || '');
|
||||
setPriority(2);
|
||||
setModelEntry({ model: 'opus' });
|
||||
setModelEntry(defaultFeatureModel);
|
||||
setWorkMode(
|
||||
getDefaultWorkMode(useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode)
|
||||
);
|
||||
@@ -473,9 +477,31 @@ export function AddFeatureDialog({
|
||||
|
||||
{/* AI & Execution Section */}
|
||||
<div className={cardClass}>
|
||||
<div className={sectionHeaderClass}>
|
||||
<Cpu className="w-4 h-4 text-muted-foreground" />
|
||||
<span>AI & Execution</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={sectionHeaderClass}>
|
||||
<Cpu className="w-4 h-4 text-muted-foreground" />
|
||||
<span>AI & Execution</span>
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onOpenChange(false);
|
||||
navigate({ to: '/settings', search: { view: 'defaults' } });
|
||||
}}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Settings2 className="w-3.5 h-3.5" />
|
||||
<span>Edit Defaults</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Change default model and planning settings for new features</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
|
||||
@@ -419,7 +419,7 @@ export function BacklogPlanDialog({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">{renderContent()}</div>
|
||||
<div className="py-4 overflow-y-auto">{renderContent()}</div>
|
||||
|
||||
<DialogFooter>
|
||||
{mode === 'input' && (
|
||||
|
||||
@@ -117,7 +117,7 @@ export function CreatePRDialog({
|
||||
description: `PR already exists for ${result.result.branch}`,
|
||||
action: {
|
||||
label: 'View PR',
|
||||
onClick: () => window.open(result.result!.prUrl!, '_blank'),
|
||||
onClick: () => window.open(result.result!.prUrl!, '_blank', 'noopener,noreferrer'),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -125,7 +125,7 @@ export function CreatePRDialog({
|
||||
description: `PR created from ${result.result.branch}`,
|
||||
action: {
|
||||
label: 'View PR',
|
||||
onClick: () => window.open(result.result!.prUrl!, '_blank'),
|
||||
onClick: () => window.open(result.result!.prUrl!, '_blank', 'noopener,noreferrer'),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -251,7 +251,10 @@ export function CreatePRDialog({
|
||||
<p className="text-sm text-muted-foreground mt-1">Your PR is ready for review</p>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Button onClick={() => window.open(prUrl, '_blank')} className="gap-2">
|
||||
<Button
|
||||
onClick={() => window.open(prUrl, '_blank', 'noopener,noreferrer')}
|
||||
className="gap-2"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
View Pull Request
|
||||
</Button>
|
||||
@@ -277,7 +280,7 @@ export function CreatePRDialog({
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (browserUrl) {
|
||||
window.open(browserUrl, '_blank');
|
||||
window.open(browserUrl, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
}}
|
||||
className="gap-2 w-full"
|
||||
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
FeatureTextFilePath as DescriptionTextFilePath,
|
||||
ImagePreviewMap,
|
||||
} from '@/components/ui/description-image-dropzone';
|
||||
import { GitBranch, Cpu, FolderKanban } from 'lucide-react';
|
||||
import { GitBranch, Cpu, FolderKanban, Settings2 } from 'lucide-react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { toast } from 'sonner';
|
||||
import { cn, modelSupportsThinking } from '@/lib/utils';
|
||||
import { Feature, ModelAlias, ThinkingLevel, useAppStore, PlanningMode } from '@/store/app-store';
|
||||
@@ -86,6 +87,7 @@ export function EditFeatureDialog({
|
||||
isMaximized,
|
||||
allFeatures,
|
||||
}: EditFeatureDialogProps) {
|
||||
const navigate = useNavigate();
|
||||
const [editingFeature, setEditingFeature] = useState<Feature | null>(feature);
|
||||
// Derive initial workMode from feature's branchName
|
||||
const [workMode, setWorkMode] = useState<WorkMode>(() => {
|
||||
@@ -363,9 +365,31 @@ export function EditFeatureDialog({
|
||||
|
||||
{/* AI & Execution Section */}
|
||||
<div className={cardClass}>
|
||||
<div className={sectionHeaderClass}>
|
||||
<Cpu className="w-4 h-4 text-muted-foreground" />
|
||||
<span>AI & Execution</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={sectionHeaderClass}>
|
||||
<Cpu className="w-4 h-4 text-muted-foreground" />
|
||||
<span>AI & Execution</span>
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
navigate({ to: '/settings', search: { view: 'defaults' } });
|
||||
}}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Settings2 className="w-3.5 h-3.5" />
|
||||
<span>Edit Defaults</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Change default model and planning settings for new features</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
|
||||
@@ -13,7 +13,8 @@ import { Label } from '@/components/ui/label';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
import { Feature, ModelAlias, ThinkingLevel, PlanningMode } from '@/store/app-store';
|
||||
import { TestingTabContent, PrioritySelect, PlanningModeSelect } from '../shared';
|
||||
import { TestingTabContent, PrioritySelect, PlanningModeSelect, WorkModeSelector } from '../shared';
|
||||
import type { WorkMode } from '../shared';
|
||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||
import { isCursorModel, isClaudeModel, type PhaseModelEntry } from '@automaker/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -23,7 +24,10 @@ interface MassEditDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
selectedFeatures: Feature[];
|
||||
onApply: (updates: Partial<Feature>) => Promise<void>;
|
||||
onApply: (updates: Partial<Feature>, workMode: WorkMode) => Promise<void>;
|
||||
branchSuggestions: string[];
|
||||
branchCardCounts?: Record<string, number>;
|
||||
currentBranch?: string;
|
||||
}
|
||||
|
||||
interface ApplyState {
|
||||
@@ -33,6 +37,7 @@ interface ApplyState {
|
||||
requirePlanApproval: boolean;
|
||||
priority: boolean;
|
||||
skipTests: boolean;
|
||||
branchName: boolean;
|
||||
}
|
||||
|
||||
function getMixedValues(features: Feature[]): Record<string, boolean> {
|
||||
@@ -47,6 +52,7 @@ function getMixedValues(features: Feature[]): Record<string, boolean> {
|
||||
),
|
||||
priority: !features.every((f) => f.priority === first.priority),
|
||||
skipTests: !features.every((f) => f.skipTests === first.skipTests),
|
||||
branchName: !features.every((f) => f.branchName === first.branchName),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,7 +103,15 @@ function FieldWrapper({ label, isMixed, willApply, onApplyChange, children }: Fi
|
||||
);
|
||||
}
|
||||
|
||||
export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: MassEditDialogProps) {
|
||||
export function MassEditDialog({
|
||||
open,
|
||||
onClose,
|
||||
selectedFeatures,
|
||||
onApply,
|
||||
branchSuggestions,
|
||||
branchCardCounts,
|
||||
currentBranch,
|
||||
}: MassEditDialogProps) {
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
|
||||
// Track which fields to apply
|
||||
@@ -108,6 +122,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
requirePlanApproval: false,
|
||||
priority: false,
|
||||
skipTests: false,
|
||||
branchName: false,
|
||||
});
|
||||
|
||||
// Field values
|
||||
@@ -118,6 +133,18 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
const [priority, setPriority] = useState(2);
|
||||
const [skipTests, setSkipTests] = useState(false);
|
||||
|
||||
// Work mode and branch name state
|
||||
const [workMode, setWorkMode] = useState<WorkMode>(() => {
|
||||
// Derive initial work mode from first selected feature's branchName
|
||||
if (selectedFeatures.length > 0 && selectedFeatures[0].branchName) {
|
||||
return 'custom';
|
||||
}
|
||||
return 'current';
|
||||
});
|
||||
const [branchName, setBranchName] = useState(() => {
|
||||
return getInitialValue(selectedFeatures, 'branchName', '') as string;
|
||||
});
|
||||
|
||||
// Calculate mixed values
|
||||
const mixedValues = useMemo(() => getMixedValues(selectedFeatures), [selectedFeatures]);
|
||||
|
||||
@@ -131,6 +158,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
requirePlanApproval: false,
|
||||
priority: false,
|
||||
skipTests: false,
|
||||
branchName: false,
|
||||
});
|
||||
setModel(getInitialValue(selectedFeatures, 'model', 'sonnet') as ModelAlias);
|
||||
setThinkingLevel(getInitialValue(selectedFeatures, 'thinkingLevel', 'none') as ThinkingLevel);
|
||||
@@ -138,6 +166,10 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
setRequirePlanApproval(getInitialValue(selectedFeatures, 'requirePlanApproval', false));
|
||||
setPriority(getInitialValue(selectedFeatures, 'priority', 2));
|
||||
setSkipTests(getInitialValue(selectedFeatures, 'skipTests', false));
|
||||
// Reset work mode and branch name
|
||||
const initialBranchName = getInitialValue(selectedFeatures, 'branchName', '') as string;
|
||||
setBranchName(initialBranchName);
|
||||
setWorkMode(initialBranchName ? 'custom' : 'current');
|
||||
}
|
||||
}, [open, selectedFeatures]);
|
||||
|
||||
@@ -150,6 +182,12 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
if (applyState.requirePlanApproval) updates.requirePlanApproval = requirePlanApproval;
|
||||
if (applyState.priority) updates.priority = priority;
|
||||
if (applyState.skipTests) updates.skipTests = skipTests;
|
||||
if (applyState.branchName) {
|
||||
// For 'current' mode, use empty string (work on current branch)
|
||||
// For 'auto' mode, use empty string (will be auto-generated)
|
||||
// For 'custom' mode, use the specified branch name
|
||||
updates.branchName = workMode === 'custom' ? branchName : '';
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
onClose();
|
||||
@@ -158,7 +196,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
|
||||
setIsApplying(true);
|
||||
try {
|
||||
await onApply(updates);
|
||||
await onApply(updates, workMode);
|
||||
onClose();
|
||||
} finally {
|
||||
setIsApplying(false);
|
||||
@@ -293,6 +331,25 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
testIdPrefix="mass-edit"
|
||||
/>
|
||||
</FieldWrapper>
|
||||
|
||||
{/* Branch / Work Mode */}
|
||||
<FieldWrapper
|
||||
label="Branch / Work Mode"
|
||||
isMixed={mixedValues.branchName}
|
||||
willApply={applyState.branchName}
|
||||
onApplyChange={(apply) => setApplyState((prev) => ({ ...prev, branchName: apply }))}
|
||||
>
|
||||
<WorkModeSelector
|
||||
workMode={workMode}
|
||||
onWorkModeChange={setWorkMode}
|
||||
branchName={branchName}
|
||||
onBranchNameChange={setBranchName}
|
||||
branchSuggestions={branchSuggestions}
|
||||
branchCardCounts={branchCardCounts}
|
||||
currentBranch={currentBranch}
|
||||
testIdPrefix="mass-edit-work-mode"
|
||||
/>
|
||||
</FieldWrapper>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@@ -45,7 +45,7 @@ export function PlanSettingsDialog({
|
||||
className="text-sm font-medium cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<GitBranch className="w-4 h-4 text-brand-500" />
|
||||
Use selected worktree branch
|
||||
Default to worktree mode
|
||||
</Label>
|
||||
<Switch
|
||||
id="plan-worktree-branch-toggle"
|
||||
@@ -55,8 +55,8 @@ export function PlanSettingsDialog({
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
When enabled, features created via the Plan dialog will be assigned to the currently
|
||||
selected worktree branch. When disabled, features will be added to the main branch.
|
||||
Planned features will automatically use isolated worktrees, keeping changes separate
|
||||
from your main branch until you're ready to merge.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@ export function WorktreeSettingsDialog({
|
||||
className="text-sm font-medium cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<GitBranch className="w-4 h-4 text-brand-500" />
|
||||
Use selected worktree branch
|
||||
Default to worktree mode
|
||||
</Label>
|
||||
<Switch
|
||||
id="worktree-branch-toggle"
|
||||
@@ -55,8 +55,8 @@ export function WorktreeSettingsDialog({
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
When enabled, the Add Feature dialog will default to custom branch mode with the
|
||||
currently selected worktree branch pre-filled.
|
||||
New features will automatically use isolated worktrees, keeping changes separate
|
||||
from your main branch until you're ready to merge.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,7 +102,10 @@ export function useBoardEffects({
|
||||
const checkAllContexts = async () => {
|
||||
const featuresWithPotentialContext = features.filter(
|
||||
(f) =>
|
||||
f.status === 'in_progress' || f.status === 'waiting_approval' || f.status === 'verified'
|
||||
f.status === 'in_progress' ||
|
||||
f.status === 'waiting_approval' ||
|
||||
f.status === 'verified' ||
|
||||
(typeof f.status === 'string' && f.status.startsWith('pipeline_'))
|
||||
);
|
||||
const contextChecks = await Promise.all(
|
||||
featuresWithPotentialContext.map(async (f) => ({
|
||||
|
||||
@@ -148,8 +148,12 @@ export function WorktreeActionsDropdown({
|
||||
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||
Dev Server Running (:{devServerInfo?.port})
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => onOpenDevServerUrl(worktree)} className="text-xs">
|
||||
<Globe className="w-3.5 h-3.5 mr-2" />
|
||||
<DropdownMenuItem
|
||||
onClick={() => onOpenDevServerUrl(worktree)}
|
||||
className="text-xs"
|
||||
aria-label={`Open dev server on port ${devServerInfo?.port} in browser`}
|
||||
>
|
||||
<Globe className="w-3.5 h-3.5 mr-2" aria-hidden="true" />
|
||||
Open in Browser
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
@@ -325,7 +329,7 @@ export function WorktreeActionsDropdown({
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
window.open(worktree.pr!.url, '_blank');
|
||||
window.open(worktree.pr!.url, '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
|
||||
@@ -298,20 +298,29 @@ export function WorktreeTab({
|
||||
)}
|
||||
|
||||
{isDevServerRunning && (
|
||||
<Button
|
||||
variant={isSelected ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn(
|
||||
'h-7 w-7 p-0 rounded-none border-r-0',
|
||||
isSelected && 'bg-primary text-primary-foreground',
|
||||
!isSelected && 'bg-secondary/50 hover:bg-secondary',
|
||||
'text-green-500'
|
||||
)}
|
||||
onClick={() => onOpenDevServerUrl(worktree)}
|
||||
title={`Open dev server (port ${devServerInfo?.port})`}
|
||||
>
|
||||
<Globe className="w-3 h-3" />
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={isSelected ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn(
|
||||
'h-7 w-7 p-0 rounded-none border-r-0',
|
||||
isSelected && 'bg-primary text-primary-foreground',
|
||||
!isSelected && 'bg-secondary/50 hover:bg-secondary',
|
||||
'text-green-500'
|
||||
)}
|
||||
onClick={() => onOpenDevServerUrl(worktree)}
|
||||
aria-label={`Open dev server on port ${devServerInfo?.port} in browser`}
|
||||
>
|
||||
<Globe className="w-3 h-3" aria-hidden="true" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Open dev server (:{devServerInfo?.port})</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
<WorktreeActionsDropdown
|
||||
|
||||
@@ -118,8 +118,37 @@ export function useDevServers({ projectPath }: UseDevServersOptions) {
|
||||
const handleOpenDevServerUrl = useCallback(
|
||||
(worktree: WorktreeInfo) => {
|
||||
const serverInfo = runningDevServers.get(getWorktreeKey(worktree));
|
||||
if (serverInfo) {
|
||||
window.open(serverInfo.url, '_blank');
|
||||
if (!serverInfo) {
|
||||
logger.warn('No dev server info found for worktree:', getWorktreeKey(worktree));
|
||||
toast.error('Dev server not found', {
|
||||
description: 'The dev server may have stopped. Try starting it again.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Rewrite URL hostname to match the current browser's hostname.
|
||||
// This ensures dev server URLs work when accessing Automaker from
|
||||
// remote machines (e.g., 192.168.x.x or hostname.local instead of localhost).
|
||||
const devServerUrl = new URL(serverInfo.url);
|
||||
|
||||
// Security: Only allow http/https protocols to prevent potential attacks
|
||||
// via data:, javascript:, file:, or other dangerous URL schemes
|
||||
if (devServerUrl.protocol !== 'http:' && devServerUrl.protocol !== 'https:') {
|
||||
logger.error('Invalid dev server URL protocol:', devServerUrl.protocol);
|
||||
toast.error('Invalid dev server URL', {
|
||||
description: 'The server returned an unsupported URL protocol.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
devServerUrl.hostname = window.location.hostname;
|
||||
window.open(devServerUrl.toString(), '_blank', 'noopener,noreferrer');
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse dev server URL:', error);
|
||||
toast.error('Failed to open dev server', {
|
||||
description: 'The server URL could not be processed. Please try again.',
|
||||
});
|
||||
}
|
||||
},
|
||||
[runningDevServers, getWorktreeKey]
|
||||
|
||||
Reference in New Issue
Block a user