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.
This commit is contained in:
Stefan de Vogelaere
2026-01-17 20:14:21 +01:00
parent 099ebaf800
commit 03103fd1bb
3 changed files with 122 additions and 17 deletions

View File

@@ -1,7 +1,9 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { useNavigate } from '@tanstack/react-router';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useAppStore } from '@/store/app-store';
import type { WorktreeInfo } from '../types'; import type { WorktreeInfo } from '../types';
const logger = createLogger('WorktreeActions'); const logger = createLogger('WorktreeActions');
@@ -39,6 +41,8 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre
const [isPushing, setIsPushing] = useState(false); const [isPushing, setIsPushing] = useState(false);
const [isSwitching, setIsSwitching] = useState(false); const [isSwitching, setIsSwitching] = useState(false);
const [isActivating, setIsActivating] = useState(false); const [isActivating, setIsActivating] = useState(false);
const navigate = useNavigate();
const setPendingTerminalCwd = useAppStore((state) => state.setPendingTerminalCwd);
const handleSwitchBranch = useCallback( const handleSwitchBranch = useCallback(
async (worktree: WorktreeInfo, branchName: string) => { async (worktree: WorktreeInfo, branchName: string) => {
@@ -143,23 +147,16 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre
} }
}, []); }, []);
const handleOpenInTerminal = useCallback(async (worktree: WorktreeInfo) => { const handleOpenInTerminal = useCallback(
try { (worktree: WorktreeInfo) => {
const api = getElectronAPI(); // Set the pending terminal cwd to the worktree path
if (!api?.worktree?.openInTerminal) { setPendingTerminalCwd(worktree.path);
logger.warn('Open in terminal API not available'); // Navigate to the terminal page
return; navigate({ to: '/terminal' });
} logger.info('Opening terminal for worktree:', worktree.path);
const result = await api.worktree.openInTerminal(worktree.path); },
if (result.success && result.result) { [navigate, setPendingTerminalCwd]
toast.success(result.result.message); );
} else if (result.error) {
toast.error(result.error);
}
} catch (error) {
logger.error('Open in terminal failed:', error);
}
}, []);
return { return {
isPulling, isPulling,

View File

@@ -244,6 +244,7 @@ export function TerminalView() {
setTerminalScrollbackLines, setTerminalScrollbackLines,
setTerminalScreenReaderMode, setTerminalScreenReaderMode,
updateTerminalPanelSizes, updateTerminalPanelSizes,
setPendingTerminalCwd,
} = useAppStore(); } = useAppStore();
const [status, setStatus] = useState<TerminalStatus | null>(null); const [status, setStatus] = useState<TerminalStatus | null>(null);
@@ -537,6 +538,100 @@ export function TerminalView() {
} }
}, [terminalState.isUnlocked, fetchServerSettings]); }, [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<string | null>(null);
const pendingTerminalCreatedRef = useRef<boolean>(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<string, string> = {};
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 // Handle project switching - save and restore terminal layouts
// Uses terminalState.lastActiveProjectPath (persisted in store) instead of a local ref // Uses terminalState.lastActiveProjectPath (persisted in store) instead of a local ref
// This ensures terminals persist when navigating away from terminal route and back // This ensures terminals persist when navigating away from terminal route and back

View File

@@ -531,6 +531,7 @@ export interface TerminalState {
lineHeight: number; // Line height multiplier for terminal text lineHeight: number; // Line height multiplier for terminal text
maxSessions: number; // Maximum concurrent terminal sessions (server setting) maxSessions: number; // Maximum concurrent terminal sessions (server setting)
lastActiveProjectPath: string | null; // Last project path to detect route changes vs project switches 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 // Persisted terminal layout - now includes sessionIds for reconnection
@@ -1229,6 +1230,7 @@ export interface AppActions {
setTerminalLineHeight: (lineHeight: number) => void; setTerminalLineHeight: (lineHeight: number) => void;
setTerminalMaxSessions: (maxSessions: number) => void; setTerminalMaxSessions: (maxSessions: number) => void;
setTerminalLastActiveProjectPath: (projectPath: string | null) => void; setTerminalLastActiveProjectPath: (projectPath: string | null) => void;
setPendingTerminalCwd: (cwd: string | null) => void;
addTerminalTab: (name?: string) => string; addTerminalTab: (name?: string) => string;
removeTerminalTab: (tabId: string) => void; removeTerminalTab: (tabId: string) => void;
setActiveTerminalTab: (tabId: string) => void; setActiveTerminalTab: (tabId: string) => void;
@@ -1445,6 +1447,7 @@ const initialState: AppState = {
lineHeight: 1.0, lineHeight: 1.0,
maxSessions: 100, maxSessions: 100,
lastActiveProjectPath: null, lastActiveProjectPath: null,
pendingTerminalCwd: null,
}, },
terminalLayoutByProject: {}, terminalLayoutByProject: {},
specCreatingForProject: null, specCreatingForProject: null,
@@ -2893,6 +2896,9 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
maxSessions: current.maxSessions, maxSessions: current.maxSessions,
// Preserve lastActiveProjectPath - it will be updated separately when needed // Preserve lastActiveProjectPath - it will be updated separately when needed
lastActiveProjectPath: current.lastActiveProjectPath, 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<AppState & AppActions>()((set, get) => ({
}); });
}, },
setPendingTerminalCwd: (cwd) => {
const current = get().terminalState;
set({
terminalState: { ...current, pendingTerminalCwd: cwd },
});
},
addTerminalTab: (name) => { addTerminalTab: (name) => {
const current = get().terminalState; const current = get().terminalState;
const newTabId = `tab-${Date.now()}`; const newTabId = `tab-${Date.now()}`;