mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
feat(ui): add terminal open mode setting (new tab vs split)
Add a setting to choose how "Open in Terminal" behaves:
- New Tab: Creates a new tab named after the branch (default)
- Split: Adds to current tab as a split view
Changes:
- Add openTerminalMode setting to terminal state ('newTab' | 'split')
- Update terminal-view to respect the setting
- Add UI in Terminal Settings to toggle the behavior
- Rename pendingTerminalCwd to pendingTerminal with branch name
The new tab mode names tabs after the branch for easy identification.
The split mode is useful for comparing terminals side by side.
This commit is contained in:
@@ -42,7 +42,7 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre
|
||||
const [isSwitching, setIsSwitching] = useState(false);
|
||||
const [isActivating, setIsActivating] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const setPendingTerminalCwd = useAppStore((state) => state.setPendingTerminalCwd);
|
||||
const setPendingTerminal = useAppStore((state) => state.setPendingTerminal);
|
||||
|
||||
const handleSwitchBranch = useCallback(
|
||||
async (worktree: WorktreeInfo, branchName: string) => {
|
||||
@@ -149,13 +149,13 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre
|
||||
|
||||
const handleOpenInTerminal = useCallback(
|
||||
(worktree: WorktreeInfo) => {
|
||||
// Set the pending terminal cwd to the worktree path
|
||||
setPendingTerminalCwd(worktree.path);
|
||||
// Set the pending terminal with cwd and branch name
|
||||
setPendingTerminal({ cwd: worktree.path, branchName: worktree.branch });
|
||||
// Navigate to the terminal page
|
||||
navigate({ to: '/terminal' });
|
||||
logger.info('Opening terminal for worktree:', worktree.path);
|
||||
logger.info('Opening terminal for worktree:', worktree.path, 'branch:', worktree.branch);
|
||||
},
|
||||
[navigate, setPendingTerminalCwd]
|
||||
[navigate, setPendingTerminal]
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -25,6 +25,7 @@ export function TerminalSection() {
|
||||
setTerminalScrollbackLines,
|
||||
setTerminalLineHeight,
|
||||
setTerminalDefaultFontSize,
|
||||
setOpenTerminalMode,
|
||||
} = useAppStore();
|
||||
|
||||
const {
|
||||
@@ -34,6 +35,7 @@ export function TerminalSection() {
|
||||
scrollbackLines,
|
||||
lineHeight,
|
||||
defaultFontSize,
|
||||
openTerminalMode,
|
||||
} = terminalState;
|
||||
|
||||
return (
|
||||
@@ -165,6 +167,26 @@ export function TerminalSection() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Open in Terminal Mode */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-foreground font-medium">Open in Terminal Mode</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
How to open terminals from the "Open in Terminal" action in the worktree menu
|
||||
</p>
|
||||
<Select
|
||||
value={openTerminalMode}
|
||||
onValueChange={(value: 'newTab' | 'split') => setOpenTerminalMode(value)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="newTab">New Tab (named after branch)</SelectItem>
|
||||
<SelectItem value="split">Split Current Tab</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Screen Reader Mode */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
|
||||
@@ -244,7 +244,7 @@ export function TerminalView() {
|
||||
setTerminalScrollbackLines,
|
||||
setTerminalScreenReaderMode,
|
||||
updateTerminalPanelSizes,
|
||||
setPendingTerminalCwd,
|
||||
setPendingTerminal,
|
||||
} = useAppStore();
|
||||
|
||||
const [status, setStatus] = useState<TerminalStatus | null>(null);
|
||||
@@ -538,23 +538,24 @@ 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);
|
||||
// Handle pending terminal (from "open in terminal" action on worktree menu)
|
||||
// When pendingTerminal is set and we're ready, create a terminal based on openTerminalMode setting
|
||||
const pendingTerminalRef = useRef<string | null>(null);
|
||||
const pendingTerminalCreatedRef = useRef<boolean>(false);
|
||||
useEffect(() => {
|
||||
const pendingCwd = terminalState.pendingTerminalCwd;
|
||||
const pending = terminalState.pendingTerminal;
|
||||
const openMode = terminalState.openTerminalMode;
|
||||
|
||||
// Skip if no pending cwd
|
||||
if (!pendingCwd) {
|
||||
// Reset the created ref when there's no pending cwd
|
||||
// Skip if no pending terminal
|
||||
if (!pending) {
|
||||
// Reset the created ref when there's no pending terminal
|
||||
pendingTerminalCreatedRef.current = false;
|
||||
pendingTerminalCwdRef.current = null;
|
||||
pendingTerminalRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if we already created a terminal for this exact cwd
|
||||
if (pendingCwd === pendingTerminalCwdRef.current && pendingTerminalCreatedRef.current) {
|
||||
if (pending.cwd === pendingTerminalRef.current && pendingTerminalCreatedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -571,13 +572,12 @@ export function TerminalView() {
|
||||
}
|
||||
|
||||
// Track that we're processing this cwd
|
||||
pendingTerminalCwdRef.current = pendingCwd;
|
||||
pendingTerminalRef.current = pending.cwd;
|
||||
|
||||
// Create a terminal with the pending cwd
|
||||
logger.info('Creating terminal from pending cwd:', pendingCwd);
|
||||
logger.info('Creating terminal from pending:', pending, 'mode:', openMode);
|
||||
|
||||
// Create terminal with the specified cwd
|
||||
const createTerminalWithCwd = async () => {
|
||||
const createTerminalFromPending = async () => {
|
||||
try {
|
||||
const headers: Record<string, string> = {};
|
||||
const authToken = useAppStore.getState().terminalState.authToken;
|
||||
@@ -587,46 +587,66 @@ export function TerminalView() {
|
||||
|
||||
const response = await apiFetch('/api/terminal/sessions', 'POST', {
|
||||
headers,
|
||||
body: { cwd: pendingCwd, cols: 80, rows: 24 },
|
||||
body: { cwd: pending.cwd, cols: 80, rows: 24 },
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Mark as successfully created
|
||||
pendingTerminalCreatedRef.current = true;
|
||||
addTerminalToLayout(data.data.id);
|
||||
|
||||
if (openMode === 'newTab') {
|
||||
// Create a new tab named after the branch
|
||||
const newTabId = addTerminalTab(pending.branchName);
|
||||
|
||||
// Set the tab's layout to the new terminal
|
||||
useAppStore
|
||||
.getState()
|
||||
.setTerminalTabLayout(
|
||||
newTabId,
|
||||
{ type: 'terminal', sessionId: data.data.id, size: 100 },
|
||||
data.data.id
|
||||
);
|
||||
toast.success(`Opened terminal for ${pending.branchName}`);
|
||||
} else {
|
||||
// Split mode: add to current tab layout
|
||||
addTerminalToLayout(data.data.id);
|
||||
toast.success(`Opened terminal in ${pending.cwd.split('/').pop() || pending.cwd}`);
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Clear the pending terminal after successful creation
|
||||
setPendingTerminal(null);
|
||||
} else {
|
||||
logger.error('Failed to create session from pending cwd:', data.error);
|
||||
logger.error('Failed to create session from pending terminal:', data.error);
|
||||
toast.error('Failed to open terminal', {
|
||||
description: data.error || 'Unknown error',
|
||||
});
|
||||
// Clear pending cwd on failure to prevent infinite retries
|
||||
setPendingTerminalCwd(null);
|
||||
// Clear pending terminal on failure to prevent infinite retries
|
||||
setPendingTerminal(null);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Create session error from pending cwd:', err);
|
||||
logger.error('Create session error from pending terminal:', err);
|
||||
toast.error('Failed to open terminal');
|
||||
// Clear pending cwd on error to prevent infinite retries
|
||||
setPendingTerminalCwd(null);
|
||||
// Clear pending terminal on error to prevent infinite retries
|
||||
setPendingTerminal(null);
|
||||
}
|
||||
};
|
||||
|
||||
createTerminalWithCwd();
|
||||
createTerminalFromPending();
|
||||
}, [
|
||||
terminalState.pendingTerminalCwd,
|
||||
terminalState.pendingTerminal,
|
||||
terminalState.openTerminalMode,
|
||||
terminalState.isUnlocked,
|
||||
loading,
|
||||
status?.enabled,
|
||||
status?.passwordRequired,
|
||||
setPendingTerminalCwd,
|
||||
setPendingTerminal,
|
||||
addTerminalTab,
|
||||
addTerminalToLayout,
|
||||
defaultRunScript,
|
||||
fetchServerSettings,
|
||||
|
||||
@@ -531,7 +531,8 @@ 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)
|
||||
pendingTerminal: { cwd: string; branchName: string } | null; // Pending terminal to create (from "open in terminal" action)
|
||||
openTerminalMode: 'newTab' | 'split'; // How to open terminals from "Open in Terminal" action
|
||||
}
|
||||
|
||||
// Persisted terminal layout - now includes sessionIds for reconnection
|
||||
@@ -1230,7 +1231,8 @@ export interface AppActions {
|
||||
setTerminalLineHeight: (lineHeight: number) => void;
|
||||
setTerminalMaxSessions: (maxSessions: number) => void;
|
||||
setTerminalLastActiveProjectPath: (projectPath: string | null) => void;
|
||||
setPendingTerminalCwd: (cwd: string | null) => void;
|
||||
setPendingTerminal: (pending: { cwd: string; branchName: string } | null) => void;
|
||||
setOpenTerminalMode: (mode: 'newTab' | 'split') => void;
|
||||
addTerminalTab: (name?: string) => string;
|
||||
removeTerminalTab: (tabId: string) => void;
|
||||
setActiveTerminalTab: (tabId: string) => void;
|
||||
@@ -1447,7 +1449,8 @@ const initialState: AppState = {
|
||||
lineHeight: 1.0,
|
||||
maxSessions: 100,
|
||||
lastActiveProjectPath: null,
|
||||
pendingTerminalCwd: null,
|
||||
pendingTerminal: null,
|
||||
openTerminalMode: 'newTab',
|
||||
},
|
||||
terminalLayoutByProject: {},
|
||||
specCreatingForProject: null,
|
||||
@@ -2896,9 +2899,11 @@ 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
|
||||
// Preserve pendingTerminal - this is set by "open in terminal" action and should
|
||||
// survive the clearTerminalState() call that happens during project switching
|
||||
pendingTerminalCwd: current.pendingTerminalCwd,
|
||||
pendingTerminal: current.pendingTerminal,
|
||||
// Preserve openTerminalMode - user preference
|
||||
openTerminalMode: current.openTerminalMode,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -2990,10 +2995,17 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
setPendingTerminalCwd: (cwd) => {
|
||||
setPendingTerminal: (pending) => {
|
||||
const current = get().terminalState;
|
||||
set({
|
||||
terminalState: { ...current, pendingTerminalCwd: cwd },
|
||||
terminalState: { ...current, pendingTerminal: pending },
|
||||
});
|
||||
},
|
||||
|
||||
setOpenTerminalMode: (mode) => {
|
||||
const current = get().terminalState;
|
||||
set({
|
||||
terminalState: { ...current, openTerminalMode: mode },
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user