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 { 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,

View File

@@ -244,6 +244,7 @@ export function TerminalView() {
setTerminalScrollbackLines,
setTerminalScreenReaderMode,
updateTerminalPanelSizes,
setPendingTerminalCwd,
} = useAppStore();
const [status, setStatus] = useState<TerminalStatus | null>(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<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
// Uses terminalState.lastActiveProjectPath (persisted in store) instead of a local ref
// 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
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<AppState & AppActions>()((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<AppState & AppActions>()((set, get) => ({
});
},
setPendingTerminalCwd: (cwd) => {
const current = get().terminalState;
set({
terminalState: { ...current, pendingTerminalCwd: cwd },
});
},
addTerminalTab: (name) => {
const current = get().terminalState;
const newTabId = `tab-${Date.now()}`;