From 2d42e77309baa88c262cedd2146b2835a2b32623 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:48:03 -0700 Subject: [PATCH] Revert "Preserve explicit slept workspace state (#4536)" This reverts commit 0d2abc5cb700a7114fff1384066ee1355c163d1c. --- src/main/ipc/remote-workspace.test.ts | 27 +--- src/main/ipc/remote-workspace.ts | 10 +- .../src/components/WorktreeJumpPalette.tsx | 17 ++- .../sidebar/WorktreeContextMenu.test.ts | 23 +++ .../sidebar/WorktreeContextMenu.tsx | 34 ++++- .../src/components/sidebar/WorktreeList.tsx | 20 ++- .../sleep-worktree-activation-race.test.ts | 6 - .../sidebar/sleep-worktree-flow.test.ts | 6 - .../components/sidebar/sleep-worktree-flow.ts | 2 - ...e-visible-workspace-kanban-worktree-ids.ts | 14 +- .../sidebar/visible-worktrees.test.ts | 96 ++++++++---- .../components/sidebar/visible-worktrees.ts | 22 ++- src/renderer/src/hooks/useIpcEvents.test.ts | 142 ------------------ src/renderer/src/hooks/useIpcEvents.ts | 8 - .../lib/sidebar-worktree-activation.test.ts | 15 +- .../src/lib/sidebar-worktree-activation.ts | 13 +- .../workspace-session-browser-history.test.ts | 3 +- .../lib/workspace-session-liveness.test.ts | 1 - .../src/lib/workspace-session-patch.ts | 4 - .../workspace-session-relevant-fields.test.ts | 3 +- src/renderer/src/lib/workspace-session.ts | 17 +-- .../src/lib/worktree-activity-state.ts | 8 - src/renderer/src/store/slices/browser.test.ts | 10 -- src/renderer/src/store/slices/browser.ts | 12 +- src/renderer/src/store/slices/repos.ts | 10 -- src/renderer/src/store/slices/settings.ts | 1 - src/renderer/src/store/slices/tabs.test.ts | 17 --- .../store/slices/terminals-hydration.test.ts | 98 ------------ src/renderer/src/store/slices/terminals.ts | 62 +------- .../src/store/slices/worktrees.test.ts | 14 -- src/renderer/src/store/slices/worktrees.ts | 22 --- .../src/web/web-workspace-session.test.ts | 2 - src/renderer/src/web/web-workspace-session.ts | 3 +- ...emote-workspace-session-projection.test.ts | 11 +- .../remote-workspace-session-projection.ts | 25 +-- src/shared/remote-workspace-types.ts | 1 - src/shared/types.ts | 3 - src/shared/workspace-session-schema.test.ts | 20 --- src/shared/workspace-session-schema.ts | 3 +- 39 files changed, 215 insertions(+), 590 deletions(-) diff --git a/src/main/ipc/remote-workspace.test.ts b/src/main/ipc/remote-workspace.test.ts index 5b529825c..6da587ac8 100644 --- a/src/main/ipc/remote-workspace.test.ts +++ b/src/main/ipc/remote-workspace.test.ts @@ -99,7 +99,7 @@ describe('remoteWorkspaceSessionMatchesSnapshot', () => { ).toBe(true) }) - it('treats empty optional projection records as equivalent to absent fields', () => { + it('treats empty optional projection fields as equivalent to absent fields', () => { expect( remoteWorkspaceSessionMatchesSnapshot( snapshot({ @@ -107,11 +107,10 @@ describe('remoteWorkspaceSessionMatchesSnapshot', () => { activeTabId: null, tabsByWorktreePath: {}, terminalLayoutsByTabId: {}, + activeWorktreePathsOnShutdown: [], activeTabIdByWorktreePath: {}, remoteSessionIdsByTabId: {}, - lastVisitedAtByWorktreePath: {}, - defaultTerminalTabsAppliedByWorktreePath: {}, - sleptWorktreePaths: {} + lastVisitedAtByWorktreePath: {} }), { activeWorktreePath: null, @@ -123,26 +122,6 @@ describe('remoteWorkspaceSessionMatchesSnapshot', () => { ).toBe(true) }) - it('preserves an explicit empty shutdown list as session state', () => { - expect( - remoteWorkspaceSessionMatchesSnapshot( - snapshot({ - activeWorktreePath: null, - activeTabId: null, - tabsByWorktreePath: {}, - terminalLayoutsByTabId: {}, - activeWorktreePathsOnShutdown: [] - }), - { - activeWorktreePath: null, - activeTabId: null, - tabsByWorktreePath: {}, - terminalLayoutsByTabId: {} - } - ) - ).toBe(false) - }) - it('detects actual target session changes', () => { expect( remoteWorkspaceSessionMatchesSnapshot( diff --git a/src/main/ipc/remote-workspace.ts b/src/main/ipc/remote-workspace.ts index ad797c2af..5ccc3c12c 100644 --- a/src/main/ipc/remote-workspace.ts +++ b/src/main/ipc/remote-workspace.ts @@ -104,9 +104,7 @@ function normalizeOptionalStringArray(value: unknown): string[] | undefined { return undefined } const normalized = value.filter((entry): entry is string => typeof entry === 'string') - // Why: [] means "known no live worktrees"; undefined means legacy/unknown - // and triggers hydration fallback from tab wake hints. - return normalized + return normalized.length > 0 ? normalized : undefined } function normalizeOptionalRecord>(value: unknown): T | undefined { @@ -148,11 +146,7 @@ function normalizeRemoteSession(raw: unknown): RemoteWorkspaceSession { ), lastVisitedAtByWorktreePath: normalizeOptionalRecord>( input.lastVisitedAtByWorktreePath - ), - defaultTerminalTabsAppliedByWorktreePath: normalizeOptionalRecord>( - input.defaultTerminalTabsAppliedByWorktreePath - ), - sleptWorktreePaths: normalizeOptionalRecord>(input.sleptWorktreePaths) + ) } } diff --git a/src/renderer/src/components/WorktreeJumpPalette.tsx b/src/renderer/src/components/WorktreeJumpPalette.tsx index 19b9bdf72..dac7e58f0 100644 --- a/src/renderer/src/components/WorktreeJumpPalette.tsx +++ b/src/renderer/src/components/WorktreeJumpPalette.tsx @@ -17,7 +17,7 @@ import { getLinkedWorkItemSuggestedName } from '@/lib/new-workspace' import type { LinkedWorkItemSummary } from '@/lib/new-workspace' import { sortWorktreesSmart } from '@/components/sidebar/smart-sort' import { isDefaultBranchWorkspace } from '@/components/sidebar/visible-worktrees' -import { isSleptWorkspace } from '@/lib/worktree-activity-state' +import { isInactiveWorkspace } from '@/lib/worktree-activity-state' import { orderEmptyQueryWorktrees } from '@/lib/order-empty-query-worktrees' import StatusIndicator from '@/components/sidebar/StatusIndicator' import { cn } from '@/lib/utils' @@ -267,7 +267,6 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null { const sshConnectionStates = useAppStore((s) => s.sshConnectionStates) const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace) const showSleepingWorkspaces = useAppStore((s) => s.showSleepingWorkspaces) - const sleptWorktreeIds = useAppStore((s) => s.sleptWorktreeIds) const lastVisitedAtByWorktreeId = useAppStore((s) => s.lastVisitedAtByWorktreeId) const workspacePortScan = useAppStore((s) => s.workspacePortScan?.result ?? null) const openNewBrowserTabInActiveWorkspace = useAppStore( @@ -313,12 +312,22 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null { if (hideDefaultBranchWorkspace && isDefaultBranchWorkspace(worktree)) { return false } - if (!showSleepingWorkspaces && isSleptWorkspace(worktree.id, sleptWorktreeIds)) { + if ( + !showSleepingWorkspaces && + isInactiveWorkspace(worktree.id, tabsByWorktree, ptyIdsByTabId, browserTabsByWorktree) + ) { return false } return true }), - [allWorktrees, hideDefaultBranchWorkspace, sleptWorktreeIds, showSleepingWorkspaces] + [ + allWorktrees, + browserTabsByWorktree, + hideDefaultBranchWorkspace, + ptyIdsByTabId, + showSleepingWorkspaces, + tabsByWorktree + ] ) // Why: empty-query rows use focus-recency (lastVisitedAtByWorktreeId) with diff --git a/src/renderer/src/components/sidebar/WorktreeContextMenu.test.ts b/src/renderer/src/components/sidebar/WorktreeContextMenu.test.ts index 12073d1e1..18f19bdff 100644 --- a/src/renderer/src/components/sidebar/WorktreeContextMenu.test.ts +++ b/src/renderer/src/components/sidebar/WorktreeContextMenu.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { + hasSleepableWorkspaceActivity, isContextWorktreeDeletable, shouldUseNativeContextMenu, shouldIgnoreNestedWorktreeContextMenuScope, @@ -105,6 +106,28 @@ describe('shouldContinueDeleteSiblingPositionRestore', () => { }) }) +describe('hasSleepableWorkspaceActivity', () => { + it('treats preserved empty PTY arrays as slept, not live', () => { + expect( + hasSleepableWorkspaceActivity('wt-1', { 'wt-1': [{ id: 'tab-1' }] }, { 'tab-1': [] }, {}) + ).toBe(false) + }) + + it('detects live terminal and browser activity', () => { + expect( + hasSleepableWorkspaceActivity( + 'wt-1', + { 'wt-1': [{ id: 'tab-1' }] }, + { 'tab-1': ['pty-1'] }, + {} + ) + ).toBe(true) + expect(hasSleepableWorkspaceActivity('wt-1', {}, {}, { 'wt-1': [{ id: 'browser-1' }] })).toBe( + true + ) + }) +}) + describe('project removal from workspace context menus', () => { it('routes primary workspace rows to project removal in non-repo grouped views', () => { const gitRepo = { id: 'repo-1' } diff --git a/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx b/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx index 0bf08912b..64c50fcba 100644 --- a/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx +++ b/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx @@ -36,6 +36,7 @@ import type { Repo, Worktree } from '../../../../shared/types' import { runWorktreeBatchDelete, runWorktreeDelete } from './delete-worktree-flow' import { runSleepWorktrees } from './sleep-worktree-flow' import { activateAndRevealWorktree } from '@/lib/worktree-activation' +import { tabHasLivePty } from '@/lib/tab-has-live-pty' import { VIRTUALIZED_SCROLL_ANCHOR_RECORD_EVENT } from '@/hooks/useVirtualizedScrollAnchor' import { getLineageRenderInfo } from './worktree-list-groups' import { getWorkspaceStatus, getWorkspaceStatusVisualMeta } from './workspace-status' @@ -93,6 +94,18 @@ function shouldSuppressContextMenuFollowUpClick(contextMenuOpenedAt: number, now ) } +function hasSleepableWorkspaceActivity( + worktreeId: string, + tabsByWorktree: Record, + ptyIdsByTabId: Record, + browserTabsByWorktree: Record +): boolean { + const tabs = tabsByWorktree[worktreeId] ?? [] + const hasLiveTerminal = tabs.some((tab) => tabHasLivePty(ptyIdsByTabId, tab.id)) + const hasBrowser = (browserTabsByWorktree[worktreeId] ?? []).length > 0 + return hasLiveTerminal || hasBrowser +} + function shouldRemoveProjectFromContextMenu( repo: Pick | null | undefined, worktree: Pick @@ -216,11 +229,21 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({ const worktreeMap = useWorktreeMap() const worktreeLineageById = useAppStore((s) => s.worktreeLineageById) const updateWorktreeLineage = useAppStore((s) => s.updateWorktreeLineage) + const tabsByWorktree = useAppStore((s) => s.tabsByWorktree) + const ptyIdsByTabId = useAppStore((s) => s.ptyIdsByTabId) + const browserTabsByWorktree = useAppStore((s) => s.browserTabsByWorktree) const deleteStateByWorktreeId = useAppStore((s) => s.deleteStateByWorktreeId) const scopeRef = useRef(null) const contextMenuOpenedAtRef = useRef(null) const activeContextWorktrees = menuOpen ? contextWorktrees : effectiveSelectedWorktrees const isMultiContext = activeContextWorktrees.length > 1 + const sleepableWorktrees = useMemo( + () => + activeContextWorktrees.filter((item) => + hasSleepableWorkspaceActivity(item.id, tabsByWorktree, ptyIdsByTabId, browserTabsByWorktree) + ), + [activeContextWorktrees, browserTabsByWorktree, ptyIdsByTabId, tabsByWorktree] + ) const deletingContext = useMemo( () => activeContextWorktrees.some((item) => deleteStateByWorktreeId[item.id]?.isDeleting), [activeContextWorktrees, deleteStateByWorktreeId] @@ -245,8 +268,8 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({ ) const removesProject = shouldRemoveProjectFromContextMenu(repo, worktree) const sleepLabel = - isMultiContext && activeContextWorktrees.length > 0 - ? `Sleep ${activeContextWorktrees.length} Workspace${activeContextWorktrees.length === 1 ? '' : 's'}` + isMultiContext && sleepableWorktrees.length > 0 + ? `Sleep ${sleepableWorktrees.length} Workspace${sleepableWorktrees.length === 1 ? '' : 's'}` : 'Sleep' const deleteLabel = isMultiContext && batchDeleteWorktrees.length > 0 @@ -358,7 +381,7 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({ ]) const handleCloseTerminals = useCallback(() => { - const worktreeIds = activeContextWorktrees.map((item) => item.id) + const worktreeIds = sleepableWorktrees.map((item) => item.id) setMenuOpenState(false) // Why: Sleep can remount the sidebar when it clears the active workspace. // Let Radix finish closing the menu first so its focus/portal teardown @@ -366,7 +389,7 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({ window.setTimeout(() => { void runSleepWorktrees(worktreeIds) }, 50) - }, [activeContextWorktrees, setMenuOpenState]) + }, [setMenuOpenState, sleepableWorktrees]) const handleDelete = useCallback(() => { // Folder mode handled inline because it routes to a different modal; @@ -602,7 +625,7 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({ {sleepLabel} @@ -661,6 +684,7 @@ export { CLOSE_ALL_CONTEXT_MENUS_EVENT, WORKTREE_CONTEXT_MENU_SCOPE_ATTR, WORKTREE_NATIVE_CONTEXT_MENU_ATTR, + hasSleepableWorkspaceActivity, isContextWorktreeDeletable, shouldRemoveProjectFromContextMenu, shouldUseNativeContextMenu, diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index df4d19497..a9b58e21e 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -107,6 +107,7 @@ import { } from './visible-worktrees' import { getVisibleWorktreeBrowserActivityTabs, + getVisibleWorktreeTerminalActivityTabs, getWorktreeSectionTerminalActivityTabs } from './visible-worktree-activity-inputs' import { selectTerminalLayoutRootsForWorktrees } from './worktree-card-status-inputs' @@ -3483,7 +3484,6 @@ const WorktreeList = React.memo(function WorktreeList({ const sortBy = useAppStore((s) => s.sortBy) const setSortBy = useAppStore((s) => s.setSortBy) const showSleepingWorkspaces = useAppStore((s) => s.showSleepingWorkspaces) - const sleptWorktreeIds = useAppStore((s) => s.sleptWorktreeIds) const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace) const filterRepoIds = useAppStore((s) => s.filterRepoIds) const openModal = useAppStore((s) => s.openModal) @@ -3540,6 +3540,16 @@ const WorktreeList = React.memo(function WorktreeList({ agentTargetTerminalLayoutsByTabId ]) + // Read tabsByWorktree when needed for filtering or sorting + const needsActivityMaps = !showSleepingWorkspaces || sortBy === 'smart' + const tabsByWorktree = useAppStore((s) => + needsActivityMaps ? getVisibleWorktreeTerminalActivityTabs(s.tabsByWorktree) : null + ) + const ptyIdsByTabId = useAppStore((s) => (needsActivityMaps ? s.ptyIdsByTabId : null)) + const browserTabsByWorktree = useAppStore((s) => + !showSleepingWorkspaces ? getVisibleWorktreeBrowserActivityTabs(s.browserTabsByWorktree) : null + ) + const cardProps = useAppStore((s) => s.worktreeCardProperties) // PR cache is needed for PR-status grouping and when the PR card property @@ -3807,7 +3817,9 @@ const WorktreeList = React.memo(function WorktreeList({ const ids = computeVisibleWorktreeIds(worktreesByRepo, sortedIds, { filterRepoIds, showSleepingWorkspaces, - sleptWorktreeIds, + tabsByWorktree, + ptyIdsByTabId, + browserTabsByWorktree, hideDefaultBranchWorkspace, repoMap, worktreeLineageById @@ -3829,7 +3841,9 @@ const WorktreeList = React.memo(function WorktreeList({ showSleepingWorkspaces, hideDefaultBranchWorkspace, repoMap, - sleptWorktreeIds, + tabsByWorktree, + ptyIdsByTabId, + browserTabsByWorktree, sortedIds, worktreeMap, worktreeLineageById, diff --git a/src/renderer/src/components/sidebar/sleep-worktree-activation-race.test.ts b/src/renderer/src/components/sidebar/sleep-worktree-activation-race.test.ts index 596b7c004..916e0adae 100644 --- a/src/renderer/src/components/sidebar/sleep-worktree-activation-race.test.ts +++ b/src/renderer/src/components/sidebar/sleep-worktree-activation-race.test.ts @@ -6,16 +6,12 @@ const mocks = vi.hoisted(() => { setActiveWorktree: vi.fn((worktreeId: string | null) => { state.activeWorktreeId = worktreeId }), - markWorktreeSlept: vi.fn((worktreeId: string) => { - state.sleptWorktreeIds[worktreeId] = true - }), shutdownWorktreeBrowsers: vi.fn().mockResolvedValue(undefined), shutdownWorktreeTerminals: vi.fn(async (worktreeId: string) => { for (const tab of state.tabsByWorktree[worktreeId] ?? []) { state.ptyIdsByTabId[tab.id] = [] } }), - sleptWorktreeIds: {} as Record, tabsByWorktree: {} as Record, ptyIdsByTabId: {} as Record, browserTabsByWorktree: {} as Record, @@ -79,14 +75,12 @@ describe('sleep flow vs queued slept-workspace activation', () => { mocks.toastError.mockClear() mocks.state.activeWorktreeId = 'wt-parent' mocks.state.setActiveWorktree.mockClear() - mocks.state.markWorktreeSlept.mockClear() mocks.state.shutdownWorktreeBrowsers.mockClear().mockResolvedValue(undefined) mocks.state.shutdownWorktreeTerminals.mockClear().mockImplementation(async (worktreeId) => { for (const tab of mocks.state.tabsByWorktree[worktreeId] ?? []) { mocks.state.ptyIdsByTabId[tab.id] = [] } }) - mocks.state.sleptWorktreeIds = {} mocks.state.tabsByWorktree = { 'wt-parent': [{ id: 'tab-parent' }], 'wt-child-1': [{ id: 'tab-child-1' }], diff --git a/src/renderer/src/components/sidebar/sleep-worktree-flow.test.ts b/src/renderer/src/components/sidebar/sleep-worktree-flow.test.ts index 1b0b81729..ee523e39e 100644 --- a/src/renderer/src/components/sidebar/sleep-worktree-flow.test.ts +++ b/src/renderer/src/components/sidebar/sleep-worktree-flow.test.ts @@ -3,7 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => { const state = { activeWorktreeId: null as string | null, - markWorktreeSlept: vi.fn(), setActiveWorktree: vi.fn(), shutdownWorktreeBrowsers: vi.fn().mockResolvedValue(undefined), shutdownWorktreeTerminals: vi.fn().mockResolvedValue(undefined), @@ -45,7 +44,6 @@ import { runSleepWorktree, runSleepWorktrees } from './sleep-worktree-flow' describe('runSleepWorktree', () => { beforeEach(() => { mocks.state.setActiveWorktree.mockClear() - mocks.state.markWorktreeSlept.mockClear() mocks.state.shutdownWorktreeBrowsers.mockClear().mockResolvedValue(undefined) mocks.state.shutdownWorktreeTerminals.mockClear().mockResolvedValue(undefined) mocks.state.suppressPtyExit.mockClear() @@ -72,7 +70,6 @@ describe('runSleepWorktree', () => { expect(mocks.state.shutdownWorktreeTerminals).toHaveBeenCalledWith('wt-1', { keepIdentifiers: true }) - expect(mocks.state.markWorktreeSlept).toHaveBeenCalledWith('wt-1') const browsersCallOrder = mocks.state.shutdownWorktreeBrowsers.mock.invocationCallOrder[0] const terminalsCallOrder = mocks.state.shutdownWorktreeTerminals.mock.invocationCallOrder[0] expect(browsersCallOrder).toBeLessThan(terminalsCallOrder) @@ -132,7 +129,6 @@ describe('runSleepWorktree', () => { await runSleepWorktree('wt-1') expect(mocks.state.shutdownWorktreeTerminals).not.toHaveBeenCalled() - expect(mocks.state.markWorktreeSlept).not.toHaveBeenCalled() expect(mocks.clearWorktreeSleepIntent).toHaveBeenCalledWith('wt-1') expect(mocks.toastError).toHaveBeenCalledWith( 'Failed to sleep workspace', @@ -153,12 +149,10 @@ describe('runSleepWorktree', () => { expect(mocks.state.shutdownWorktreeTerminals).not.toHaveBeenCalledWith('wt-1', { keepIdentifiers: true }) - expect(mocks.state.markWorktreeSlept).not.toHaveBeenCalledWith('wt-1') expect(mocks.state.shutdownWorktreeBrowsers).toHaveBeenCalledWith('wt-2') expect(mocks.state.shutdownWorktreeTerminals).toHaveBeenCalledWith('wt-2', { keepIdentifiers: true }) - expect(mocks.state.markWorktreeSlept).toHaveBeenCalledWith('wt-2') expect(mocks.toastError).toHaveBeenCalledWith( 'Failed to sleep some workspaces', expect.objectContaining({ description: 'first failed' }) diff --git a/src/renderer/src/components/sidebar/sleep-worktree-flow.ts b/src/renderer/src/components/sidebar/sleep-worktree-flow.ts index 39d55e96f..17df2e4e1 100644 --- a/src/renderer/src/components/sidebar/sleep-worktree-flow.ts +++ b/src/renderer/src/components/sidebar/sleep-worktree-flow.ts @@ -91,7 +91,6 @@ export async function runSleepWorktrees(worktreeIds: readonly string[]): Promise const { activeWorktreeId, setActiveWorktree, - markWorktreeSlept, shutdownWorktreeBrowsers, shutdownWorktreeTerminals } = useAppStore.getState() @@ -130,7 +129,6 @@ export async function runSleepWorktrees(worktreeIds: readonly string[]): Promise // serializer buffers into buffersByLeafId for SSH wake to reseed // scrollback. See DESIGN_DOC_TERMINAL_HISTORY_FIX_V2.md §3.3.c. await shutdownWorktreeTerminals(worktreeId, { keepIdentifiers: true }) - markWorktreeSlept(worktreeId) } catch (err) { errors.push(err instanceof Error ? err.message : String(err)) } diff --git a/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts b/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts index 38eeaff7f..b1402be5a 100644 --- a/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts +++ b/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts @@ -14,9 +14,13 @@ export function useVisibleWorkspaceKanbanWorktreeIds({ }: UseVisibleWorkspaceKanbanWorktreeIdsParams): ReadonlySet { const worktreesByRepo = useAppStore((s) => s.worktreesByRepo) const showSleepingWorkspaces = useAppStore((s) => s.showSleepingWorkspaces) - const sleptWorktreeIds = useAppStore((s) => s.sleptWorktreeIds) const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace) const filterRepoIds = useAppStore((s) => s.filterRepoIds) + const tabsByWorktree = useAppStore((s) => (!showSleepingWorkspaces ? s.tabsByWorktree : null)) + const ptyIdsByTabId = useAppStore((s) => (!showSleepingWorkspaces ? s.ptyIdsByTabId : null)) + const browserTabsByWorktree = useAppStore((s) => + !showSleepingWorkspaces ? s.browserTabsByWorktree : null + ) return useMemo(() => { // Why: the board has its own status ordering, but visibility must match @@ -26,7 +30,9 @@ export function useVisibleWorkspaceKanbanWorktreeIds({ computeVisibleWorktreeIds(worktreesByRepo, sortedIds, { filterRepoIds, showSleepingWorkspaces, - sleptWorktreeIds, + tabsByWorktree, + ptyIdsByTabId, + browserTabsByWorktree, hideDefaultBranchWorkspace, repoMap, // Why: the board has no nested lineage presentation. Ancestor injection @@ -36,11 +42,13 @@ export function useVisibleWorkspaceKanbanWorktreeIds({ ) }, [ allWorktrees, + browserTabsByWorktree, filterRepoIds, hideDefaultBranchWorkspace, + ptyIdsByTabId, repoMap, showSleepingWorkspaces, - sleptWorktreeIds, + tabsByWorktree, worktreesByRepo ]) } diff --git a/src/renderer/src/components/sidebar/visible-worktrees.test.ts b/src/renderer/src/components/sidebar/visible-worktrees.test.ts index 1b3d2be99..baec0eff6 100644 --- a/src/renderer/src/components/sidebar/visible-worktrees.test.ts +++ b/src/renderer/src/components/sidebar/visible-worktrees.test.ts @@ -6,7 +6,20 @@ import { isDefaultBranchWorkspace, sidebarHasActiveFilters } from './visible-worktrees' -import type { Repo, Worktree, WorktreeLineage } from '../../../../shared/types' +import type { Repo, TerminalTab, Worktree, WorktreeLineage } from '../../../../shared/types' + +function makeTab(id: string, worktreeId: string, ptyId: string | null): TerminalTab { + return { + id, + ptyId, + worktreeId, + title: id, + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 0 + } +} function makeWorktree(id: string, repoId = 'repo1'): Worktree & { instanceId: string } { return { @@ -63,7 +76,9 @@ function visibleOptions(overrides: Partial = {}): VisibleOptions return { filterRepoIds: [], showSleepingWorkspaces: true, - sleptWorktreeIds: {}, + tabsByWorktree: {}, + ptyIdsByTabId: {}, + browserTabsByWorktree: {}, hideDefaultBranchWorkspace: false, repoMap, worktreeLineageById: {}, @@ -83,14 +98,15 @@ function filterState(overrides: Partial = {}): FilterState { } describe('computeVisibleWorktreeIds', () => { - it('keeps unslept worktrees visible when sleeping workspaces are hidden', () => { - const wt = makeWorktree('wt-unslept') + it('keeps browser-tab worktrees visible when sleeping workspaces are hidden', () => { + const wt = makeWorktree('wt-browser') const result = computeVisibleWorktreeIds( { repo1: [wt] }, [wt.id], visibleOptions({ - showSleepingWorkspaces: false + showSleepingWorkspaces: false, + browserTabsByWorktree: { [wt.id]: [{ id: 'browser-1' }] } }) ) @@ -104,29 +120,14 @@ describe('computeVisibleWorktreeIds', () => { { repo1: [wt] }, [wt.id], visibleOptions({ - showSleepingWorkspaces: false, - sleptWorktreeIds: { [wt.id]: true } + showSleepingWorkspaces: false }) ) expect(result).toEqual([]) }) - it('does not hide unslept inactive worktrees when sleeping workspaces are hidden', () => { - const wt = makeWorktree('wt-unslept') - - const result = computeVisibleWorktreeIds( - { repo1: [wt] }, - [wt.id], - visibleOptions({ - showSleepingWorkspaces: false - }) - ) - - expect(result).toEqual([wt.id]) - }) - - it('hides explicitly slept inactive worktrees when sleeping workspaces are hidden', () => { + it('does not treat slept wake-hint tabs as live surfaces', () => { const wt = makeWorktree('wt-slept') const result = computeVisibleWorktreeIds( @@ -134,13 +135,47 @@ describe('computeVisibleWorktreeIds', () => { [wt.id], visibleOptions({ showSleepingWorkspaces: false, - sleptWorktreeIds: { [wt.id]: true } + tabsByWorktree: { [wt.id]: [makeTab('tab-slept', wt.id, 'wake-hint-session')] }, + // Sleep preserves tab.ptyId as the wake hint but clears live PTY ids. + ptyIdsByTabId: { 'tab-slept': [] } }) ) expect(result).toEqual([]) }) + it('hides paired web host terminal mirrors while their stream handle is pending', () => { + const wt = makeWorktree('wt-web-pending') + + const result = computeVisibleWorktreeIds( + { repo1: [wt] }, + [wt.id], + visibleOptions({ + showSleepingWorkspaces: false, + tabsByWorktree: { [wt.id]: [makeTab('web-terminal-host-tab-1', wt.id, null)] }, + ptyIdsByTabId: {} + }) + ) + + expect(result).toEqual([]) + }) + + it('keeps paired web host terminal mirrors visible after their stream handle is ready', () => { + const wt = makeWorktree('wt-web-ready') + + const result = computeVisibleWorktreeIds( + { repo1: [wt] }, + [wt.id], + visibleOptions({ + showSleepingWorkspaces: false, + tabsByWorktree: { [wt.id]: [makeTab('web-terminal-host-tab-1', wt.id, null)] }, + ptyIdsByTabId: { 'web-terminal-host-tab-1': ['pty-web-ready'] } + }) + ) + + expect(result).toEqual([wt.id]) + }) + it('hides branch-backed main worktrees when default branch workspaces are hidden', () => { const main = makeWorktree('main') const feature = makeWorktree('feature') @@ -188,19 +223,21 @@ describe('computeVisibleWorktreeIds', () => { expect(result).toEqual([feature1.id, feature2.id]) }) - it('composes with sleeping visibility: hidden mains stay hidden while unslept features remain', () => { + it('composes with sleeping visibility: hidden mains stay hidden while live features remain', () => { const main = makeWorktree('main') main.isMainWorktree = true const feature = makeWorktree('feature') // Why: verifies filter ordering — the default-branch hide runs before // sleeping visibility, so the hidden main does not slip back in while the - // feature survives because it was not explicitly slept. + // feature survives because it has a live PTY. const result = computeVisibleWorktreeIds( { repo1: [main, feature] }, [main.id, feature.id], visibleOptions({ showSleepingWorkspaces: false, + tabsByWorktree: { [feature.id]: [makeTab('t1', feature.id, 'p1')] }, + ptyIdsByTabId: { t1: ['p1'] }, hideDefaultBranchWorkspace: true }) ) @@ -243,7 +280,8 @@ describe('computeVisibleWorktreeIds', () => { [child.id, parent.id], visibleOptions({ showSleepingWorkspaces: false, - sleptWorktreeIds: { [parent.id]: true }, + tabsByWorktree: { [child.id]: [makeTab('t-child', child.id, 'p-child')] }, + ptyIdsByTabId: { 't-child': ['p-child'] }, worktreeLineageById: { [child.id]: lineage } }) ) @@ -263,7 +301,8 @@ describe('computeVisibleWorktreeIds', () => { [child.id, parent.id], visibleOptions({ showSleepingWorkspaces: false, - sleptWorktreeIds: { [parent.id]: true }, + tabsByWorktree: { [child.id]: [makeTab('t-child', child.id, 'p-child')] }, + ptyIdsByTabId: { 't-child': ['p-child'] }, worktreeLineageById: { [child.id]: lineage } }) ) @@ -282,7 +321,8 @@ describe('computeVisibleWorktreeIds', () => { [child.id, parent.id], visibleOptions({ showSleepingWorkspaces: false, - sleptWorktreeIds: { [parent.id]: true }, + tabsByWorktree: { [child.id]: [makeTab('t-child', child.id, 'p-child')] }, + ptyIdsByTabId: { 't-child': ['p-child'] }, worktreeLineageById: { [child.id]: lineage } }) ) diff --git a/src/renderer/src/components/sidebar/visible-worktrees.ts b/src/renderer/src/components/sidebar/visible-worktrees.ts index 8e68fcb39..a4ce1d055 100644 --- a/src/renderer/src/components/sidebar/visible-worktrees.ts +++ b/src/renderer/src/components/sidebar/visible-worktrees.ts @@ -1,6 +1,6 @@ -import type { Worktree, Repo, WorktreeLineage } from '../../../../shared/types' +import type { Worktree, Repo, TerminalTab, WorktreeLineage } from '../../../../shared/types' import { buildWorktreeComparator, sortWorktreesSmart } from './smart-sort' -import { isSleptWorkspace } from '@/lib/worktree-activity-state' +import { isInactiveWorkspace } from '@/lib/worktree-activity-state' import { useAppStore } from '@/store' import { getAllWorktreesFromState, getRepoMapFromState } from '@/store/selectors' import { DEFAULT_SHOW_SLEEPING_WORKSPACES } from '../../../../shared/constants' @@ -84,7 +84,9 @@ export function computeVisibleWorktreeIds( opts: { filterRepoIds: string[] showSleepingWorkspaces: boolean - sleptWorktreeIds: Record + tabsByWorktree: Record[]> | null + ptyIdsByTabId: Record | null + browserTabsByWorktree?: Record | null // Why required: every caller (WorktreeList, getVisibleWorktreeIds // fallback, tests) reads the flag from the UI store. Making the field // required prevents a future caller from silently dropping the filter by @@ -114,7 +116,15 @@ export function computeVisibleWorktreeIds( } if (!opts.showSleepingWorkspaces) { - all = all.filter((w) => !isSleptWorkspace(w.id, opts.sleptWorktreeIds)) + all = all.filter( + (w) => + !isInactiveWorkspace( + w.id, + opts.tabsByWorktree, + opts.ptyIdsByTabId, + opts.browserTabsByWorktree + ) + ) } // Apply cached sort order. Items not yet in the cache (e.g. brand-new @@ -243,7 +253,9 @@ export function getVisibleWorktreeIds(): string[] { return computeVisibleWorktreeIds(state.worktreesByRepo, sortedIds, { filterRepoIds: state.filterRepoIds, showSleepingWorkspaces: state.showSleepingWorkspaces, - sleptWorktreeIds: state.sleptWorktreeIds, + tabsByWorktree: state.tabsByWorktree, + ptyIdsByTabId: state.ptyIdsByTabId, + browserTabsByWorktree: state.browserTabsByWorktree, hideDefaultBranchWorkspace: state.hideDefaultBranchWorkspace, repoMap, worktreeLineageById: state.worktreeLineageById diff --git a/src/renderer/src/hooks/useIpcEvents.test.ts b/src/renderer/src/hooks/useIpcEvents.test.ts index 213440c2c..fd0b8cbbb 100644 --- a/src/renderer/src/hooks/useIpcEvents.test.ts +++ b/src/renderer/src/hooks/useIpcEvents.test.ts @@ -220,7 +220,6 @@ describe('useIpcEvents browser tab create routing', () => { setUpdateStatus: vi.fn(), fetchRepos: vi.fn(), fetchWorktrees: vi.fn(), - fetchWorktreeLineage: vi.fn(), setActiveView: vi.fn(), activeModal: null, closeModal: vi.fn(), @@ -689,7 +688,6 @@ describe('useIpcEvents updater integration', () => { setUpdateStatus: vi.fn(), fetchRepos: vi.fn(), fetchWorktrees: vi.fn(), - fetchWorktreeLineage: vi.fn(), setActiveView: vi.fn(), activeModal: null, closeModal: vi.fn(), @@ -2552,7 +2550,6 @@ describe('useIpcEvents agent status snapshot integration', () => { setUpdateStatus: vi.fn(), fetchRepos: vi.fn(), fetchWorktrees: vi.fn(), - fetchWorktreeLineage: vi.fn(), setActiveView: vi.fn(), activeModal: null, closeModal: vi.fn(), @@ -3960,145 +3957,6 @@ describe('useIpcEvents agent status snapshot integration', () => { expect(hydrateBrowserSession).not.toHaveBeenCalled() }) - it('merges remote slept and default-terminal markers for the changed target only', async () => { - const hydrateWorkspaceSession = vi.fn() - const onChangedListenerRef: { - current: - | ((event: { - targetId: string - sourceClientId?: string - snapshot: Record - }) => void) - | null - } = { current: null } - const localWorktreeId = 'repo-local::/local' - const remoteWorktreeId = 'repo-remote::/repo' - const setRemoteWorkspaceSyncStatus = vi.fn() - const storeState: StoreLike = buildStoreState({ - workspaceSessionReady: true, - activeRepoId: 'repo-local', - activeWorktreeId: localWorktreeId, - activeTabId: 'tab-local', - repos: [ - { id: 'repo-local', connectionId: null }, - { id: 'repo-remote', connectionId: 'conn-1' } - ], - worktreesByRepo: { - 'repo-local': [{ id: localWorktreeId, repoId: 'repo-local' }], - 'repo-remote': [{ id: remoteWorktreeId, repoId: 'repo-remote' }] - }, - tabsByWorktree: { - [localWorktreeId]: [ - { id: 'tab-local', ptyId: null, worktreeId: localWorktreeId, title: 'Local' } - ] - }, - ptyIdsByTabId: {}, - terminalLayoutsByTabId: {}, - activeTabIdByWorktree: { [localWorktreeId]: 'tab-local' }, - openFiles: [], - activeFileIdByWorktree: {}, - activeTabTypeByWorktree: {}, - browserTabsByWorktree: {}, - browserPagesByWorkspace: {}, - activeBrowserTabIdByWorktree: {}, - browserUrlHistory: [], - unifiedTabsByWorktree: {}, - groupsByWorktree: {}, - layoutByWorktree: {}, - activeGroupIdByWorktree: {}, - sshConnectionStates: new Map(), - lastKnownRelayPtyIdByTabId: {}, - lastVisitedAtByWorktreeId: {}, - defaultTerminalTabsAppliedByWorktreeId: { [localWorktreeId]: true }, - sleptWorktreeIds: { [localWorktreeId]: true }, - hydrateWorkspaceSession, - hydrateTabsSession: vi.fn(), - hydrateEditorSession: vi.fn(), - hydrateBrowserSession: vi.fn(), - markRemoteWorkspaceHydrated: vi.fn(), - setRemoteWorkspaceSyncStatus, - reconnectPersistedTerminals: vi.fn(() => Promise.resolve()) - }) - - stubReactSyncEffect() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - stubAuxiliaryModules() - vi.stubGlobal( - 'window', - buildWindowApi({ - onSet: () => () => {}, - remoteWorkspace: { - clientId: () => Promise.resolve('client-self'), - onChanged: (cb: typeof onChangedListenerRef.current) => { - onChangedListenerRef.current = cb - return () => {} - } - } - }) - ) - - const { useIpcEvents } = await import('./useIpcEvents') - - useIpcEvents() - await Promise.resolve() - - onChangedListenerRef.current?.({ - targetId: 'conn-1', - sourceClientId: 'client-other', - snapshot: { - revision: 7, - updatedAt: Date.now(), - session: { - activeWorktreePath: '/repo', - activeTabId: 'tab-remote', - tabsByWorktreePath: { - '/repo': [ - { - id: 'tab-remote', - ptyId: null, - worktreePath: '/repo', - title: 'Remote', - customTitle: null, - color: null, - sortOrder: 1, - createdAt: 1 - } - ] - }, - terminalLayoutsByTabId: {}, - defaultTerminalTabsAppliedByWorktreePath: { '/repo': true }, - sleptWorktreePaths: { '/repo': true } - } - } - }) - for ( - let attempt = 0; - attempt < 10 && hydrateWorkspaceSession.mock.calls.length === 0; - attempt += 1 - ) { - await new Promise((resolve) => setTimeout(resolve, 0)) - } - - expect(hydrateWorkspaceSession).toHaveBeenCalledTimes(1) - expect(hydrateWorkspaceSession).toHaveBeenCalledWith( - expect.objectContaining({ - defaultTerminalTabsAppliedByWorktreeId: { - [localWorktreeId]: true, - [remoteWorktreeId]: true - }, - sleptWorktreeIds: { - [localWorktreeId]: true, - [remoteWorktreeId]: true - } - }) - ) - }) - it('silently discards snapshot entries whose tabs are still unknown', async () => { const setAgentStatus = vi.fn() const getSnapshot = vi.fn(() => diff --git a/src/renderer/src/hooks/useIpcEvents.ts b/src/renderer/src/hooks/useIpcEvents.ts index 2e76b4c49..fbd6147c1 100644 --- a/src/renderer/src/hooks/useIpcEvents.ts +++ b/src/renderer/src/hooks/useIpcEvents.ts @@ -446,14 +446,6 @@ function mergeRemoteWorkspaceSession( lastVisitedAtByWorktreeId: { ...omitTargetWorktrees(current.lastVisitedAtByWorktreeId), ...remote.lastVisitedAtByWorktreeId - }, - defaultTerminalTabsAppliedByWorktreeId: { - ...omitTargetWorktrees(current.defaultTerminalTabsAppliedByWorktreeId), - ...remote.defaultTerminalTabsAppliedByWorktreeId - }, - sleptWorktreeIds: { - ...omitTargetWorktrees(current.sleptWorktreeIds), - ...remote.sleptWorktreeIds } } } diff --git a/src/renderer/src/lib/sidebar-worktree-activation.test.ts b/src/renderer/src/lib/sidebar-worktree-activation.test.ts index e9185d32c..72859af80 100644 --- a/src/renderer/src/lib/sidebar-worktree-activation.test.ts +++ b/src/renderer/src/lib/sidebar-worktree-activation.test.ts @@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => { const state = { - sleptWorktreeIds: {} as Record, tabsByWorktree: {} as Record, ptyIdsByTabId: {} as Record, browserTabsByWorktree: {} as Record, @@ -63,7 +62,6 @@ describe('sidebar worktree activation', () => { mocks.scheduleAfterInputQuiet.mockClear() mocks.pendingCallbacks.length = 0 mocks.pendingCancels.length = 0 - mocks.state.sleptWorktreeIds = {} mocks.state.tabsByWorktree = {} mocks.state.ptyIdsByTabId = {} mocks.state.browserTabsByWorktree = {} @@ -71,7 +69,8 @@ describe('sidebar worktree activation', () => { }) it('cancels a queued slept-workspace activation', () => { - mocks.state.sleptWorktreeIds = { 'wt-parent': true } + mocks.state.tabsByWorktree = { 'wt-parent': [{ id: 'tab-1' }] } + mocks.state.ptyIdsByTabId = { 'tab-1': [] } activateWorktreeFromSidebar('wt-parent') cancelPendingSidebarWorktreeActivation() @@ -81,13 +80,13 @@ describe('sidebar worktree activation', () => { expect(mocks.activateAndRevealWorktree).not.toHaveBeenCalled() }) - it('does not defer an unslept workspace even when it has no live PTY', () => { - mocks.state.tabsByWorktree = { 'wt-unslept': [{ id: 'tab-1' }] } - mocks.state.ptyIdsByTabId = { 'tab-1': [] } + it('does not defer a workspace with a live PTY', () => { + mocks.state.tabsByWorktree = { 'wt-live': [{ id: 'tab-1' }] } + mocks.state.ptyIdsByTabId = { 'tab-1': ['pty-1'] } - activateWorktreeFromSidebar('wt-unslept') + activateWorktreeFromSidebar('wt-live') expect(mocks.scheduleAfterInputQuiet).not.toHaveBeenCalled() - expect(mocks.activateAndRevealWorktree).toHaveBeenCalledWith('wt-unslept') + expect(mocks.activateAndRevealWorktree).toHaveBeenCalledWith('wt-live') }) }) diff --git a/src/renderer/src/lib/sidebar-worktree-activation.ts b/src/renderer/src/lib/sidebar-worktree-activation.ts index 2401413ab..7181749d2 100644 --- a/src/renderer/src/lib/sidebar-worktree-activation.ts +++ b/src/renderer/src/lib/sidebar-worktree-activation.ts @@ -1,5 +1,6 @@ import { useAppStore } from '@/store' import { activateAndRevealWorktree } from '@/lib/worktree-activation' +import { tabHasLivePty } from '@/lib/tab-has-live-pty' import { markInputQuietSchedulerInput, scheduleAfterInputQuiet } from '@/lib/input-quiet-scheduler' const SLEPT_WORKTREE_ACTIVATION_INPUT_QUIET_MS = 450 @@ -17,7 +18,17 @@ export function cancelPendingSidebarWorktreeActivation(): void { function shouldDeferSidebarWorktreeActivation(worktreeId: string): boolean { const state = useAppStore.getState() - return Boolean(state.sleptWorktreeIds[worktreeId]) + const tabs = state.tabsByWorktree[worktreeId] ?? [] + if (tabs.length === 0) { + return false + } + if ((state.browserTabsByWorktree[worktreeId] ?? []).length > 0) { + return false + } + if (state.openFiles.some((file) => file.worktreeId === worktreeId)) { + return false + } + return tabs.every((tab) => !tabHasLivePty(state.ptyIdsByTabId, tab.id)) } export function activateWorktreeFromSidebar(worktreeId: string): void { diff --git a/src/renderer/src/lib/workspace-session-browser-history.test.ts b/src/renderer/src/lib/workspace-session-browser-history.test.ts index 060d0c82d..47f3e12b7 100644 --- a/src/renderer/src/lib/workspace-session-browser-history.test.ts +++ b/src/renderer/src/lib/workspace-session-browser-history.test.ts @@ -29,8 +29,7 @@ function createSnapshot(browserUrlHistory: BrowserHistoryEntry[]): WorkspaceSess worktreesByRepo: {}, lastKnownRelayPtyIdByTabId: {}, lastVisitedAtByWorktreeId: {}, - defaultTerminalTabsAppliedByWorktreeId: {}, - sleptWorktreeIds: {} + defaultTerminalTabsAppliedByWorktreeId: {} } } diff --git a/src/renderer/src/lib/workspace-session-liveness.test.ts b/src/renderer/src/lib/workspace-session-liveness.test.ts index 2de2f6469..160b7ee27 100644 --- a/src/renderer/src/lib/workspace-session-liveness.test.ts +++ b/src/renderer/src/lib/workspace-session-liveness.test.ts @@ -30,7 +30,6 @@ function createSnapshot( lastKnownRelayPtyIdByTabId: {}, lastVisitedAtByWorktreeId: {}, defaultTerminalTabsAppliedByWorktreeId: {}, - sleptWorktreeIds: {}, ...overrides } } diff --git a/src/renderer/src/lib/workspace-session-patch.ts b/src/renderer/src/lib/workspace-session-patch.ts index d9fd42025..667a92d64 100644 --- a/src/renderer/src/lib/workspace-session-patch.ts +++ b/src/renderer/src/lib/workspace-session-patch.ts @@ -8,7 +8,6 @@ import { buildPersistedBrowserPagesByWorkspace, buildPersistedBrowserTabsByWorktree, buildSanitizedTabsByWorktree, - buildSleptWorktreeIds, buildTerminalSessionData, type WorkspaceSessionSnapshot } from './workspace-session' @@ -132,9 +131,6 @@ export function buildWorkspaceSessionPatch( ? snapshot.defaultTerminalTabsAppliedByWorktreeId : undefined } - if (changed.has('sleptWorktreeIds')) { - patch.sleptWorktreeIds = buildSleptWorktreeIds(snapshot) - } return patch } diff --git a/src/renderer/src/lib/workspace-session-relevant-fields.test.ts b/src/renderer/src/lib/workspace-session-relevant-fields.test.ts index ed793ba41..c15b09c91 100644 --- a/src/renderer/src/lib/workspace-session-relevant-fields.test.ts +++ b/src/renderer/src/lib/workspace-session-relevant-fields.test.ts @@ -29,8 +29,7 @@ describe('SESSION_RELEVANT_FIELDS', () => { worktreesByRepo: true, lastKnownRelayPtyIdByTabId: true, lastVisitedAtByWorktreeId: true, - defaultTerminalTabsAppliedByWorktreeId: true, - sleptWorktreeIds: true + defaultTerminalTabsAppliedByWorktreeId: true } it('contains every key of WorkspaceSessionSnapshot', () => { diff --git a/src/renderer/src/lib/workspace-session.ts b/src/renderer/src/lib/workspace-session.ts index 73bb31d21..c03a02461 100644 --- a/src/renderer/src/lib/workspace-session.ts +++ b/src/renderer/src/lib/workspace-session.ts @@ -54,8 +54,7 @@ export type WorkspaceSessionSnapshot = Pick< | 'lastKnownRelayPtyIdByTabId' | 'lastVisitedAtByWorktreeId' | 'defaultTerminalTabsAppliedByWorktreeId' -> & - Partial> +> // Why: the App-level Zustand subscriber that debounces session writes uses // this list as a shallow-equality gate so it only resets the timer when a @@ -88,8 +87,7 @@ export const SESSION_RELEVANT_FIELDS = [ 'worktreesByRepo', 'lastKnownRelayPtyIdByTabId', 'lastVisitedAtByWorktreeId', - 'defaultTerminalTabsAppliedByWorktreeId', - 'sleptWorktreeIds' + 'defaultTerminalTabsAppliedByWorktreeId' ] as const satisfies readonly (keyof WorkspaceSessionSnapshot)[] type _MissingSessionField = Exclude< @@ -318,14 +316,6 @@ export function buildLastVisitedAtByWorktreeId( : undefined } -export function buildSleptWorktreeIds( - snapshot: WorkspaceSessionSnapshot -): WorkspaceSessionState['sleptWorktreeIds'] { - return snapshot.sleptWorktreeIds && Object.keys(snapshot.sleptWorktreeIds).length > 0 - ? snapshot.sleptWorktreeIds - : undefined -} - export function buildWorkspaceSessionPayload( snapshot: WorkspaceSessionSnapshot ): WorkspaceSessionState { @@ -372,8 +362,7 @@ export function buildWorkspaceSessionPayload( snapshot.defaultTerminalTabsAppliedByWorktreeId && Object.keys(snapshot.defaultTerminalTabsAppliedByWorktreeId).length > 0 ? snapshot.defaultTerminalTabsAppliedByWorktreeId - : undefined, - sleptWorktreeIds: buildSleptWorktreeIds(snapshot) + : undefined } return pruneLocalTerminalScrollbackBuffers(payload, snapshot.repos) diff --git a/src/renderer/src/lib/worktree-activity-state.ts b/src/renderer/src/lib/worktree-activity-state.ts index 1c9a08f2e..fbd39c32e 100644 --- a/src/renderer/src/lib/worktree-activity-state.ts +++ b/src/renderer/src/lib/worktree-activity-state.ts @@ -7,14 +7,6 @@ type BrowserLikeTab = { id: string } type TabsByWorktree = Record type PtyIdsByTabId = Record type BrowserTabsByWorktree = Record -type SleptWorktreeIds = Record - -export function isSleptWorkspace( - worktreeId: string, - sleptWorktreeIds: SleptWorktreeIds | null | undefined -): boolean { - return Boolean(sleptWorktreeIds?.[worktreeId]) -} export function hasActiveWorkspaceActivity( worktreeId: string, diff --git a/src/renderer/src/store/slices/browser.test.ts b/src/renderer/src/store/slices/browser.test.ts index 91cc858cf..989a0d9b6 100644 --- a/src/renderer/src/store/slices/browser.test.ts +++ b/src/renderer/src/store/slices/browser.test.ts @@ -41,7 +41,6 @@ function createTestStore() { unifiedTabsByWorktree: {}, tabBarOrderByWorktree: {}, tabsByWorktree: {}, - sleptWorktreeIds: {}, openFiles: [], activeTabType: 'terminal', activeTabTypeByWorktree: {}, @@ -174,15 +173,6 @@ describe('createBrowserSlice annotations', () => { expect(store.getState().activeBrowserTabIdByWorktree['wt-1']).toBeNull() }) - it('clears an explicit slept marker when opening a browser tab', () => { - const store = createTestStore() - store.setState({ sleptWorktreeIds: { 'wt-1': true } }) - - store.getState().createBrowserTab('wt-1', 'https://example.com') - - expect(store.getState().sleptWorktreeIds['wt-1']).toBeUndefined() - }) - it('preserves browser map references when a page-state update is unchanged', () => { const store = createTestStore() const tab = store.getState().createBrowserTab('wt-1', 'https://example.com', { diff --git a/src/renderer/src/store/slices/browser.ts b/src/renderer/src/store/slices/browser.ts index 6a0a1b003..b4102a68e 100644 --- a/src/renderer/src/store/slices/browser.ts +++ b/src/renderer/src/store/slices/browser.ts @@ -438,13 +438,6 @@ export const createBrowserSlice: StateCreator = set((s) => { const existingTabs = s.browserTabsByWorktree[worktreeId] ?? [] - const nextSleptWorktreeIds = s.sleptWorktreeIds[worktreeId] - ? (() => { - const next = { ...s.sleptWorktreeIds } - delete next[worktreeId] - return next - })() - : s.sleptWorktreeIds const nextTabBarOrder = (() => { const currentOrder = s.tabBarOrderByWorktree[worktreeId] ?? [] const terminalIds = (s.tabsByWorktree[worktreeId] ?? []).map((tab) => tab.id) @@ -509,10 +502,7 @@ export const createBrowserSlice: StateCreator = [workspaceId]: true, [page.id]: true } - : s.pendingAddressBarFocusByTabId, - ...(nextSleptWorktreeIds !== s.sleptWorktreeIds - ? { sleptWorktreeIds: nextSleptWorktreeIds } - : {}) + : s.pendingAddressBarFocusByTabId } }) diff --git a/src/renderer/src/store/slices/repos.ts b/src/renderer/src/store/slices/repos.ts index 88f85882d..39c227168 100644 --- a/src/renderer/src/store/slices/repos.ts +++ b/src/renderer/src/store/slices/repos.ts @@ -601,7 +601,6 @@ export const createRepoSlice: StateCreator = (set, // Drop this repo's timestamps explicitly so they cannot survive prune // forever after the repo is removed. let nextLastVisitedAtByWorktreeId = s.lastVisitedAtByWorktreeId - let nextSleptWorktreeIds = s.sleptWorktreeIds for (const id of Object.keys(s.lastVisitedAtByWorktreeId)) { if (getRepoIdFromWorktreeId(id) === projectId) { if (nextLastVisitedAtByWorktreeId === s.lastVisitedAtByWorktreeId) { @@ -610,14 +609,6 @@ export const createRepoSlice: StateCreator = (set, delete nextLastVisitedAtByWorktreeId[id] } } - for (const id of Object.keys(s.sleptWorktreeIds)) { - if (getRepoIdFromWorktreeId(id) === projectId) { - if (nextSleptWorktreeIds === s.sleptWorktreeIds) { - nextSleptWorktreeIds = { ...s.sleptWorktreeIds } - } - delete nextSleptWorktreeIds[id] - } - } const nextRepos = s.repos.filter((r) => r.id !== projectId) return { repos: nextRepos, @@ -637,7 +628,6 @@ export const createRepoSlice: StateCreator = (set, activeFileId: activeFileCleared ? null : s.activeFileId, activeTabType: activeFileCleared ? 'terminal' : s.activeTabType, lastVisitedAtByWorktreeId: nextLastVisitedAtByWorktreeId, - sleptWorktreeIds: nextSleptWorktreeIds, sortEpoch: s.sortEpoch + 1, // Why: removing the last repo while in settings leaves activeView as // 'settings', which renders an empty settings pane instead of Landing. diff --git a/src/renderer/src/store/slices/settings.ts b/src/renderer/src/store/slices/settings.ts index cc7a0ff55..d7bd1ddf6 100644 --- a/src/renderer/src/store/slices/settings.ts +++ b/src/renderer/src/store/slices/settings.ts @@ -52,7 +52,6 @@ function runtimeScopedStateReset(): Partial { sortEpoch: 0, everActivatedWorktreeIds: new Set(), lastVisitedAtByWorktreeId: {}, - sleptWorktreeIds: {}, hasHydratedWorktreePurge: false, unifiedTabsByWorktree: {}, groupsByWorktree: {}, diff --git a/src/renderer/src/store/slices/tabs.test.ts b/src/renderer/src/store/slices/tabs.test.ts index 99c25475c..68a9223bc 100644 --- a/src/renderer/src/store/slices/tabs.test.ts +++ b/src/renderer/src/store/slices/tabs.test.ts @@ -205,23 +205,6 @@ describe('TabsSlice', () => { } }) }) - - it('clears an explicit slept marker when opening a terminal tab', () => { - store.getState().markWorktreeSlept(WT) - - store.getState().createTab(WT) - - expect(store.getState().sleptWorktreeIds[WT]).toBeUndefined() - }) - - it('clears an explicit slept marker when a terminal PTY becomes live', () => { - const tab = store.getState().createTab(WT) - store.getState().markWorktreeSlept(WT) - - store.getState().updateTabPtyId(tab.id, 'pty-live') - - expect(store.getState().sleptWorktreeIds[WT]).toBeUndefined() - }) }) // ─── closeUnifiedTab ──────────────────────────────────────────────── diff --git a/src/renderer/src/store/slices/terminals-hydration.test.ts b/src/renderer/src/store/slices/terminals-hydration.test.ts index 5b1a26e8c..7ba26f57a 100644 --- a/src/renderer/src/store/slices/terminals-hydration.test.ts +++ b/src/renderer/src/store/slices/terminals-hydration.test.ts @@ -227,104 +227,6 @@ describe('hydrateWorkspaceSession', () => { }) }) - it('hydrates only explicit slept markers that are not awaiting reconnect', () => { - const store = createTestStore() - const sleptWorktreeId = 'repo1::/slept' - const reconnectWorktreeId = 'repo1::/reconnect' - seedStore(store, { - worktreesByRepo: { - repo1: [ - makeWorktree({ id: sleptWorktreeId, repoId: 'repo1', path: '/slept' }), - makeWorktree({ id: reconnectWorktreeId, repoId: 'repo1', path: '/reconnect' }) - ] - } - }) - - const session: WorkspaceSessionState = { - activeRepoId: 'repo1', - activeWorktreeId: sleptWorktreeId, - activeTabId: null, - activeWorktreeIdsOnShutdown: [reconnectWorktreeId], - terminalLayoutsByTabId: {}, - tabsByWorktree: { - [reconnectWorktreeId]: [ - makeTab({ id: 'tab-reconnect', worktreeId: reconnectWorktreeId, ptyId: 'pty-1' }) - ] - }, - sleptWorktreeIds: { - [sleptWorktreeId]: true, - [reconnectWorktreeId]: true, - 'repo1::/missing': true - } - } - - store.getState().hydrateWorkspaceSession(session) - - expect(store.getState().sleptWorktreeIds).toEqual({ [sleptWorktreeId]: true }) - expect(store.getState().pendingReconnectWorktreeIds).toEqual([reconnectWorktreeId]) - }) - - it('does not reconnect explicitly slept wake-hint tabs when shutdown list is missing', () => { - const store = createTestStore() - const sleptWorktreeId = 'repo1::/slept' - seedStore(store, { - worktreesByRepo: { - repo1: [makeWorktree({ id: sleptWorktreeId, repoId: 'repo1', path: '/slept' })] - } - }) - - const session: WorkspaceSessionState = { - activeRepoId: 'repo1', - activeWorktreeId: null, - activeTabId: null, - terminalLayoutsByTabId: {}, - tabsByWorktree: { - [sleptWorktreeId]: [ - makeTab({ id: 'tab-slept', worktreeId: sleptWorktreeId, ptyId: 'wake-hint' }) - ] - }, - sleptWorktreeIds: { - [sleptWorktreeId]: true - } - } - - store.getState().hydrateWorkspaceSession(session) - - expect(store.getState().sleptWorktreeIds).toEqual({ [sleptWorktreeId]: true }) - expect(store.getState().pendingReconnectWorktreeIds).toEqual([]) - }) - - it('does not reconnect explicitly slept wake-hint tabs when shutdown list is empty', () => { - const store = createTestStore() - const sleptWorktreeId = 'repo1::/slept' - seedStore(store, { - worktreesByRepo: { - repo1: [makeWorktree({ id: sleptWorktreeId, repoId: 'repo1', path: '/slept' })] - } - }) - - const session: WorkspaceSessionState = { - activeRepoId: 'repo1', - activeWorktreeId: null, - activeTabId: null, - activeWorktreeIdsOnShutdown: [], - terminalLayoutsByTabId: {}, - tabsByWorktree: { - [sleptWorktreeId]: [ - makeTab({ id: 'tab-slept', worktreeId: sleptWorktreeId, ptyId: 'wake-hint' }) - ] - }, - sleptWorktreeIds: { - [sleptWorktreeId]: true - } - } - - store.getState().hydrateWorkspaceSession(session) - - expect(store.getState().sleptWorktreeIds).toEqual({ [sleptWorktreeId]: true }) - expect(store.getState().pendingReconnectWorktreeIds).toEqual([]) - }) - it('seeds worktree nav history with the restored active worktree', () => { // Why: without seeding, the first sidebar click after startup becomes the // only history entry, so Back stays disabled until the user clicks a diff --git a/src/renderer/src/store/slices/terminals.ts b/src/renderer/src/store/slices/terminals.ts index 925e879e7..64e2f29ed 100644 --- a/src/renderer/src/store/slices/terminals.ts +++ b/src/renderer/src/store/slices/terminals.ts @@ -261,9 +261,6 @@ export type TerminalSlice = { workspaceSessionReady: boolean defaultTerminalTabsAppliedByWorktreeId: Record markDefaultTerminalTabsApplied: (worktreeId: string) => void - sleptWorktreeIds: Record - markWorktreeSlept: (worktreeId: string) => void - clearWorktreeSlept: (worktreeId: string) => void /** True only after hydrateWorkspaceSession ran from a real load of * orca-data.json. Guards the debounced session writer so that a crash * during early startup (fetchRepos / fetchAllWorktrees / session.get / @@ -448,28 +445,6 @@ export const createTerminalSlice: StateCreator } } }), - sleptWorktreeIds: {}, - markWorktreeSlept: (worktreeId) => - set((s) => { - if (s.sleptWorktreeIds[worktreeId]) { - return {} - } - return { - sleptWorktreeIds: { - ...s.sleptWorktreeIds, - [worktreeId]: true - } - } - }), - clearWorktreeSlept: (worktreeId) => - set((s) => { - if (!s.sleptWorktreeIds[worktreeId]) { - return {} - } - const next = { ...s.sleptWorktreeIds } - delete next[worktreeId] - return { sleptWorktreeIds: next } - }), hydrationSucceeded: false, setHydrationSucceeded: (value) => { set({ hydrationSucceeded: value }) @@ -570,13 +545,6 @@ export const createTerminalSlice: StateCreator const existing = (s.tabsByWorktree[worktreeId] ?? []).filter( (entry) => !orphanTerminalIds.has(entry.id) ) - const nextSleptWorktreeIds = s.sleptWorktreeIds[worktreeId] - ? (() => { - const next = { ...s.sleptWorktreeIds } - delete next[worktreeId] - return next - })() - : s.sleptWorktreeIds // Why: caller-supplied id (e.g. main pre-allocates the tabId for CLI // background terminals so the paneKey env baked into the PTY matches // the renderer's tab id). Fall back to minting if the id collides — a @@ -723,10 +691,7 @@ export const createTerminalSlice: StateCreator terminalLayoutsByTabId: { ...s.terminalLayoutsByTabId, [tab.id]: emptyLayoutSnapshot() - }, - ...(nextSleptWorktreeIds !== s.sleptWorktreeIds - ? { sleptWorktreeIds: nextSleptWorktreeIds } - : {}) + } } }) const shouldRecordInteraction = @@ -1384,14 +1349,6 @@ export const createTerminalSlice: StateCreator const isFirstPty = existingPtyIds.length === 0 const isActiveWorktree = worktreeId != null && s.activeWorktreeId === worktreeId const shouldBumpSortEpoch = isFirstPty && isActiveWorktree && !wasActivationSpawn - const nextSleptWorktreeIds = - worktreeId && s.sleptWorktreeIds[worktreeId] - ? (() => { - const next = { ...s.sleptWorktreeIds } - delete next[worktreeId!] - return next - })() - : s.sleptWorktreeIds return { ...(nextTabsByWorktree !== s.tabsByWorktree ? { tabsByWorktree: nextTabsByWorktree } : {}), ptyIdsByTabId: { @@ -1402,9 +1359,6 @@ export const createTerminalSlice: StateCreator ...s.lastKnownRelayPtyIdByTabId, [tabId]: ptyId }, - ...(nextSleptWorktreeIds !== s.sleptWorktreeIds - ? { sleptWorktreeIds: nextSleptWorktreeIds } - : {}), ...(shouldBumpSortEpoch ? { sortEpoch: s.sortEpoch + 1 } : {}) } }) @@ -2018,23 +1972,12 @@ export const createTerminalSlice: StateCreator // activeWorktreeIdsOnShutdown is absent (upgrade from older build). // The raw tabs still carry ptyId values before clearTransientTerminalState // nulls them, so we can infer which worktrees had active terminals. - // If an explicit slept marker is present, do not let this legacy fallback - // reinterpret its preserved wake hint as an active reconnect target. const shutdownIds = session.activeWorktreeIdsOnShutdown ?? Object.entries(session.tabsByWorktree) - .filter( - ([worktreeId, tabs]) => - !session.sleptWorktreeIds?.[worktreeId] && tabs.some((t) => t.ptyId) - ) + .filter(([, tabs]) => tabs.some((t) => t.ptyId)) .map(([wId]) => wId) const pendingReconnectWorktreeIds = shutdownIds.filter((id) => validWorktreeIds.has(id)) - const sleptWorktreeIds = Object.fromEntries( - Object.entries(session.sleptWorktreeIds ?? {}).filter( - ([worktreeId]) => - validWorktreeIds.has(worktreeId) && !pendingReconnectWorktreeIds.includes(worktreeId) - ) - ) as Record // Why: capture which specific tabs had live PTYs per worktree from the // raw session data BEFORE clearTransientTerminalState nulled the ptyIds. @@ -2177,7 +2120,6 @@ export const createTerminalSlice: StateCreator lastVisitedAtByWorktreeId: session.lastVisitedAtByWorktreeId ?? {}, defaultTerminalTabsAppliedByWorktreeId: session.defaultTerminalTabsAppliedByWorktreeId ?? {}, - sleptWorktreeIds, pendingReconnectWorktreeIds, pendingReconnectTabByWorktree, pendingReconnectPtyIdByTabId, diff --git a/src/renderer/src/store/slices/worktrees.test.ts b/src/renderer/src/store/slices/worktrees.test.ts index 42e1e8c18..59a49ce94 100644 --- a/src/renderer/src/store/slices/worktrees.test.ts +++ b/src/renderer/src/store/slices/worktrees.test.ts @@ -106,7 +106,6 @@ function createTestStore() { shutdownWorktreeTerminals: vi.fn().mockResolvedValue(undefined), shutdownWorktreeBrowsers: vi.fn().mockResolvedValue(undefined), tabsByWorktree: {}, - sleptWorktreeIds: {}, tabBarOrderByWorktree: {}, pendingReconnectTabByWorktree: {}, activeTabIdByWorktree: {}, @@ -2731,19 +2730,6 @@ describe('worktree unread (show-until-interact)', () => { }) ) }) - - it('clears an explicit slept marker when activating the worktree', () => { - const store = createTestStore() - const worktree = makeWorktree({ id: 'repo1::/path/wt1', repoId: 'repo1', path: '/path/wt1' }) - store.setState({ - worktreesByRepo: { repo1: [worktree] }, - sleptWorktreeIds: { [worktree.id]: true } - } as Partial) - - store.getState().setActiveWorktree(worktree.id) - - expect(store.getState().sleptWorktreeIds[worktree.id]).toBeUndefined() - }) }) // Why: design §4.4 — the hydration-time purge must be gated behind a diff --git a/src/renderer/src/store/slices/worktrees.ts b/src/renderer/src/store/slices/worktrees.ts index 5853f8bab..43f87b4c3 100644 --- a/src/renderer/src/store/slices/worktrees.ts +++ b/src/renderer/src/store/slices/worktrees.ts @@ -657,7 +657,6 @@ function buildWorktreePurgeState(s: AppState, worktreeIds: string[]): Partial return next })() : s.lastVisitedAtByWorktreeId - const nextSleptWorktreeIds = - worktreeId in s.sleptWorktreeIds - ? (() => { - const next = { ...s.sleptWorktreeIds } - delete next[worktreeId] - return next - })() - : s.sleptWorktreeIds return { worktreesByRepo: next, worktreeLineageById: nextLineage, @@ -1401,7 +1392,6 @@ export const createWorktreeSlice: StateCreator activeTabType: removedActiveWorktree || activeFileCleared ? 'terminal' : s.activeTabType, everActivatedWorktreeIds: nextEverActivatedWorktreeIds, lastVisitedAtByWorktreeId: nextLastVisitedAtByWorktreeId, - sleptWorktreeIds: nextSleptWorktreeIds, sortEpoch: s.sortEpoch + 1 } }) @@ -2205,14 +2195,6 @@ export const createWorktreeSlice: StateCreator const nextEverActivated = isFirstActivation ? new Set([...s.everActivatedWorktreeIds, worktreeId!]) : s.everActivatedWorktreeIds - const nextSleptWorktreeIds = - worktreeId in s.sleptWorktreeIds - ? (() => { - const next = { ...s.sleptWorktreeIds } - delete next[worktreeId] - return next - })() - : s.sleptWorktreeIds const nextWorktrees = shouldClearUnread ? applyWorktreeUpdates(s.worktreesByRepo, worktreeId, metaUpdates) : s.worktreesByRepo @@ -2247,7 +2229,6 @@ export const createWorktreeSlice: StateCreator s.activeTabId !== activeTabId || nextActiveTabTypeByWorktree !== s.activeTabTypeByWorktree || nextEverActivated !== s.everActivatedWorktreeIds || - nextSleptWorktreeIds !== s.sleptWorktreeIds || nextWorktrees !== s.worktreesByRepo || nextDetectedWorktrees !== s.detectedWorktreesByRepo if (!hasStateChange) { @@ -2265,9 +2246,6 @@ export const createWorktreeSlice: StateCreator activeTabTypeByWorktree: nextActiveTabTypeByWorktree, activeTabId, everActivatedWorktreeIds: nextEverActivated, - ...(nextSleptWorktreeIds !== s.sleptWorktreeIds - ? { sleptWorktreeIds: nextSleptWorktreeIds } - : {}), ...(nextWorktrees !== s.worktreesByRepo ? { worktreesByRepo: nextWorktrees } : {}), ...(nextDetectedWorktrees !== s.detectedWorktreesByRepo ? { detectedWorktreesByRepo: nextDetectedWorktrees } diff --git a/src/renderer/src/web/web-workspace-session.test.ts b/src/renderer/src/web/web-workspace-session.test.ts index 809c0abf7..95f1686cd 100644 --- a/src/renderer/src/web/web-workspace-session.test.ts +++ b/src/renderer/src/web/web-workspace-session.test.ts @@ -32,7 +32,6 @@ describe('sanitizeWebRuntimeWorkspaceSession', () => { } }, activeWorktreeIdsOnShutdown: ['repo-1::/worktree'], - sleptWorktreeIds: { 'repo-1::/worktree': true }, openFilesByWorktree: { 'repo-1::/worktree': [ { @@ -147,7 +146,6 @@ describe('sanitizeWebRuntimeWorkspaceSession', () => { }) expect(sanitized.remoteSessionIdsByTabId).toBeUndefined() expect(sanitized.activeWorktreeIdsOnShutdown).toBeUndefined() - expect(sanitized.sleptWorktreeIds).toEqual({ 'repo-1::/worktree': true }) expect(sanitized.unifiedTabs).toBeUndefined() expect(sanitized.tabGroups).toBeUndefined() }) diff --git a/src/renderer/src/web/web-workspace-session.ts b/src/renderer/src/web/web-workspace-session.ts index 4b3e1801f..fbafdd277 100644 --- a/src/renderer/src/web/web-workspace-session.ts +++ b/src/renderer/src/web/web-workspace-session.ts @@ -13,7 +13,6 @@ export function sanitizeWebRuntimeWorkspaceSession( activeRepoId: session.activeRepoId ?? null, activeWorktreeId: session.activeWorktreeId ?? null, browserUrlHistory: session.browserUrlHistory ?? defaults.browserUrlHistory, - lastVisitedAtByWorktreeId: session.lastVisitedAtByWorktreeId, - sleptWorktreeIds: session.sleptWorktreeIds ?? defaults.sleptWorktreeIds + lastVisitedAtByWorktreeId: session.lastVisitedAtByWorktreeId } } diff --git a/src/shared/remote-workspace-session-projection.test.ts b/src/shared/remote-workspace-session-projection.test.ts index 578e4789d..76fdadc8d 100644 --- a/src/shared/remote-workspace-session-projection.test.ts +++ b/src/shared/remote-workspace-session-projection.test.ts @@ -49,10 +49,6 @@ describe('remote workspace session projection', () => { defaultTerminalTabsAppliedByWorktreeId: { 'repo-a::/srv/app': true as const, 'repo-local::/tmp/local': true as const - }, - sleptWorktreeIds: { - 'repo-a::/srv/app': true as const, - 'repo-local::/tmp/local': true as const } } @@ -70,7 +66,6 @@ describe('remote workspace session projection', () => { }) expect(projected.remoteSessionIdsByTabId).toEqual({ 'tab-1': 'pty-1' }) expect(projected.defaultTerminalTabsAppliedByWorktreePath).toEqual({ '/srv/app': true }) - expect(projected.sleptWorktreePaths).toEqual({ '/srv/app': true }) }) it('imports projected terminal state into this client repo id', () => { @@ -96,8 +91,7 @@ describe('remote workspace session projection', () => { 'tab-1': { root: null, activeLeafId: null, expandedLeafId: null } }, remoteSessionIdsByTabId: { 'tab-1': 'pty-1' }, - defaultTerminalTabsAppliedByWorktreePath: { '/srv/app': true }, - sleptWorktreePaths: { '/srv/app': true } + defaultTerminalTabsAppliedByWorktreePath: { '/srv/app': true } }, { resolveWorktreeId: (path) => (path === '/srv/app' ? 'repo-b::/srv/app' : null) } ) @@ -112,9 +106,6 @@ describe('remote workspace session projection', () => { expect(session.defaultTerminalTabsAppliedByWorktreeId).toEqual({ 'repo-b::/srv/app': true }) - expect(session.sleptWorktreeIds).toEqual({ - 'repo-b::/srv/app': true - }) }) it('imports active worktree metadata even when the worktree has no terminal tabs', () => { diff --git a/src/shared/remote-workspace-session-projection.ts b/src/shared/remote-workspace-session-projection.ts index 940824632..647b577db 100644 --- a/src/shared/remote-workspace-session-projection.ts +++ b/src/shared/remote-workspace-session-projection.ts @@ -90,17 +90,6 @@ export function exportRemoteWorkspaceSession( } } - const sleptWorktreePaths: Record = {} - for (const worktreeId of Object.keys(session.sleptWorktreeIds ?? {})) { - if (!options.isTargetWorktree(worktreeId)) { - continue - } - const worktreePath = worktreePathFromId(worktreeId) - if (worktreePath) { - sleptWorktreePaths[worktreePath] = true - } - } - return { activeWorktreePath, activeTabId, @@ -123,8 +112,7 @@ export function exportRemoteWorkspaceSession( ) : undefined, lastVisitedAtByWorktreePath, - defaultTerminalTabsAppliedByWorktreePath, - sleptWorktreePaths + defaultTerminalTabsAppliedByWorktreePath } } @@ -189,14 +177,6 @@ export function importRemoteWorkspaceSession( } } - const sleptWorktreeIds: Record = {} - for (const worktreePath of Object.keys(remote.sleptWorktreePaths ?? {})) { - const worktreeId = resolvePath(worktreePath) - if (worktreeId) { - sleptWorktreeIds[worktreeId] = true - } - } - return { ...session, activeRepoId: activeWorktreeId ? (splitWorktreeId(activeWorktreeId)?.repoId ?? null) : null, @@ -220,7 +200,6 @@ export function importRemoteWorkspaceSession( ) : undefined, lastVisitedAtByWorktreeId, - defaultTerminalTabsAppliedByWorktreeId, - sleptWorktreeIds + defaultTerminalTabsAppliedByWorktreeId } } diff --git a/src/shared/remote-workspace-types.ts b/src/shared/remote-workspace-types.ts index 2fd7ea862..419bb7651 100644 --- a/src/shared/remote-workspace-types.ts +++ b/src/shared/remote-workspace-types.ts @@ -14,7 +14,6 @@ export type RemoteWorkspaceSession = { remoteSessionIdsByTabId?: Record lastVisitedAtByWorktreePath?: Record defaultTerminalTabsAppliedByWorktreePath?: Record - sleptWorktreePaths?: Record } export type RemoteWorkspaceSnapshot = { diff --git a/src/shared/types.ts b/src/shared/types.ts index 32101fb09..a47571b48 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -719,9 +719,6 @@ export type WorkspaceSessionState = { * considered. Persisted so closing all tabs and re-opening the workspace * does not recreate the template. */ defaultTerminalTabsAppliedByWorktreeId?: Record - /** Worktrees the user explicitly slept. Inactive terminal state alone does - * not imply sleep; a worktree only sleeps through the user sleep action. */ - sleptWorktreeIds?: Record } export type WorkspaceSessionPatch = Partial diff --git a/src/shared/workspace-session-schema.test.ts b/src/shared/workspace-session-schema.test.ts index 0a3ba0d2f..7b02044a6 100644 --- a/src/shared/workspace-session-schema.test.ts +++ b/src/shared/workspace-session-schema.test.ts @@ -242,26 +242,6 @@ describe('parseWorkspaceSession', () => { } }) - it('accepts explicit slept worktree markers', () => { - const result = parseWorkspaceSession({ - activeRepoId: null, - activeWorktreeId: null, - activeTabId: null, - tabsByWorktree: {}, - terminalLayoutsByTabId: {}, - sleptWorktreeIds: { - 'repo1::/path/wt1': true - } - }) - - expect(result.ok).toBe(true) - if (result.ok) { - expect(result.value.sleptWorktreeIds).toEqual({ - 'repo1::/path/wt1': true - }) - } - }) - it('caps oversized browser history while parsing legacy workspace sessions', () => { const result = parseWorkspaceSession({ activeRepoId: null, diff --git a/src/shared/workspace-session-schema.ts b/src/shared/workspace-session-schema.ts index a5fe7b6c1..f78c6cfee 100644 --- a/src/shared/workspace-session-schema.ts +++ b/src/shared/workspace-session-schema.ts @@ -254,8 +254,7 @@ export const workspaceSessionStateSchema: z.ZodType = z.o z.record(z.string(), z.number().finite().nonnegative()) ) .optional(), - defaultTerminalTabsAppliedByWorktreeId: z.record(z.string(), z.literal(true)).optional(), - sleptWorktreeIds: z.record(z.string(), z.literal(true)).optional() + defaultTerminalTabsAppliedByWorktreeId: z.record(z.string(), z.literal(true)).optional() }) export type ParsedWorkspaceSession =