refactor(tabs): delete unused copy-tab and merge-group store actions (#28836)

* refactor(tabs): delete unused copy-tab and merge-group store actions

copyUnifiedTabToGroup and mergeGroupIntoSibling had no callers outside
the store and its tests. Tests that used the copy action as a setup step
now create the duplicate tab directly, the same call the action made.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(tabs): follow the split-ratio file rename in the layout-writer baseline

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jinwoo Hong
2026-10-14 17:07:20 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 2de7381204
commit b024043feb
9 changed files with 60 additions and 180 deletions
+1 -1
View File
@@ -95,8 +95,8 @@
8 src/renderer/src/store/slices/tabs/tabs-label-actions.ts
10 src/renderer/src/store/slices/tabs/tabs-move-actions.ts
9 src/renderer/src/store/slices/tabs/tabs-reconciliation.ts
1 src/renderer/src/store/slices/tabs/tabs-secondary-actions.ts
4 src/renderer/src/store/slices/tabs/tabs-session-actions.ts
1 src/renderer/src/store/slices/tabs/tabs-split-ratio-actions.ts
5 src/renderer/src/store/slices/tabs/tabs-surface.ts
6 src/renderer/src/store/slices/terminal-orphan-helpers.ts
4 src/renderer/src/store/slices/terminal-tab-title-batch.ts
@@ -173,7 +173,11 @@ it('scopes the floating reveal to its selected split pane', () => {
if (!siblingGroupId) {
throw new Error('Split missing')
}
store.getState().copyUnifiedTabToGroup(tab.id, siblingGroupId)
store.getState().createUnifiedTab(FLOATING, tab.contentType, {
entityId: tab.entityId,
label: tab.label,
targetGroupId: siblingGroupId
})
store.getState().focusGroup(FLOATING, tab.groupId)
position(fileId, { line: 300, column: 7 }, true)
flushFrames()
@@ -74,7 +74,7 @@ describe('resolveWebSessionVisibleTabId — grouped state is authoritative', ()
expect(resolveWebSessionVisibleTabId(state, WT)).toBe('tab-shown')
})
// Why: copyUnifiedTabToGroup duplicates contentType + entityId into another group, so a plain
// Why: opening a file into a second split group duplicates contentType + entityId, so a plain
// array scan can return the background copy and drag group focus to it.
it('prefers the active group copy when one entity exists in two split groups', () => {
const background = tab({
@@ -1,6 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import type * as AgentStatusModule from '@/lib/agent-status'
import { getDefaultUIState } from '../../../../shared/constants'
import { createTabsSliceMockApi } from './tabs-slice-test-harness'
import { createTestStore } from './store-test-helpers'
@@ -131,63 +130,6 @@ describe('TabsSlice', () => {
).toEqual([tab.id])
})
it('copies a unified tab into another group', () => {
const tab = store.getState().createUnifiedTab(WT, 'editor', {
id: 'file-a.ts',
executionHostId: 'runtime:host-b',
label: 'file-a.ts'
})
const sourceGroupId = store.getState().groupsByWorktree[WT][0].id
const targetGroupId = store.getState().createEmptySplitGroup(WT, sourceGroupId, 'right')
expect(targetGroupId).toBeTruthy()
const copied = store.getState().copyUnifiedTabToGroup(tab.id, targetGroupId!)
expect(copied).not.toBeNull()
const state = store.getState()
expect(state.unifiedTabsByWorktree[WT]).toHaveLength(2)
expect(
state.groupsByWorktree[WT].find((group) => group.id === sourceGroupId)?.tabOrder
).toEqual([tab.id])
expect(
state.groupsByWorktree[WT].find((group) => group.id === targetGroupId)?.tabOrder
).toEqual([copied!.id])
expect(copied?.entityId).toBe(tab.entityId)
expect(copied?.executionHostId).toBe('runtime:host-b')
})
it('merges a group into its sibling', () => {
const setMock = vi.mocked(window.api.ui.set)
const t1 = store.getState().createUnifiedTab(WT, 'editor', {
id: 'file-a.ts',
label: 'file-a.ts'
})
const sourceGroupId = store.getState().groupsByWorktree[WT][0].id
const targetGroupId = store.getState().createEmptySplitGroup(WT, sourceGroupId, 'right')
expect(targetGroupId).toBeTruthy()
store.getState().createUnifiedTab(WT, 'editor', {
id: 'file-b.ts',
label: 'file-b.ts',
targetGroupId: targetGroupId!
})
store.getState().hydratePersistedUI(getDefaultUIState())
setMock.mockClear()
const mergedInto = store.getState().mergeGroupIntoSibling(WT, targetGroupId!)
expect(mergedInto).toBe(sourceGroupId)
const state = store.getState()
expect(state.groupsByWorktree[WT]).toHaveLength(1)
expect(state.groupsByWorktree[WT][0].tabOrder).toEqual([t1.id, 'file-b.ts'])
expect(state.layoutByWorktree[WT]).toEqual({ type: 'leaf', groupId: sourceGroupId })
expect(setMock).toHaveBeenCalledTimes(1)
expect(setMock).toHaveBeenCalledWith({
featureInteractions: {
'terminal-panes': expect.objectContaining({ interactionCount: 1 })
}
})
})
it('drops a unified tab into another group and collapses an emptied source group', () => {
const tab = store.getState().createUnifiedTab(WT, 'editor', {
id: 'file-a.ts',
@@ -9,7 +9,7 @@ import { createTabsLabelActions } from './tabs-label-actions'
import { createTabsGroupActions } from './tabs-group-actions'
import { createTabsMoveActions } from './tabs-move-actions'
import { createTabsDropActions } from './tabs-drop-actions'
import { createTabsSecondaryActions } from './tabs-secondary-actions'
import { createTabsSplitRatioActions } from './tabs-split-ratio-actions'
import { createTabsSessionActions } from './tabs-session-actions'
export const createTabsSlice: StateCreator<AppState, [], [], TabsSlice> = (set, get) => ({
@@ -25,6 +25,6 @@ export const createTabsSlice: StateCreator<AppState, [], [], TabsSlice> = (set,
...createTabsGroupActions(set, get),
...createTabsMoveActions(set, get),
...createTabsDropActions(set, get),
...createTabsSecondaryActions(set, get),
...createTabsSplitRatioActions(set),
...createTabsSessionActions(set, get)
})
@@ -1,97 +0,0 @@
import type { TabGroupLayoutNode } from '../../../../../shared/tab-types'
import type { TabsSlice, TabsSliceGet, TabsSliceSet } from './tabs-slice-contract'
import { findGroupAndWorktree, findTabAndWorktree } from '../tab-group-state'
import {
findSiblingGroupId,
updateSplitRatio
} from '../../../../../shared/workspace-layout/tab-group-layout-tree'
export function createTabsSecondaryActions(
set: TabsSliceSet,
get: TabsSliceGet
): Pick<TabsSlice, 'copyUnifiedTabToGroup' | 'mergeGroupIntoSibling' | 'setTabGroupSplitRatio'> {
return {
copyUnifiedTabToGroup: (tabId, targetGroupId, init) => {
const foundTab = findTabAndWorktree(get().unifiedTabsByWorktree, tabId)
const foundTarget = findGroupAndWorktree(get().groupsByWorktree, targetGroupId)
if (!foundTab || !foundTarget || foundTab.worktreeId !== foundTarget.worktreeId) {
return null
}
const { tab, worktreeId } = foundTab
return get().createUnifiedTab(worktreeId, tab.contentType, {
entityId: init?.entityId ?? tab.entityId,
executionHostId: tab.executionHostId,
label: init?.label ?? tab.label,
generatedLabel: init?.generatedLabel ?? tab.generatedLabel,
quickCommandLabel: init?.quickCommandLabel ?? tab.quickCommandLabel,
customLabel: init?.customLabel ?? tab.customLabel,
color: init?.color ?? tab.color,
isPinned: init?.isPinned ?? tab.isPinned,
id: init?.id,
targetGroupId
})
},
mergeGroupIntoSibling: (worktreeId, groupId) => {
const state = get()
const groups = state.groupsByWorktree[worktreeId] ?? []
const sourceGroup = groups.find((candidate) => candidate.id === groupId)
const layout = state.layoutByWorktree[worktreeId]
if (!sourceGroup || !layout || groups.length <= 1) {
return null
}
const targetGroupId = findSiblingGroupId(layout, groupId)
if (!targetGroupId) {
return null
}
const orderedSourceTabs = (state.unifiedTabsByWorktree[worktreeId] ?? []).filter(
(tab) => tab.groupId === groupId
)
for (const tabId of sourceGroup.tabOrder) {
const item = orderedSourceTabs.find((tab) => tab.id === tabId)
if (!item) {
continue
}
get().moveUnifiedTabToGroup(item.id, targetGroupId, { recordInteraction: false })
}
get().closeEmptyGroup(worktreeId, groupId)
get().recordFeatureInteraction?.('terminal-panes')
return targetGroupId
},
setTabGroupSplitRatio: (worktreeId, nodePath, ratio) => {
set((state) => {
const currentLayout = state.layoutByWorktree[worktreeId]
if (!currentLayout) {
return state
}
// Why: an unchanged ratio must not mint fresh root state — every store
// subscriber wakes on the new reference (STA-3328).
let targetNode: TabGroupLayoutNode | undefined = currentLayout
for (const segment of nodePath.length > 0 ? nodePath.split('.') : []) {
targetNode =
targetNode &&
targetNode.type === 'split' &&
(segment === 'first' || segment === 'second')
? targetNode[segment]
: undefined
}
if (!targetNode || targetNode.type !== 'split' || (targetNode.ratio ?? 0.5) === ratio) {
return state
}
return {
layoutByWorktree: {
...state.layoutByWorktree,
// Why: split ratios belong to the tab-group model (not transient UI), so persist them for restores and multi-step group ops.
[worktreeId]: updateSplitRatio(
currentLayout,
nodePath.length > 0 ? nodePath.split('.') : [],
ratio
)
}
}
})
}
}
}
@@ -136,24 +136,6 @@ export type TabsSlice = {
splitDirection?: TabSplitDirection
}
) => boolean
copyUnifiedTabToGroup: (
tabId: string,
targetGroupId: string,
init?: Partial<
Pick<
Tab,
| 'id'
| 'entityId'
| 'label'
| 'generatedLabel'
| 'quickCommandLabel'
| 'customLabel'
| 'color'
| 'isPinned'
>
>
) => Tab | null
mergeGroupIntoSibling: (worktreeId: string, groupId: string) => string | null
setTabGroupSplitRatio: (worktreeId: string, nodePath: string, ratio: number) => void
reconcileWorktreeTabModel: (worktreeId: string) => {
renderableTabCount: number
@@ -0,0 +1,43 @@
import type { TabGroupLayoutNode } from '../../../../../shared/tab-types'
import type { TabsSlice, TabsSliceSet } from './tabs-slice-contract'
import { updateSplitRatio } from '../../../../../shared/workspace-layout/tab-group-layout-tree'
export function createTabsSplitRatioActions(
set: TabsSliceSet
): Pick<TabsSlice, 'setTabGroupSplitRatio'> {
return {
setTabGroupSplitRatio: (worktreeId, nodePath, ratio) => {
set((state) => {
const currentLayout = state.layoutByWorktree[worktreeId]
if (!currentLayout) {
return state
}
// Why: an unchanged ratio must not mint fresh root state — every store
// subscriber wakes on the new reference (STA-3328).
let targetNode: TabGroupLayoutNode | undefined = currentLayout
for (const segment of nodePath.length > 0 ? nodePath.split('.') : []) {
targetNode =
targetNode &&
targetNode.type === 'split' &&
(segment === 'first' || segment === 'second')
? targetNode[segment]
: undefined
}
if (!targetNode || targetNode.type !== 'split' || (targetNode.ratio ?? 0.5) === ratio) {
return state
}
return {
layoutByWorktree: {
...state.layoutByWorktree,
// Why: split ratios belong to the tab-group model (not transient UI), so persist them for restores and multi-step group ops.
[worktreeId]: updateSplitRatio(
currentLayout,
nodePath.length > 0 ? nodePath.split('.') : [],
ratio
)
}
}
})
}
}
}
+8 -2
View File
@@ -133,9 +133,15 @@ test('CLI file positioning preserves background navigation and reveals the reque
await orcaPage.evaluate(
({ id, split }) => {
const state = window.__store?.getState()
if (!state?.copyUnifiedTabToGroup(split.tabId, split.siblingGroupId)) {
throw new Error('Editor tab was not copied')
const tab = state?.unifiedTabsByWorktree[id]?.find((item) => item.id === split.tabId)
if (!state || !tab) {
throw new Error('Editor tab missing')
}
state.createUnifiedTab(id, tab.contentType, {
entityId: tab.entityId,
label: tab.label,
targetGroupId: split.siblingGroupId
})
state.focusGroup(id, split.groupId)
},
{ id: worktreeId, split }