Skip to content

Commit b9b698e

Browse files
committed
Refactor layout focus and storage
1 parent 66c70d9 commit b9b698e

4 files changed

Lines changed: 489 additions & 166 deletions

File tree

anycode/App.tsx

Lines changed: 56 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { useFileTree } from './hooks/useFileTree';
2626
import { useTerminals } from './hooks/useTerminals';
2727
import { useEditors } from './hooks/useEditors';
2828
import { useAgents } from './hooks/useAgents';
29+
import { useLayout } from './hooks/useLayout';
2930
import { type AcpPermissionMode } from './types';
3031
import { useTerminalPanes } from './features/terminal/useTerminalPanes';
3132
import { useAgentPanes } from './features/agents/useAgentPanes';
@@ -37,11 +38,6 @@ import { AgentPanel } from './features/agents/AgentPanel';
3738
const App: React.FC = () => {
3839
const [diffEnabled, setDiffEnabled] = useState<boolean>(loadDiffEnabled());
3940
const [editorDiffEnabledByPane, setEditorDiffEnabledByPane] = useState<Record<string, boolean>>({});
40-
const [focusRequest, setFocusRequest] = useState<{
41-
target: PanelId;
42-
panelKey?: string;
43-
nonce: number;
44-
} | null>(null);
4541
// const [followEnabled, setFollowEnabled] = useState<boolean>(loadFollowEnabled());
4642
const [permissionMode, setPermissionMode] = useState<AcpPermissionMode>(loadAcpPermissionMode());
4743

@@ -67,13 +63,6 @@ const App: React.FC = () => {
6763
const git = useGit({ wsRef, isConnected });
6864
const search = useSearch({ wsRef, isConnected });
6965
const wasConnectedRef = useRef<boolean>(false);
70-
const activePanelRef = useRef<{ panelId: PanelId; panelKey: string } | null>(null);
71-
const activeAgentPaneIdRef = useRef<string>('agent');
72-
const panelKeysByIdRef = useRef<Record<PanelId, string[]>>({
73-
toolbar: ['toolbar'], files: ['files'],
74-
search: ['search'], changes: ['changes'],
75-
editor: [], terminal: [], agent: [],
76-
});
7766
const agents = useAgents({
7867
wsRef,
7968
isConnected,
@@ -187,97 +176,6 @@ const App: React.FC = () => {
187176
saveItem('terminals', terminals.terminals);
188177
}, [terminals.terminals]);
189178

190-
const handleCtrlFocusShortcut = useCallback((e: KeyboardEvent): boolean => {
191-
if (!e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
192-
return false;
193-
}
194-
195-
const focusTarget: PanelId | null =
196-
e.code === 'Digit1' ? 'files'
197-
: e.code === 'Digit2' ? 'editor'
198-
: e.code === 'Digit3' ? 'terminal'
199-
: e.code === 'Digit4' ? 'agent'
200-
: null;
201-
202-
if (!focusTarget) {
203-
return false;
204-
}
205-
206-
let targetPanelKey: string | undefined;
207-
208-
if (focusTarget === 'editor' || focusTarget === 'terminal' || focusTarget === 'agent') {
209-
const panelKeys = panelKeysByIdRef.current[focusTarget] ?? [];
210-
if (panelKeys.length > 0) {
211-
const activePanel = activePanelRef.current;
212-
const isTargetFocused = activePanel?.panelId === focusTarget
213-
&& panelKeys.includes(activePanel.panelKey);
214-
215-
if (isTargetFocused && activePanel) {
216-
const currentIndex = panelKeys.indexOf(activePanel.panelKey);
217-
targetPanelKey = panelKeys[(currentIndex + 1) % panelKeys.length];
218-
} else {
219-
const preferredPanelKey = focusTarget === 'editor'
220-
? editors.activeEditorPaneId
221-
: focusTarget === 'terminal'
222-
? terminalPanes.activePaneId
223-
: activeAgentPaneIdRef.current;
224-
targetPanelKey = preferredPanelKey && panelKeys.includes(preferredPanelKey)
225-
? preferredPanelKey
226-
: panelKeys[0];
227-
}
228-
}
229-
}
230-
231-
e.preventDefault();
232-
setFocusRequest({ target: focusTarget, panelKey: targetPanelKey, nonce: Date.now() });
233-
return true;
234-
}, [editors.activeEditorPaneId, terminalPanes.activePaneId]);
235-
236-
useEffect(() => {
237-
const handleKeyDown = (e: KeyboardEvent) => {
238-
const activePaneId = editors.activeEditorPaneId;
239-
if (activePaneId && editors.handleReferencesPeekKeyDown(activePaneId, e)) {
240-
return;
241-
}
242-
243-
if (e.metaKey && e.key === 'f') {
244-
e.preventDefault();
245-
}
246-
247-
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
248-
e.preventDefault();
249-
if (editors.activeFileId) {
250-
editors.saveFile(editors.activeFileId);
251-
}
252-
}
253-
254-
if (handleCtrlFocusShortcut(e)) {
255-
return;
256-
}
257-
258-
if (e.ctrlKey && e.key === '-') {
259-
e.preventDefault();
260-
editors.undoCursor();
261-
} else if (e.ctrlKey && e.key === '_') {
262-
e.preventDefault();
263-
editors.redoCursor();
264-
}
265-
};
266-
267-
document.addEventListener('keydown', handleKeyDown, true);
268-
return () => {
269-
document.removeEventListener('keydown', handleKeyDown, true);
270-
};
271-
}, [
272-
editors.activeEditorPaneId,
273-
editors.activeFileId,
274-
editors.handleReferencesPeekKeyDown,
275-
handleCtrlFocusShortcut,
276-
editors.redoCursor,
277-
editors.saveFile,
278-
editors.undoCursor,
279-
]);
280-
281179
const handleSearch = ({ pattern }: { id: string; pattern: string }) => {
282180
search.startSearch(pattern);
283181
};
@@ -330,42 +228,58 @@ const App: React.FC = () => {
330228
setSelectedAgentId: agents.setSelectedAgentId,
331229
});
332230

231+
const layout = useLayout({
232+
activeEditorPaneId: editors.activeEditorPaneId,
233+
activeTerminalPaneId: terminalPanes.activePaneId,
234+
activeAgentPaneId: agentPanes.activePaneId,
235+
onFocusEditorPane: editors.focusEditorInPane,
236+
onActivateTerminalPane: terminalPanes.setActivePaneId,
237+
onActivateAgentPane: agentPanes.setActivePaneId,
238+
});
239+
333240
useEffect(() => {
334-
if (!focusRequest) {
335-
return;
336-
}
241+
const handleKeyDown = (e: KeyboardEvent) => {
242+
const activePaneId = editors.activeEditorPaneId;
243+
if (activePaneId && editors.handleReferencesPeekKeyDown(activePaneId, e)) {
244+
return;
245+
}
337246

338-
if (focusRequest.target === 'editor' && editors.activeEditorPaneId) {
339-
editors.focusEditorInPane(focusRequest.panelKey ?? editors.activeEditorPaneId);
340-
return;
341-
}
247+
if (e.metaKey && e.key === 'f') {
248+
e.preventDefault();
249+
}
342250

343-
if (focusRequest.target === 'terminal') {
344-
const knownTerminalPanels = panelKeysByIdRef.current.terminal;
345-
const requestedKey = focusRequest.panelKey;
346-
const resolvedKey = requestedKey && knownTerminalPanels.includes(requestedKey)
347-
? requestedKey
348-
: terminalPanes.activePaneId;
349-
terminalPanes.setActivePaneId(resolvedKey || 'terminal');
350-
return;
351-
}
251+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
252+
e.preventDefault();
253+
if (editors.activeFileId) {
254+
editors.saveFile(editors.activeFileId);
255+
}
256+
}
352257

353-
if (focusRequest.target === 'agent') {
354-
const knownAgentPanels = panelKeysByIdRef.current.agent;
355-
const requestedKey = focusRequest.panelKey;
356-
const resolvedKey = requestedKey && knownAgentPanels.includes(requestedKey)
357-
? requestedKey
358-
: agentPanes.activePaneId;
359-
agentPanes.setActivePaneId(resolvedKey || 'agent');
360-
}
258+
if (layout.handleCtrlFocusShortcut(e)) {
259+
return;
260+
}
261+
262+
if (e.ctrlKey && e.key === '-') {
263+
e.preventDefault();
264+
editors.undoCursor();
265+
} else if (e.ctrlKey && e.key === '_') {
266+
e.preventDefault();
267+
editors.redoCursor();
268+
}
269+
};
270+
271+
document.addEventListener('keydown', handleKeyDown, true);
272+
return () => {
273+
document.removeEventListener('keydown', handleKeyDown, true);
274+
};
361275
}, [
362-
agentPanes.activePaneId,
363-
agentPanes.setActivePaneId,
364276
editors.activeEditorPaneId,
365-
editors.focusEditorInPane,
366-
focusRequest,
367-
terminalPanes.activePaneId,
368-
terminalPanes.setActivePaneId,
277+
editors.activeFileId,
278+
editors.handleReferencesPeekKeyDown,
279+
editors.redoCursor,
280+
editors.saveFile,
281+
editors.undoCursor,
282+
layout,
369283
]);
370284

371285
const handleStartSpecificAgent = useCallback((agent: AcpAgent) => {
@@ -400,7 +314,7 @@ const App: React.FC = () => {
400314
<FilesPanel
401315
fileTree={fileTree.fileTree}
402316
activeNodeId={fileTree.activeNodeId}
403-
focusRequestToken={focusRequest?.target === 'files' ? focusRequest.nonce : null}
317+
focusRequestToken={layout.getFocusRequestToken('files')}
404318
onActivateNode={fileTree.setActiveNode}
405319
onToggle={fileTree.toggleNode}
406320
onSelect={fileTree.selectNode}
@@ -440,9 +354,7 @@ const App: React.FC = () => {
440354
return (
441355
<TerminalPanel
442356
panelKey={panelKey}
443-
focusRequestToken={focusRequest?.target === 'terminal' && (!focusRequest.panelKey || focusRequest.panelKey === panelKey)
444-
? focusRequest.nonce
445-
: null}
357+
focusRequestToken={layout.getFocusRequestToken('terminal', panelKey)}
446358
isConnected={isConnected}
447359
terminals={terminals.terminals}
448360
terminalPanes={terminalPanes}
@@ -456,9 +368,7 @@ const App: React.FC = () => {
456368
return (
457369
<AgentPanel
458370
panelKey={panelKey}
459-
focusRequestToken={focusRequest?.target === 'agent' && (!focusRequest.panelKey || focusRequest.panelKey === panelKey)
460-
? focusRequest.nonce
461-
: null}
371+
focusRequestToken={layout.getFocusRequestToken('agent', panelKey)}
462372
isConnected={isConnected}
463373
agentPanes={agentPanes}
464374
agents={agents}
@@ -499,10 +409,7 @@ const App: React.FC = () => {
499409
};
500410

501411
const handlePanelAdded = useCallback((panelId: PanelId, panelKey: string) => {
502-
const current = panelKeysByIdRef.current[panelId] ?? [];
503-
if (!current.includes(panelKey)) {
504-
panelKeysByIdRef.current[panelId] = [...current, panelKey];
505-
}
412+
layout.handlePanelAdded(panelId, panelKey);
506413

507414
if (panelId === 'changes') {
508415
git.fetchGitStatus();
@@ -520,13 +427,10 @@ const App: React.FC = () => {
520427
if (panelId === 'terminal') {
521428
terminalPanes.registerPane(panelKey);
522429
}
523-
}, [agentPanes, editors, git.fetchGitStatus, terminalPanes]);
430+
}, [agentPanes, editors, git.fetchGitStatus, layout, terminalPanes]);
524431

525432
const handlePanelRemoved = useCallback((panelId: PanelId, panelKey: string) => {
526-
const current = panelKeysByIdRef.current[panelId] ?? [];
527-
if (current.includes(panelKey)) {
528-
panelKeysByIdRef.current[panelId] = current.filter((key) => key !== panelKey);
529-
}
433+
layout.handlePanelRemoved(panelId, panelKey);
530434

531435
if (panelId === 'editor') {
532436
editors.unregisterEditorPane(panelKey);
@@ -547,10 +451,10 @@ const App: React.FC = () => {
547451
if (panelId === 'terminal') {
548452
terminalPanes.unregisterPane(panelKey);
549453
}
550-
}, [agentPanes, editors, terminalPanes]);
454+
}, [agentPanes, editors, layout, terminalPanes]);
551455

552456
const handlePanelActivated = useCallback((panelId: PanelId, panelKey: string) => {
553-
activePanelRef.current = { panelId, panelKey };
457+
layout.handlePanelActivated(panelId, panelKey);
554458

555459
if (panelId === 'editor') {
556460
editors.setActiveEditorPaneId(panelKey);
@@ -568,14 +472,13 @@ const App: React.FC = () => {
568472
return;
569473
}
570474
if (panelId === 'agent') {
571-
activeAgentPaneIdRef.current = panelKey;
572475
agentPanes.setActivePaneId(panelKey);
573476
return;
574477
}
575478
if (panelId === 'terminal') {
576479
terminalPanes.setActivePaneId(panelKey);
577480
}
578-
}, [agentPanes, editors, terminalPanes]);
481+
}, [agentPanes, editors, layout, terminalPanes]);
579482

580483
return (
581484
<div className="app-container toolbar-header-compact">

anycode/components/layout/Layout.tsx

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ import {
2020
saveItem,
2121
} from '../../storage';
2222
import { Icons } from '../Icons';
23+
import {
24+
CURRENT_LAYOUT_VERSION,
25+
LAYOUT_VERSION_STORAGE_KEY,
26+
createLayoutState,
27+
getDockviewLayout,
28+
isLayoutState,
29+
loadLayoutState,
30+
storeLayoutState,
31+
type DockviewLayout,
32+
} from './layoutState';
2333
import './Layout.css';
2434

2535
export type SplitPaneConfig = {
@@ -189,8 +199,6 @@ type LayoutProps = {
189199
isEditorDiffEnabled?: (panelKey: string) => boolean;
190200
};
191201

192-
type SerializedLayout = ReturnType<DockviewApi['toJSON']>;
193-
194202
const PANEL_INSTANCE_SEPARATOR = '__';
195203
const EMPTY_PANE_PREFIX = 'empty-pane-';
196204

@@ -293,10 +301,6 @@ const panelTitles: Record<PanelId, string> = Object.fromEntries(
293301
) as Record<PanelId, string>;
294302

295303
const panelSyncOrder: PanelId[] = ['files', 'editor', 'agent', 'search', 'changes', 'terminal'];
296-
const LAYOUT_STORAGE_KEY = 'layout';
297-
const LAYOUT_VERSION_STORAGE_KEY = 'layoutVersion';
298-
const CURRENT_LAYOUT_VERSION = 4;
299-
300304
const loadPanelVisibility = (): PanelVisibility => ({
301305
files: (loadItem<number>(LAYOUT_VERSION_STORAGE_KEY) ?? 0) < CURRENT_LAYOUT_VERSION ? true : loadFilesPanelVisible(),
302306
search: (loadItem<number>(LAYOUT_VERSION_STORAGE_KEY) ?? 0) < CURRENT_LAYOUT_VERSION ? true : (loadItem<boolean>('searchPanelVisible') ?? false),
@@ -307,16 +311,25 @@ const loadPanelVisibility = (): PanelVisibility => ({
307311
toolbar: true,
308312
});
309313

310-
const hasSavedPanel = (layout: SerializedLayout, panelId: PanelId): boolean => (
314+
const hasSavedPanel = (layout: DockviewLayout, panelId: PanelId): boolean => (
311315
Object.keys(layout.panels).some((panelKey) => getPanelBaseId(panelKey) === panelId)
312316
);
313317

314-
const shouldUseSavedLayout = (layout: SerializedLayout): boolean => {
318+
const shouldUseSavedLayout = (layoutState: ReturnType<typeof loadLayoutState>): boolean => {
319+
if (!layoutState) {
320+
return false;
321+
}
322+
315323
const savedLayoutVersion = loadItem<number>(LAYOUT_VERSION_STORAGE_KEY) ?? 0;
316-
if (savedLayoutVersion < CURRENT_LAYOUT_VERSION) {
324+
if (isLayoutState(layoutState) && layoutState.version !== CURRENT_LAYOUT_VERSION) {
325+
return false;
326+
}
327+
328+
if (!isLayoutState(layoutState) && savedLayoutVersion < 4) {
317329
return false;
318330
}
319331

332+
const layout = getDockviewLayout(layoutState, (panelId) => panelTitles[panelId]);
320333
if (hasSavedPanel(layout, 'toolbar')) {
321334
return false;
322335
}
@@ -583,8 +596,7 @@ export const Layout: React.FC<LayoutProps> = ({
583596
Object.entries(raw.panels).map(([id, state]) => [id, { ...state, params: {} }]),
584597
),
585598
};
586-
saveItem(LAYOUT_STORAGE_KEY, sanitized);
587-
saveItem(LAYOUT_VERSION_STORAGE_KEY, CURRENT_LAYOUT_VERSION);
599+
storeLayoutState(createLayoutState(sanitized, getPanelBaseId));
588600
}, 120);
589601
}, []);
590602

@@ -838,8 +850,11 @@ export const Layout: React.FC<LayoutProps> = ({
838850
}),
839851
];
840852

841-
const savedLayout = loadItem<SerializedLayout>(LAYOUT_STORAGE_KEY);
842-
const useSavedLayout = Boolean(savedLayout?.grid && savedLayout?.panels && shouldUseSavedLayout(savedLayout));
853+
const savedLayoutState = loadLayoutState();
854+
const savedLayout = savedLayoutState
855+
? getDockviewLayout(savedLayoutState, (panelId) => panelTitles[panelId])
856+
: null;
857+
const useSavedLayout = Boolean(savedLayout?.grid && savedLayout?.panels && shouldUseSavedLayout(savedLayoutState));
843858
let restoredSavedLayout = false;
844859

845860
isRestoringLayoutRef.current = true;

0 commit comments

Comments
 (0)