mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 10:23:07 +00:00
Fix: Delete Worktree Crash + PR Comments + Dev Server UX Improvements (#792)
* Changes from fix/delete-worktree-hotifx * fix: Improve bot detection and prevent UI overflow issues - Include GitHub app-initiated comments in bot detection - Wrap handleQuickCreateSession with useCallback to fix dependency issues - Truncate long branch names in agent header to prevent layout overflow * feat: Support GitHub App comments in PR review and fix session filtering * feat: Return invalidation result from delete session handler
This commit is contained in:
@@ -20,9 +20,13 @@ import { AgentInputArea } from './agent-view/input-area';
|
||||
const LG_BREAKPOINT = 1024;
|
||||
|
||||
export function AgentView() {
|
||||
const { currentProject } = useAppStore();
|
||||
const { currentProject, getCurrentWorktree } = useAppStore();
|
||||
const [input, setInput] = useState('');
|
||||
const [currentTool, setCurrentTool] = useState<string | null>(null);
|
||||
|
||||
// Get the current worktree to scope sessions and agent working directory
|
||||
const currentWorktree = currentProject ? getCurrentWorktree(currentProject.path) : null;
|
||||
const effectiveWorkingDirectory = currentWorktree?.path || currentProject?.path;
|
||||
// Initialize session manager state - starts as true to match SSR
|
||||
// Then updates on mount based on actual screen size to prevent hydration mismatch
|
||||
const [showSessionManager, setShowSessionManager] = useState(true);
|
||||
@@ -52,9 +56,10 @@ export function AgentView() {
|
||||
// Guard to prevent concurrent invocations of handleCreateSessionFromEmptyState
|
||||
const createSessionInFlightRef = useRef(false);
|
||||
|
||||
// Session management hook
|
||||
// Session management hook - scoped to current worktree
|
||||
const { currentSessionId, handleSelectSession } = useAgentSession({
|
||||
projectPath: currentProject?.path,
|
||||
workingDirectory: effectiveWorkingDirectory,
|
||||
});
|
||||
|
||||
// Use the Electron agent hook (only if we have a session)
|
||||
@@ -71,7 +76,7 @@ export function AgentView() {
|
||||
clearServerQueue,
|
||||
} = useElectronAgent({
|
||||
sessionId: currentSessionId || '',
|
||||
workingDirectory: currentProject?.path,
|
||||
workingDirectory: effectiveWorkingDirectory,
|
||||
model: modelSelection.model,
|
||||
thinkingLevel: modelSelection.thinkingLevel,
|
||||
onToolUse: (toolName) => {
|
||||
@@ -229,6 +234,7 @@ export function AgentView() {
|
||||
currentSessionId={currentSessionId}
|
||||
onSelectSession={handleSelectSession}
|
||||
projectPath={currentProject.path}
|
||||
workingDirectory={effectiveWorkingDirectory}
|
||||
isCurrentSessionThinking={isProcessing}
|
||||
onQuickCreateRef={quickCreateSessionRef}
|
||||
/>
|
||||
@@ -248,6 +254,7 @@ export function AgentView() {
|
||||
showSessionManager={showSessionManager}
|
||||
onToggleSessionManager={() => setShowSessionManager(!showSessionManager)}
|
||||
onClearChat={handleClearChat}
|
||||
worktreeBranch={currentWorktree?.branch}
|
||||
/>
|
||||
|
||||
{/* Messages */}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Bot, PanelLeftClose, PanelLeft, Wrench, Trash2 } from 'lucide-react';
|
||||
import { Bot, PanelLeftClose, PanelLeft, Wrench, Trash2, GitBranch } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface AgentHeaderProps {
|
||||
@@ -11,6 +11,7 @@ interface AgentHeaderProps {
|
||||
showSessionManager: boolean;
|
||||
onToggleSessionManager: () => void;
|
||||
onClearChat: () => void;
|
||||
worktreeBranch?: string;
|
||||
}
|
||||
|
||||
export function AgentHeader({
|
||||
@@ -23,6 +24,7 @@ export function AgentHeader({
|
||||
showSessionManager,
|
||||
onToggleSessionManager,
|
||||
onClearChat,
|
||||
worktreeBranch,
|
||||
}: AgentHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border bg-card/50 backdrop-blur-sm">
|
||||
@@ -32,10 +34,18 @@ export function AgentHeader({
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-foreground">AI Agent</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{projectName}
|
||||
{currentSessionId && !isConnected && ' - Connecting...'}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>
|
||||
{projectName}
|
||||
{currentSessionId && !isConnected && ' - Connecting...'}
|
||||
</span>
|
||||
{worktreeBranch && (
|
||||
<span className="inline-flex items-center gap-1 text-xs bg-muted/50 px-2 py-0.5 rounded-full border border-border">
|
||||
<GitBranch className="w-3 h-3 shrink-0" />
|
||||
<span className="max-w-[180px] truncate">{worktreeBranch}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const logger = createLogger('AgentSession');
|
||||
|
||||
interface UseAgentSessionOptions {
|
||||
projectPath: string | undefined;
|
||||
workingDirectory?: string; // Current worktree path for per-worktree session persistence
|
||||
}
|
||||
|
||||
interface UseAgentSessionResult {
|
||||
@@ -13,49 +14,56 @@ interface UseAgentSessionResult {
|
||||
handleSelectSession: (sessionId: string | null) => void;
|
||||
}
|
||||
|
||||
export function useAgentSession({ projectPath }: UseAgentSessionOptions): UseAgentSessionResult {
|
||||
export function useAgentSession({
|
||||
projectPath,
|
||||
workingDirectory,
|
||||
}: UseAgentSessionOptions): UseAgentSessionResult {
|
||||
const { setLastSelectedSession, getLastSelectedSession } = useAppStore();
|
||||
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
||||
|
||||
// Track if initial session has been loaded
|
||||
const initialSessionLoadedRef = useRef(false);
|
||||
|
||||
// Use workingDirectory as the persistence key so sessions are scoped per worktree
|
||||
const persistenceKey = workingDirectory || projectPath;
|
||||
|
||||
// Handle session selection with persistence
|
||||
const handleSelectSession = useCallback(
|
||||
(sessionId: string | null) => {
|
||||
setCurrentSessionId(sessionId);
|
||||
// Persist the selection for this project
|
||||
if (projectPath) {
|
||||
setLastSelectedSession(projectPath, sessionId);
|
||||
// Persist the selection for this worktree/project
|
||||
if (persistenceKey) {
|
||||
setLastSelectedSession(persistenceKey, sessionId);
|
||||
}
|
||||
},
|
||||
[projectPath, setLastSelectedSession]
|
||||
[persistenceKey, setLastSelectedSession]
|
||||
);
|
||||
|
||||
// Restore last selected session when switching to Agent view or when project changes
|
||||
// Restore last selected session when switching to Agent view or when worktree changes
|
||||
useEffect(() => {
|
||||
if (!projectPath) {
|
||||
if (!persistenceKey) {
|
||||
// No project, reset
|
||||
setCurrentSessionId(null);
|
||||
initialSessionLoadedRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only restore once per project
|
||||
// Only restore once per persistence key
|
||||
if (initialSessionLoadedRef.current) return;
|
||||
initialSessionLoadedRef.current = true;
|
||||
|
||||
const lastSessionId = getLastSelectedSession(projectPath);
|
||||
const lastSessionId = getLastSelectedSession(persistenceKey);
|
||||
if (lastSessionId) {
|
||||
logger.info('Restoring last selected session:', lastSessionId);
|
||||
setCurrentSessionId(lastSessionId);
|
||||
}
|
||||
}, [projectPath, getLastSelectedSession]);
|
||||
}, [persistenceKey, getLastSelectedSession]);
|
||||
|
||||
// Reset initialSessionLoadedRef when project changes
|
||||
// Reset when worktree/project changes - clear current session and allow restore
|
||||
useEffect(() => {
|
||||
initialSessionLoadedRef.current = false;
|
||||
}, [projectPath]);
|
||||
setCurrentSessionId(null);
|
||||
}, [persistenceKey]);
|
||||
|
||||
return {
|
||||
currentSessionId,
|
||||
|
||||
@@ -486,6 +486,11 @@ export function BoardView() {
|
||||
} else {
|
||||
// Specific worktree selected - find it by path
|
||||
found = worktrees.find((w) => !w.isMain && pathsEqual(w.path, currentWorktreePath));
|
||||
// If the selected worktree no longer exists (e.g. just deleted),
|
||||
// fall back to main to prevent rendering with undefined worktree
|
||||
if (!found) {
|
||||
found = worktrees.find((w) => w.isMain);
|
||||
}
|
||||
}
|
||||
if (!found) return undefined;
|
||||
// Ensure all required WorktreeInfo fields are present
|
||||
@@ -1953,6 +1958,16 @@ export function BoardView() {
|
||||
}
|
||||
defaultDeleteBranch={getDefaultDeleteBranch(currentProject.path)}
|
||||
onDeleted={(deletedWorktree, _deletedBranch) => {
|
||||
// If the deleted worktree was currently selected, immediately reset to main
|
||||
// to prevent the UI from trying to render a non-existent worktree view
|
||||
if (
|
||||
currentWorktreePath !== null &&
|
||||
pathsEqual(currentWorktreePath, deletedWorktree.path)
|
||||
) {
|
||||
const mainBranch = worktrees.find((w) => w.isMain)?.branch || 'main';
|
||||
setCurrentWorktree(currentProject.path, null, mainBranch);
|
||||
}
|
||||
|
||||
// Reset features that were assigned to the deleted worktree (by branch)
|
||||
hookFeatures.forEach((feature) => {
|
||||
// Match by branch name since worktreePath is no longer stored
|
||||
|
||||
@@ -217,9 +217,14 @@ export function useBoardActions({
|
||||
const needsTitleGeneration =
|
||||
!titleWasGenerated && !featureData.title.trim() && featureData.description.trim();
|
||||
|
||||
const initialStatus = featureData.initialStatus || 'backlog';
|
||||
const {
|
||||
initialStatus: requestedStatus,
|
||||
workMode: _workMode,
|
||||
...restFeatureData
|
||||
} = featureData;
|
||||
const initialStatus = requestedStatus || 'backlog';
|
||||
const newFeatureData = {
|
||||
...featureData,
|
||||
...restFeatureData,
|
||||
title: titleWasGenerated ? titleForBranch : featureData.title,
|
||||
titleGenerating: needsTitleGeneration,
|
||||
status: initialStatus,
|
||||
@@ -1161,10 +1166,15 @@ export function useBoardActions({
|
||||
|
||||
const handleDuplicateFeature = useCallback(
|
||||
async (feature: Feature, asChild: boolean = false) => {
|
||||
// Copy all feature data, stripping id, status (handled by create), and runtime/state fields
|
||||
// Copy all feature data, stripping id, status (handled by create), and runtime/state fields.
|
||||
// Also strip initialStatus and workMode which are transient creation parameters that
|
||||
// should not carry over to duplicates (initialStatus: 'in_progress' would cause
|
||||
// the duplicate to immediately appear in "In Progress" instead of "Backlog").
|
||||
const {
|
||||
id: _id,
|
||||
status: _status,
|
||||
initialStatus: _initialStatus,
|
||||
workMode: _workMode,
|
||||
startedAt: _startedAt,
|
||||
error: _error,
|
||||
summary: _summary,
|
||||
@@ -1212,6 +1222,8 @@ export function useBoardActions({
|
||||
const {
|
||||
id: _id,
|
||||
status: _status,
|
||||
initialStatus: _initialStatus,
|
||||
workMode: _workMode,
|
||||
startedAt: _startedAt,
|
||||
error: _error,
|
||||
summary: _summary,
|
||||
|
||||
@@ -399,29 +399,57 @@ export function WorktreeActionsDropdown({
|
||||
Open in Browser
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={() => onViewDevServerLogs(worktree)} className="text-xs">
|
||||
<ScrollText className="w-3.5 h-3.5 mr-2" />
|
||||
View Logs
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onStopDevServer(worktree)}
|
||||
className="text-xs text-destructive focus:text-destructive"
|
||||
>
|
||||
<Square className="w-3.5 h-3.5 mr-2" />
|
||||
Stop Dev Server
|
||||
</DropdownMenuItem>
|
||||
{/* Stop Dev Server - split button: click main area to stop, chevron for view logs */}
|
||||
<DropdownMenuSub>
|
||||
<div className="flex items-center">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onStopDevServer(worktree)}
|
||||
className="text-xs flex-1 pr-0 rounded-r-none text-destructive focus:text-destructive"
|
||||
>
|
||||
<Square className="w-3.5 h-3.5 mr-2" />
|
||||
Stop Dev Server
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSubTrigger className="text-xs px-1 rounded-l-none border-l border-border/30 h-8" />
|
||||
</div>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem onClick={() => onViewDevServerLogs(worktree)} className="text-xs">
|
||||
<ScrollText className="w-3.5 h-3.5 mr-2" />
|
||||
View Dev Server Logs
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onStartDevServer(worktree)}
|
||||
disabled={isStartingDevServer}
|
||||
className="text-xs"
|
||||
>
|
||||
<Play className={cn('w-3.5 h-3.5 mr-2', isStartingDevServer && 'animate-pulse')} />
|
||||
{isStartingDevServer ? 'Starting...' : 'Start Dev Server'}
|
||||
</DropdownMenuItem>
|
||||
{/* Start Dev Server - split button: click main area to start, chevron for view logs */}
|
||||
<DropdownMenuSub>
|
||||
<div className="flex items-center">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onStartDevServer(worktree)}
|
||||
disabled={isStartingDevServer}
|
||||
className="text-xs flex-1 pr-0 rounded-r-none"
|
||||
>
|
||||
<Play
|
||||
className={cn('w-3.5 h-3.5 mr-2', isStartingDevServer && 'animate-pulse')}
|
||||
/>
|
||||
{isStartingDevServer ? 'Starting...' : 'Start Dev Server'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSubTrigger
|
||||
className={cn(
|
||||
'text-xs px-1 rounded-l-none border-l border-border/30 h-8',
|
||||
isStartingDevServer && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
disabled={isStartingDevServer}
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem onClick={() => onViewDevServerLogs(worktree)} className="text-xs">
|
||||
<ScrollText className="w-3.5 h-3.5 mr-2" />
|
||||
View Dev Server Logs
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -456,13 +456,20 @@ export function useDevServers({ projectPath }: UseDevServersOptions) {
|
||||
});
|
||||
// Start port detection timeout
|
||||
startPortDetectionTimer(key);
|
||||
toast.success('Dev server started, detecting port...');
|
||||
toast.success('Dev server started, detecting port...', {
|
||||
description: 'Logs are now visible in the dev server panel.',
|
||||
});
|
||||
} else {
|
||||
toast.error(result.error || 'Failed to start dev server');
|
||||
toast.error(result.error || 'Failed to start dev server', {
|
||||
description: 'Check the dev server logs panel for details.',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Start dev server failed:', error);
|
||||
toast.error('Failed to start dev server');
|
||||
toast.error('Failed to start dev server', {
|
||||
description:
|
||||
error instanceof Error ? error.message : 'Check the dev server logs panel for details.',
|
||||
});
|
||||
} finally {
|
||||
setIsStartingDevServer(false);
|
||||
}
|
||||
|
||||
@@ -659,6 +659,18 @@ export function WorktreePanel({
|
||||
// Keep logPanelWorktree set for smooth close animation
|
||||
}, []);
|
||||
|
||||
// Wrap handleStartDevServer to auto-open the logs panel so the user
|
||||
// can see output immediately (including failure reasons)
|
||||
const handleStartDevServerAndShowLogs = useCallback(
|
||||
async (worktree: WorktreeInfo) => {
|
||||
// Open logs panel immediately so output is visible from the start
|
||||
setLogPanelWorktree(worktree);
|
||||
setLogPanelOpen(true);
|
||||
await handleStartDevServer(worktree);
|
||||
},
|
||||
[handleStartDevServer]
|
||||
);
|
||||
|
||||
// Handle opening the push to remote dialog
|
||||
const handlePushNewBranch = useCallback((worktree: WorktreeInfo) => {
|
||||
setPushToRemoteWorktree(worktree);
|
||||
@@ -937,7 +949,7 @@ export function WorktreePanel({
|
||||
onResolveConflicts={onResolveConflicts}
|
||||
onMerge={handleMerge}
|
||||
onDeleteWorktree={onDeleteWorktree}
|
||||
onStartDevServer={handleStartDevServer}
|
||||
onStartDevServer={handleStartDevServerAndShowLogs}
|
||||
onStopDevServer={handleStopDevServer}
|
||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||
onViewDevServerLogs={handleViewDevServerLogs}
|
||||
@@ -1181,7 +1193,7 @@ export function WorktreePanel({
|
||||
onResolveConflicts={onResolveConflicts}
|
||||
onMerge={handleMerge}
|
||||
onDeleteWorktree={onDeleteWorktree}
|
||||
onStartDevServer={handleStartDevServer}
|
||||
onStartDevServer={handleStartDevServerAndShowLogs}
|
||||
onStopDevServer={handleStopDevServer}
|
||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||
onViewDevServerLogs={handleViewDevServerLogs}
|
||||
@@ -1288,7 +1300,7 @@ export function WorktreePanel({
|
||||
onResolveConflicts={onResolveConflicts}
|
||||
onMerge={handleMerge}
|
||||
onDeleteWorktree={onDeleteWorktree}
|
||||
onStartDevServer={handleStartDevServer}
|
||||
onStartDevServer={handleStartDevServerAndShowLogs}
|
||||
onStopDevServer={handleStopDevServer}
|
||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||
onViewDevServerLogs={handleViewDevServerLogs}
|
||||
@@ -1375,7 +1387,7 @@ export function WorktreePanel({
|
||||
onResolveConflicts={onResolveConflicts}
|
||||
onMerge={handleMerge}
|
||||
onDeleteWorktree={onDeleteWorktree}
|
||||
onStartDevServer={handleStartDevServer}
|
||||
onStartDevServer={handleStartDevServerAndShowLogs}
|
||||
onStopDevServer={handleStopDevServer}
|
||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||
onViewDevServerLogs={handleViewDevServerLogs}
|
||||
|
||||
Reference in New Issue
Block a user