Skip to content

Commit 1ab2a4e

Browse files
committed
Merge branch 'aa'
# Conflicts: # src/App.tsx
2 parents 0f69c49 + 2132eed commit 1ab2a4e

File tree

5 files changed

+109
-22
lines changed

5 files changed

+109
-22
lines changed

src/App.tsx

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ function App() {
5353

5454
// Worktree that should auto-enter edit mode for its name (used with focusNewBranchNames config)
5555
const [autoEditWorktreeId, setAutoEditWorktreeId] = useState<string | null>(null);
56+
// Element to restore focus to after editing worktree name
57+
const focusToRestoreRef = useRef<HTMLElement | null>(null);
5658

5759
// Active project (when viewing main repo terminal instead of a worktree)
5860
// If activeWorktreeId is set, activeProjectId indicates which project's worktree is active
@@ -119,6 +121,9 @@ function App() {
119121
// Per-worktree focus state (which pane has focus)
120122
const [focusStates, setFocusStates] = useState<Map<string, FocusedPane>>(new Map());
121123

124+
// Counter to trigger focus on main pane (incremented when focus is explicitly requested)
125+
const [mainFocusTrigger, setMainFocusTrigger] = useState(0);
126+
122127
// Per-project selected task (persisted to localStorage)
123128
const [selectedTasksByProject, setSelectedTasksByProject] = useState<Map<string, string>>(() => {
124129
try {
@@ -972,6 +977,21 @@ function App() {
972977
});
973978
}, []);
974979

980+
// Focus main pane of the currently active entity
981+
const handleFocusMain = useCallback(() => {
982+
const entityId = activeWorktreeId ?? activeScratchId ?? activeProjectId;
983+
if (entityId) {
984+
setFocusStates((prev) => {
985+
if (prev.get(entityId) === 'main') return prev;
986+
const next = new Map(prev);
987+
next.set(entityId, 'main');
988+
return next;
989+
});
990+
// Always increment trigger to ensure focus happens even if state was already 'main'
991+
setMainFocusTrigger((prev) => prev + 1);
992+
}
993+
}, [activeWorktreeId, activeScratchId, activeProjectId]);
994+
975995
// Switch focus between main and drawer panes
976996
const handleSwitchFocus = useCallback(() => {
977997
if (!activeEntityId) return;
@@ -1451,36 +1471,29 @@ function App() {
14511471

14521472
const handleAddWorktree = useCallback(
14531473
async (projectId: string) => {
1454-
console.log('[handleAddWorktree] Called with projectId:', projectId);
14551474
const project = projects.find((p) => p.id === projectId);
14561475
if (!project) {
1457-
console.log('[handleAddWorktree] Project not found');
14581476
return;
14591477
}
1460-
console.log('[handleAddWorktree] Found project:', project.name, 'path:', project.path);
1461-
14621478
setExpandedProjects((prev) => new Set([...prev, projectId]));
14631479

1464-
console.log('[handleAddWorktree] About to call createWorktree...');
14651480
try {
14661481
const worktree = await createWorktree(project.path);
1467-
console.log('[handleAddWorktree] createWorktree succeeded:', worktree.name);
14681482
setLoadingWorktrees((prev) => new Set([...prev, worktree.id]));
14691483
setOpenWorktreeIds((prev) => new Set([...prev, worktree.id]));
14701484
setActiveWorktreeId(worktree.id);
14711485
setActiveScratchId(null);
14721486
// Auto-focus branch name for editing if configured
14731487
if (config.worktree.focusNewBranchNames) {
1488+
// For new worktrees, don't set a focusToRestoreRef - the worktree didn't exist before,
1489+
// so there's no valid prior focus within it. We'll fall back to onFocusMain().
1490+
focusToRestoreRef.current = null;
14741491
setAutoEditWorktreeId(worktree.id);
14751492
}
14761493
} catch (err) {
14771494
const errorMessage = String(err);
1478-
console.log('[handleAddWorktree] Error caught:', errorMessage);
1479-
console.log('[handleAddWorktree] Error type:', typeof err);
1480-
console.log('[handleAddWorktree] Error object:', err);
14811495
// Check if this is an uncommitted changes error
14821496
if (errorMessage.includes('uncommitted changes')) {
1483-
console.log('[handleAddWorktree] Showing stash modal for project:', project.name);
14841497
setStashError(null); // Clear any previous error
14851498
setPendingStashProject(project);
14861499
} else {
@@ -1497,26 +1510,19 @@ function App() {
14971510

14981511
const project = pendingStashProject;
14991512
setIsStashing(true);
1500-
console.log('[handleStashAndCreate] Starting for project:', project.path);
15011513

15021514
let stashId: string | null = null;
15031515

15041516
try {
15051517
// Stash the changes and get the stash ID
1506-
console.log('[handleStashAndCreate] Stashing changes...');
15071518
stashId = await stashChanges(project.path);
1508-
console.log('[handleStashAndCreate] Stash successful with id:', stashId);
15091519

15101520
// Create the worktree
1511-
console.log('[handleStashAndCreate] Creating worktree...');
15121521
const worktree = await createWorktree(project.path);
1513-
console.log('[handleStashAndCreate] Worktree created:', worktree.name);
15141522
setActiveScratchId(null);
15151523

15161524
// Pop the stash to restore changes
1517-
console.log('[handleStashAndCreate] Popping stash with id:', stashId);
15181525
await stashPop(project.path, stashId);
1519-
console.log('[handleStashAndCreate] Stash popped');
15201526

15211527
// Update UI state
15221528
setLoadingWorktrees((prev) => new Set([...prev, worktree.id]));
@@ -1525,6 +1531,9 @@ function App() {
15251531
setPendingStashProject(null);
15261532
// Auto-focus branch name for editing if configured
15271533
if (config.worktree.focusNewBranchNames) {
1534+
// For new worktrees, don't set a focusToRestoreRef - the worktree didn't exist before,
1535+
// so there's no valid prior focus within it. We'll fall back to onFocusMain().
1536+
focusToRestoreRef.current = null;
15281537
setAutoEditWorktreeId(worktree.id);
15291538
}
15301539
} catch (err) {
@@ -1894,6 +1903,7 @@ function App() {
18941903
}, []);
18951904

18961905
const handleRenameBranch = useCallback((worktreeId: string) => {
1906+
focusToRestoreRef.current = document.activeElement as HTMLElement | null;
18971907
setAutoEditWorktreeId(worktreeId);
18981908
}, []);
18991909

@@ -2153,6 +2163,7 @@ function App() {
21532163
// Rename branch shortcut (F2 by default)
21542164
if (matchesShortcut(e, mappings.renameBranch) && activeWorktreeId) {
21552165
e.preventDefault();
2166+
focusToRestoreRef.current = document.activeElement as HTMLElement | null;
21562167
setAutoEditWorktreeId(activeWorktreeId);
21572168
return;
21582169
}
@@ -2643,6 +2654,8 @@ function App() {
26432654
activeScratchCwd={activeScratchId ? scratchCwds.get(activeScratchId) ?? null : null}
26442655
homeDir={homeDir}
26452656
autoEditWorktreeId={autoEditWorktreeId}
2657+
focusToRestoreRef={focusToRestoreRef}
2658+
onFocusMain={handleFocusMain}
26462659
onToggleProject={toggleProject}
26472660
onSelectProject={handleSelectProject}
26482661
onSelectWorktree={handleSelectWorktree}
@@ -2695,6 +2708,7 @@ function App() {
26952708
mappings={config.mappings}
26962709
activityTimeout={config.indicators.activityTimeout}
26972710
shouldAutoFocus={activeFocusState === 'main'}
2711+
focusTrigger={mainFocusTrigger}
26982712
configErrors={configErrors}
26992713
onFocus={handleMainPaneFocused}
27002714
onWorktreeNotification={handleWorktreeNotification}

src/components/MainPane/MainPane.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ interface MainPaneProps {
1919
mappings: MappingsConfig;
2020
activityTimeout: number;
2121
shouldAutoFocus: boolean;
22+
/** Counter that triggers focus when incremented */
23+
focusTrigger?: number;
2224
configErrors: ConfigError[];
2325
onFocus: (entityId: string) => void;
2426
onWorktreeNotification?: (worktreeId: string, title: string, body: string) => void;
@@ -41,6 +43,7 @@ export function MainPane({
4143
mappings,
4244
activityTimeout,
4345
shouldAutoFocus,
46+
focusTrigger,
4447
configErrors,
4548
onFocus,
4649
onWorktreeNotification,
@@ -121,6 +124,7 @@ export function MainPane({
121124
type="main"
122125
isActive={worktreeId === activeEntityId}
123126
shouldAutoFocus={worktreeId === activeEntityId && shouldAutoFocus}
127+
focusTrigger={worktreeId === activeEntityId ? focusTrigger : undefined}
124128
terminalConfig={terminalConfig}
125129
mappings={mappings}
126130
activityTimeout={activityTimeout}
@@ -145,6 +149,7 @@ export function MainPane({
145149
type="project"
146150
isActive={!activeWorktreeId && !activeScratchId && projectId === activeEntityId}
147151
shouldAutoFocus={!activeWorktreeId && !activeScratchId && projectId === activeEntityId && shouldAutoFocus}
152+
focusTrigger={!activeWorktreeId && !activeScratchId && projectId === activeEntityId ? focusTrigger : undefined}
148153
terminalConfig={terminalConfig}
149154
mappings={mappings}
150155
activityTimeout={activityTimeout}
@@ -169,6 +174,7 @@ export function MainPane({
169174
type="scratch"
170175
isActive={scratch.id === activeScratchId}
171176
shouldAutoFocus={scratch.id === activeScratchId && shouldAutoFocus}
177+
focusTrigger={scratch.id === activeScratchId ? focusTrigger : undefined}
172178
terminalConfig={terminalConfig}
173179
mappings={mappings}
174180
activityTimeout={activityTimeout}

src/components/MainPane/MainTerminal.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ interface MainTerminalProps {
3434
type?: 'main' | 'project' | 'scratch';
3535
isActive: boolean;
3636
shouldAutoFocus: boolean;
37+
/** Counter that triggers focus when incremented */
38+
focusTrigger?: number;
3739
terminalConfig: TerminalConfig;
3840
mappings: MappingsConfig;
3941
activityTimeout?: number;
@@ -43,7 +45,7 @@ interface MainTerminalProps {
4345
onCwdChange?: (cwd: string) => void;
4446
}
4547

46-
export function MainTerminal({ entityId, type = 'main', isActive, shouldAutoFocus, terminalConfig, mappings, activityTimeout = 250, onFocus, onNotification, onThinkingChange, onCwdChange }: MainTerminalProps) {
48+
export function MainTerminal({ entityId, type = 'main', isActive, shouldAutoFocus, focusTrigger, terminalConfig, mappings, activityTimeout = 250, onFocus, onNotification, onThinkingChange, onCwdChange }: MainTerminalProps) {
4749
const containerRef = useRef<HTMLDivElement>(null);
4850
const terminalRef = useRef<Terminal | null>(null);
4951
const fitAddonRef = useRef<FitAddon | null>(null);
@@ -649,12 +651,12 @@ export function MainTerminal({ entityId, type = 'main', isActive, shouldAutoFocu
649651
return () => window.removeEventListener('panel-resize-complete', handlePanelResizeComplete);
650652
}, [ptyId, isActive, immediateResize]);
651653

652-
// Focus terminal when shouldAutoFocus is true
654+
// Focus terminal when shouldAutoFocus is true or when focusTrigger changes
653655
useEffect(() => {
654656
if (shouldAutoFocus && terminalRef.current) {
655657
terminalRef.current.focus();
656658
}
657-
}, [shouldAutoFocus]);
659+
}, [shouldAutoFocus, focusTrigger]);
658660

659661
return (
660662
<div className="relative w-full h-full" style={{ backgroundColor: '#09090b', padding: terminalConfig.padding }}>

src/components/Sidebar/EditableWorktreeName.tsx

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ interface EditableWorktreeNameProps {
88
autoEdit?: boolean;
99
/** Called when auto-edit mode is consumed (user starts editing or cancels) */
1010
onAutoEditConsumed?: () => void;
11+
/** Ref to element that should receive focus when editing ends (takes precedence over previousFocus) */
12+
focusToRestoreRef?: React.RefObject<HTMLElement | null>;
13+
/** Called to focus the main terminal area when no other focus target is available */
14+
onFocusMain?: () => void;
1115
}
1216

1317
export function EditableWorktreeName({
@@ -16,12 +20,15 @@ export function EditableWorktreeName({
1620
className = '',
1721
autoEdit = false,
1822
onAutoEditConsumed,
23+
focusToRestoreRef,
24+
onFocusMain,
1925
}: EditableWorktreeNameProps) {
2026
const [isEditing, setIsEditing] = useState(false);
2127
const [editValue, setEditValue] = useState(name);
2228
const [error, setError] = useState<string | null>(null);
2329
const [isSubmitting, setIsSubmitting] = useState(false);
2430
const inputRef = useRef<HTMLInputElement>(null);
31+
const previousFocusRef = useRef<HTMLElement | null>(null);
2532

2633
// Reset edit value when name changes externally
2734
useEffect(() => {
@@ -33,6 +40,7 @@ export function EditableWorktreeName({
3340
// Auto-enter edit mode when autoEdit is true
3441
useEffect(() => {
3542
if (autoEdit && !isEditing) {
43+
previousFocusRef.current = document.activeElement as HTMLElement | null;
3644
setIsEditing(true);
3745
setError(null);
3846
onAutoEditConsumed?.();
@@ -49,6 +57,7 @@ export function EditableWorktreeName({
4957

5058
const handleDoubleClick = useCallback((e: React.MouseEvent) => {
5159
e.stopPropagation();
60+
previousFocusRef.current = document.activeElement as HTMLElement | null;
5261
setIsEditing(true);
5362
setError(null);
5463
}, []);
@@ -57,7 +66,31 @@ export function EditableWorktreeName({
5766
setIsEditing(false);
5867
setEditValue(name);
5968
setError(null);
60-
}, [name]);
69+
70+
// Helper to check if element is visible and focusable
71+
const isElementVisible = (el: HTMLElement | null): boolean => {
72+
if (!el || !el.isConnected) return false;
73+
const rect = el.getBoundingClientRect();
74+
return rect.width > 0 && rect.height > 0;
75+
};
76+
77+
// Focus restoration logic:
78+
// - If focusToRestoreRef is provided (autoEdit from App.tsx): use it or fall back to onFocusMain
79+
// (skip previousFocusRef since App.tsx has control over the focus target)
80+
// - If focusToRestoreRef is not provided (manual double-click rename): try previousFocusRef, then onFocusMain
81+
if (focusToRestoreRef) {
82+
if (focusToRestoreRef.current && isElementVisible(focusToRestoreRef.current)) {
83+
focusToRestoreRef.current.focus();
84+
} else {
85+
onFocusMain?.();
86+
}
87+
} else if (previousFocusRef.current && isElementVisible(previousFocusRef.current)) {
88+
previousFocusRef.current.focus();
89+
} else {
90+
onFocusMain?.();
91+
}
92+
previousFocusRef.current = null;
93+
}, [name, focusToRestoreRef, onFocusMain]);
6194

6295
const handleSubmit = useCallback(async () => {
6396
const trimmedValue = editValue.trim();
@@ -80,12 +113,36 @@ export function EditableWorktreeName({
80113
try {
81114
await onRename(trimmedValue);
82115
setIsEditing(false);
116+
117+
// Helper to check if element is visible and focusable
118+
const isElementVisible = (el: HTMLElement | null): boolean => {
119+
if (!el || !el.isConnected) return false;
120+
const rect = el.getBoundingClientRect();
121+
return rect.width > 0 && rect.height > 0;
122+
};
123+
124+
// Focus restoration logic:
125+
// - If focusToRestoreRef is provided (autoEdit from App.tsx): use it or fall back to onFocusMain
126+
// (skip previousFocusRef since App.tsx has control over the focus target)
127+
// - If focusToRestoreRef is not provided (manual double-click rename): try previousFocusRef, then onFocusMain
128+
if (focusToRestoreRef) {
129+
if (focusToRestoreRef.current && isElementVisible(focusToRestoreRef.current)) {
130+
focusToRestoreRef.current.focus();
131+
} else {
132+
onFocusMain?.();
133+
}
134+
} else if (previousFocusRef.current && isElementVisible(previousFocusRef.current)) {
135+
previousFocusRef.current.focus();
136+
} else {
137+
onFocusMain?.();
138+
}
139+
previousFocusRef.current = null;
83140
} catch (err) {
84141
setError(String(err));
85142
} finally {
86143
setIsSubmitting(false);
87144
}
88-
}, [editValue, name, onRename, handleCancel]);
145+
}, [editValue, name, onRename, handleCancel, focusToRestoreRef, onFocusMain]);
89146

90147
const handleKeyDown = useCallback(
91148
(e: React.KeyboardEvent) => {

src/components/Sidebar/Sidebar.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ interface SidebarProps {
6363
homeDir: string | null;
6464
/** Worktree ID that should auto-enter edit mode for its name */
6565
autoEditWorktreeId: string | null;
66+
/** Ref to element that should receive focus when editing ends */
67+
focusToRestoreRef: React.RefObject<HTMLElement | null>;
68+
/** Called to focus the main terminal area */
69+
onFocusMain: () => void;
6670
onToggleProject: (projectId: string) => void;
6771
onSelectProject: (project: Project) => void;
6872
onSelectWorktree: (worktree: Worktree) => void;
@@ -122,6 +126,8 @@ export function Sidebar({
122126
activeScratchCwd,
123127
homeDir,
124128
autoEditWorktreeId,
129+
focusToRestoreRef,
130+
onFocusMain,
125131
onToggleProject,
126132
onSelectProject,
127133
onSelectWorktree,
@@ -608,6 +614,8 @@ export function Sidebar({
608614
onRename={(newName) => onRenameWorktree(worktree.id, newName)}
609615
autoEdit={autoEditWorktreeId === worktree.id}
610616
onAutoEditConsumed={onAutoEditConsumed}
617+
focusToRestoreRef={autoEditWorktreeId === worktree.id ? focusToRestoreRef : undefined}
618+
onFocusMain={onFocusMain}
611619
/>
612620
{isLoading ? (
613621
<span className="absolute right-1" title="Starting...">

0 commit comments

Comments
 (0)