diff --git a/docs/split-groups-rollout-pr5.md b/docs/split-groups-rollout-pr5.md new file mode 100644 index 00000000..09ea2543 --- /dev/null +++ b/docs/split-groups-rollout-pr5.md @@ -0,0 +1,19 @@ +# Split Groups PR 5: Hook Group Surfaces Into Flagged Path + +This branch wires terminal, editor, and browser surfaces into the split-group +ownership path inside `Terminal.tsx`, but holds that path behind a temporary +local gate. + +Scope: +- remove duplicate legacy ownership under the flagged path +- route group-local surface creation and restore through the new model +- preserve existing default behavior while the flag stays off + +What Is Actually Hooked Up In This PR: +- `Terminal.tsx` now contains the real split-group surface path +- the new path mounts `TabGroupSplitLayout` and avoids keeping duplicate legacy surfaces mounted underneath +- the old legacy surface path is still present as the active runtime path in this branch + +What Is Not Hooked Up Yet: +- the split-group path is still disabled by the temporary local rollout gate in `Terminal.tsx` +- users should still get legacy behavior by default in this branch diff --git a/src/renderer/src/components/Terminal.tsx b/src/renderer/src/components/Terminal.tsx index 2a857e94..2c261f14 100644 --- a/src/renderer/src/components/Terminal.tsx +++ b/src/renderer/src/components/Terminal.tsx @@ -27,9 +27,14 @@ import { isUpdaterQuitAndInstallInProgress } from '@/lib/updater-beforeunload' import EditorAutosaveController from './editor/EditorAutosaveController' import BrowserPane, { destroyPersistentWebview } from './browser-pane/BrowserPane' import { reconcileTabOrder } from './tab-bar/reconcile-order' +import TabGroupSplitLayout from './tab-group/TabGroupSplitLayout' import { shouldAutoCreateInitialTerminal } from './terminal/initial-terminal' const EditorPanel = lazy(() => import('./editor/EditorPanel')) +// Why: the split-group ownership path lands before the rollout switch so we +// can exercise the full renderer/model integration in code review without +// exposing partial behavior to users. PR6 flips this to true. +const ENABLE_SPLIT_GROUPS = false function Terminal(): React.JSX.Element | null { const activeWorktreeId = useAppStore((s) => s.activeWorktreeId) @@ -50,8 +55,6 @@ function Terminal(): React.JSX.Element | null { const clearCodexRestartNotice = useAppStore((s) => s.clearCodexRestartNotice) const expandedPaneByTabId = useAppStore((s) => s.expandedPaneByTabId) const workspaceSessionReady = useAppStore((s) => s.workspaceSessionReady) - const ensureWorktreeRootGroup = useAppStore((s) => s.ensureWorktreeRootGroup) - const reconcileWorktreeTabModel = useAppStore((s) => s.reconcileWorktreeTabModel) const openFiles = useAppStore((s) => s.openFiles) const activeFileId = useAppStore((s) => s.activeFileId) const activeBrowserTabId = useAppStore((s) => s.activeBrowserTabId) @@ -66,6 +69,11 @@ function Terminal(): React.JSX.Element | null { const createBrowserTab = useAppStore((s) => s.createBrowserTab) const closeBrowserTab = useAppStore((s) => s.closeBrowserTab) const setActiveBrowserTab = useAppStore((s) => s.setActiveBrowserTab) + const groupsByWorktree = useAppStore((s) => s.groupsByWorktree) + const layoutByWorktree = useAppStore((s) => s.layoutByWorktree) + const activeGroupIdByWorktree = useAppStore((s) => s.activeGroupIdByWorktree) + const ensureWorktreeRootGroup = useAppStore((s) => s.ensureWorktreeRootGroup) + const reconcileWorktreeTabModel = useAppStore((s) => s.reconcileWorktreeTabModel) const markFileDirty = useAppStore((s) => s.markFileDirty) const setTabBarOrder = useAppStore((s) => s.setTabBarOrder) @@ -100,6 +108,90 @@ function Terminal(): React.JSX.Element | null { const worktreeBrowserTabs = activeWorktreeId ? (browserTabsByWorktree[activeWorktreeId] ?? []) : [] + const getEffectiveLayoutForWorktree = useCallback( + (worktreeId: string) => { + const layout = layoutByWorktree[worktreeId] + if (layout) { + return layout + } + const groups = groupsByWorktree[worktreeId] ?? [] + const fallbackGroupId = activeGroupIdByWorktree[worktreeId] ?? groups[0]?.id ?? null + if (!fallbackGroupId) { + return undefined + } + return { type: 'leaf', groupId: fallbackGroupId } as const + }, + [activeGroupIdByWorktree, groupsByWorktree, layoutByWorktree] + ) + const effectiveActiveLayout = activeWorktreeId + ? ENABLE_SPLIT_GROUPS + ? getEffectiveLayoutForWorktree(activeWorktreeId) + : undefined + : undefined + const activeWorktree = activeWorktreeId + ? (allWorktrees.find((worktree) => worktree.id === activeWorktreeId) ?? null) + : null + const activeTerminalTab = tabs.find((tab) => tab.id === activeTabId) ?? null + const activeEditorFile = worktreeFiles.find((file) => file.id === activeFileId) ?? null + const activeBrowserTab = worktreeBrowserTabs.find((tab) => tab.id === activeBrowserTabId) ?? null + const activeSurfaceLabel = + activeTabType === 'browser' + ? (activeBrowserTab?.title ?? activeBrowserTab?.url ?? 'Browser') + : activeTabType === 'editor' + ? (activeEditorFile?.relativePath ?? activeEditorFile?.filePath ?? 'Editor') + : (activeTerminalTab?.customTitle ?? activeTerminalTab?.title ?? 'Terminal') + const renderStaleCodexRestartChip = useCallback( + (worktreeId: string) => { + const worktreeTabs = tabsByWorktree[worktreeId] ?? [] + const staleWorktreePtyIds = worktreeTabs.flatMap((tab) => + (ptyIdsByTabId[tab.id] ?? []).filter((ptyId) => Boolean(codexRestartNoticeByPtyId[ptyId])) + ) + if (staleWorktreePtyIds.length === 0) { + return null + } + // Why: split-group and legacy workspace rendering both represent the + // same worktree-level Codex session state. Keeping one shared chip here + // preserves the single-prompt UX across rollout paths instead of letting + // one branch silently lose the restart/dismiss affordance. + return ( +
+
+ + Codex is using the previous account + +
+ + +
+
+
+ ) + }, + [ + clearCodexRestartNotice, + codexRestartNoticeByPtyId, + ptyIdsByTabId, + queueCodexPaneRestarts, + tabsByWorktree + ] + ) const activeWorktreeBrowserTabIdsKey = activeWorktreeId ? (browserTabsByWorktree[activeWorktreeId] ?? []).map((tab) => tab.id).join(',') : '' @@ -197,13 +289,7 @@ function Terminal(): React.JSX.Element | null { return } createTab(activeWorktreeId) - }, [ - workspaceSessionReady, - activeWorktreeId, - tabs.length, - createTab, - reconcileWorktreeTabModel - ]) + }, [workspaceSessionReady, activeWorktreeId, tabs.length, createTab, reconcileWorktreeTabModel]) const handleNewTab = useCallback(() => { if (!activeWorktreeId) { @@ -771,10 +857,11 @@ function Terminal(): React.JSX.Element | null { > - {/* Why: the tab bar is rendered into the titlebar via a portal so it - shares the same visual row as the "Orca" title. The portal target - (#titlebar-tabs) lives in App.tsx's titlebar. */} + {/* Why: once split groups are enabled, each group owns its own tab strip + inline like VS Code. The old titlebar portal stays only as a fallback + before the root-group layout has been established. */} {activeWorktreeId && + !effectiveActiveLayout && titlebarTabsTarget && createPortal(