From 03103fd1bb9cda81ac72e2840b7e25cdb6b96b0c Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Sat, 17 Jan 2026 20:14:21 +0100 Subject: [PATCH] fix(ui): open in terminal navigates to Automaker terminal view Instead of opening the system terminal, the "Open in Terminal" action now opens Automaker's built-in terminal with the worktree directory: - Add pendingTerminalCwd state to app store - Update use-worktree-actions to set pending cwd and navigate to /terminal - Add effect in terminal-view to create session with pending cwd This matches the original PR #558 behavior. --- .../hooks/use-worktree-actions.ts | 31 +++--- .../ui/src/components/views/terminal-view.tsx | 95 +++++++++++++++++++ apps/ui/src/store/app-store.ts | 13 +++ 3 files changed, 122 insertions(+), 17 deletions(-) diff --git a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts index d3b1db85..b666dea9 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts +++ b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts @@ -1,7 +1,9 @@ import { useState, useCallback } from 'react'; +import { useNavigate } from '@tanstack/react-router'; import { createLogger } from '@automaker/utils/logger'; import { getElectronAPI } from '@/lib/electron'; import { toast } from 'sonner'; +import { useAppStore } from '@/store/app-store'; import type { WorktreeInfo } from '../types'; const logger = createLogger('WorktreeActions'); @@ -39,6 +41,8 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre const [isPushing, setIsPushing] = useState(false); const [isSwitching, setIsSwitching] = useState(false); const [isActivating, setIsActivating] = useState(false); + const navigate = useNavigate(); + const setPendingTerminalCwd = useAppStore((state) => state.setPendingTerminalCwd); const handleSwitchBranch = useCallback( async (worktree: WorktreeInfo, branchName: string) => { @@ -143,23 +147,16 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre } }, []); - const handleOpenInTerminal = useCallback(async (worktree: WorktreeInfo) => { - try { - const api = getElectronAPI(); - if (!api?.worktree?.openInTerminal) { - logger.warn('Open in terminal API not available'); - return; - } - const result = await api.worktree.openInTerminal(worktree.path); - if (result.success && result.result) { - toast.success(result.result.message); - } else if (result.error) { - toast.error(result.error); - } - } catch (error) { - logger.error('Open in terminal failed:', error); - } - }, []); + const handleOpenInTerminal = useCallback( + (worktree: WorktreeInfo) => { + // Set the pending terminal cwd to the worktree path + setPendingTerminalCwd(worktree.path); + // Navigate to the terminal page + navigate({ to: '/terminal' }); + logger.info('Opening terminal for worktree:', worktree.path); + }, + [navigate, setPendingTerminalCwd] + ); return { isPulling, diff --git a/apps/ui/src/components/views/terminal-view.tsx b/apps/ui/src/components/views/terminal-view.tsx index 0287ca68..26db2a62 100644 --- a/apps/ui/src/components/views/terminal-view.tsx +++ b/apps/ui/src/components/views/terminal-view.tsx @@ -244,6 +244,7 @@ export function TerminalView() { setTerminalScrollbackLines, setTerminalScreenReaderMode, updateTerminalPanelSizes, + setPendingTerminalCwd, } = useAppStore(); const [status, setStatus] = useState(null); @@ -537,6 +538,100 @@ export function TerminalView() { } }, [terminalState.isUnlocked, fetchServerSettings]); + // Handle pending terminal cwd (from "open in terminal" action on worktree menu) + // When pendingTerminalCwd is set and we're ready, create a terminal with that cwd + const pendingTerminalCwdRef = useRef(null); + const pendingTerminalCreatedRef = useRef(false); + useEffect(() => { + const pendingCwd = terminalState.pendingTerminalCwd; + + // Skip if no pending cwd + if (!pendingCwd) { + // Reset the created ref when there's no pending cwd + pendingTerminalCreatedRef.current = false; + pendingTerminalCwdRef.current = null; + return; + } + + // Skip if we already created a terminal for this exact cwd + if (pendingCwd === pendingTerminalCwdRef.current && pendingTerminalCreatedRef.current) { + return; + } + + // Skip if still loading or terminal not enabled + if (loading || !status?.enabled) { + logger.debug('Waiting for terminal to be ready before creating terminal for pending cwd'); + return; + } + + // Skip if password is required but not unlocked yet + if (status.passwordRequired && !terminalState.isUnlocked) { + logger.debug('Waiting for terminal unlock before creating terminal for pending cwd'); + return; + } + + // Track that we're processing this cwd + pendingTerminalCwdRef.current = pendingCwd; + + // Create a terminal with the pending cwd + logger.info('Creating terminal from pending cwd:', pendingCwd); + + // Create terminal with the specified cwd + const createTerminalWithCwd = async () => { + try { + const headers: Record = {}; + const authToken = useAppStore.getState().terminalState.authToken; + if (authToken) { + headers['X-Terminal-Token'] = authToken; + } + + const response = await apiFetch('/api/terminal/sessions', 'POST', { + headers, + body: { cwd: pendingCwd, cols: 80, rows: 24 }, + }); + const data = await response.json(); + + if (data.success) { + // Mark as successfully created + pendingTerminalCreatedRef.current = true; + addTerminalToLayout(data.data.id); + // Mark this session as new for running initial command + if (defaultRunScript) { + setNewSessionIds((prev) => new Set(prev).add(data.data.id)); + } + fetchServerSettings(); + toast.success(`Opened terminal in ${pendingCwd.split('/').pop() || pendingCwd}`); + // Clear the pending cwd after successful creation + setPendingTerminalCwd(null); + } else { + logger.error('Failed to create session from pending cwd:', data.error); + toast.error('Failed to open terminal', { + description: data.error || 'Unknown error', + }); + // Clear pending cwd on failure to prevent infinite retries + setPendingTerminalCwd(null); + } + } catch (err) { + logger.error('Create session error from pending cwd:', err); + toast.error('Failed to open terminal'); + // Clear pending cwd on error to prevent infinite retries + setPendingTerminalCwd(null); + } + }; + + createTerminalWithCwd(); + }, [ + terminalState.pendingTerminalCwd, + terminalState.isUnlocked, + loading, + status?.enabled, + status?.passwordRequired, + setPendingTerminalCwd, + addTerminalToLayout, + defaultRunScript, + fetchServerSettings, + ]); + // Handle project switching - save and restore terminal layouts // Uses terminalState.lastActiveProjectPath (persisted in store) instead of a local ref // This ensures terminals persist when navigating away from terminal route and back diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index ee8ca98a..bf1b2295 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -531,6 +531,7 @@ export interface TerminalState { lineHeight: number; // Line height multiplier for terminal text maxSessions: number; // Maximum concurrent terminal sessions (server setting) lastActiveProjectPath: string | null; // Last project path to detect route changes vs project switches + pendingTerminalCwd: string | null; // Pending cwd to use when creating next terminal (from "open in terminal" action) } // Persisted terminal layout - now includes sessionIds for reconnection @@ -1229,6 +1230,7 @@ export interface AppActions { setTerminalLineHeight: (lineHeight: number) => void; setTerminalMaxSessions: (maxSessions: number) => void; setTerminalLastActiveProjectPath: (projectPath: string | null) => void; + setPendingTerminalCwd: (cwd: string | null) => void; addTerminalTab: (name?: string) => string; removeTerminalTab: (tabId: string) => void; setActiveTerminalTab: (tabId: string) => void; @@ -1445,6 +1447,7 @@ const initialState: AppState = { lineHeight: 1.0, maxSessions: 100, lastActiveProjectPath: null, + pendingTerminalCwd: null, }, terminalLayoutByProject: {}, specCreatingForProject: null, @@ -2893,6 +2896,9 @@ export const useAppStore = create()((set, get) => ({ maxSessions: current.maxSessions, // Preserve lastActiveProjectPath - it will be updated separately when needed lastActiveProjectPath: current.lastActiveProjectPath, + // Preserve pendingTerminalCwd - this is set by "open in terminal" action and should + // survive the clearTerminalState() call that happens during project switching + pendingTerminalCwd: current.pendingTerminalCwd, }, }); }, @@ -2984,6 +2990,13 @@ export const useAppStore = create()((set, get) => ({ }); }, + setPendingTerminalCwd: (cwd) => { + const current = get().terminalState; + set({ + terminalState: { ...current, pendingTerminalCwd: cwd }, + }); + }, + addTerminalTab: (name) => { const current = get().terminalState; const newTabId = `tab-${Date.now()}`;