diff --git a/apps/web/src/hooks/useWorkspacePanes.ts b/apps/web/src/hooks/useWorkspacePanes.ts index 8a0f72d..101193f 100644 --- a/apps/web/src/hooks/useWorkspacePanes.ts +++ b/apps/web/src/hooks/useWorkspacePanes.ts @@ -50,8 +50,8 @@ export function activePaneChatId(pane: WorkspacePane): string | undefined { // v1.9: settings pane factory. No chats, no state beyond identity — the // SettingsPane component renders Session/Project sections from the // surrounding session/project. -function settingsPane(): WorkspacePane { - return { id: generateId(), kind: 'settings', chatIds: [], activeChatIdx: -1 }; +function settingsPane(id: string = generateId()): WorkspacePane { + return { id, kind: 'settings', chatIds: [], activeChatIdx: -1 }; } // v1.14.x-html-artifact-panes: artifact pane factories. Payload travels with @@ -135,7 +135,7 @@ export interface UseWorkspacePanesResult { // Open-on-first-click, close-on-second-click. Singleton — settings panes // don't count toward MAX_PANES. Closing the only remaining pane (edge case) // falls back to an empty pane to preserve the "always one pane" invariant. - toggleSettingsPane: () => void; + toggleSettingsPane: () => string | null; removePane: (idx: number) => void; removeChatFromPanes: (chatId: string) => void; initializeFirstChatIfEmpty: (chatId: string) => void; @@ -492,14 +492,21 @@ export function useWorkspacePanes(sessionId: string): UseWorkspacePanesResult { return success ? newPaneId : null; }, [seedPaneChat]); - const toggleSettingsPane = useCallback(() => { + // Returns the new settings pane id when one is OPENED (so mobile callers can + // push ?pane= atomically — see addPaneAndSwitch), or null when it was closed. + // Id generated outside the updater so a strict-mode double-invoke agrees. + const toggleSettingsPane = useCallback((): string | null => { + const newPaneId = generateId(); + let openedId: string | null = null; setPanes((prev) => { const existingIdx = prev.findIndex((p) => p.kind === 'settings'); if (existingIdx < 0) { - const next = [...prev, settingsPane()]; + const next = [...prev, settingsPane(newPaneId)]; setActivePaneIdx(next.length - 1); + openedId = newPaneId; return next; } + openedId = null; if (prev.length <= 1) { setActivePaneIdx(0); return [emptyPane()]; @@ -508,6 +515,7 @@ export function useWorkspacePanes(sessionId: string): UseWorkspacePanesResult { setActivePaneIdx((ai) => Math.min(ai, next.length - 1)); return next; }); + return openedId; }, []); const removePane = useCallback((idx: number) => { diff --git a/apps/web/src/pages/Session.tsx b/apps/web/src/pages/Session.tsx index 3a4353c..ffc335b 100644 --- a/apps/web/src/pages/Session.tsx +++ b/apps/web/src/pages/Session.tsx @@ -123,6 +123,20 @@ function SessionInner({ sessionId }: { sessionId: string }) { }; }, [sessionId]); + // v2.3: opening the settings pane on mobile must push ?pane= atomically, or + // the URL-sync effect below snaps activePaneIdx back to the chat pane and the + // settings pane never shows (same fix as addPaneAndSwitch). toggleSettingsPane + // returns the new pane id when it opens (null when it closes → drop ?pane= so + // the effect falls back to pane 0). Desktop has no URL pane state — no-op. + const toggleSettingsAndSync = useCallback(() => { + const openedId = panesHook.toggleSettingsPane(); + if (!isMobile) return; + const params = new URLSearchParams(location.search); + if (openedId) params.set('pane', openedId); + else params.delete('pane'); + navigate(`${location.pathname}?${params.toString()}`); + }, [panesHook, isMobile, navigate, location.pathname, location.search]); + useEffect(() => { return sessionEvents.subscribe((event) => { if (event.type === 'session_renamed' && event.session_id === sessionId) { @@ -156,10 +170,10 @@ function SessionInner({ sessionId }: { sessionId: string }) { // Sidebar Settings button broadcasts this when a session is mounted; // toggleSettingsPane opens on first click, closes on second. if (event.type === 'open_settings_pane') { - panesHook.toggleSettingsPane(); + toggleSettingsAndSync(); } }); - }, [sessionId, editingName, navigate, project, panesHook]); + }, [sessionId, editingName, navigate, project, toggleSettingsAndSync]); // v1.8: URL ?pane= sync (mobile only). Lifted from Workspace.tsx so // MobileTabSwitcher's onSwitchPane can push the same URL state and the