Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ff8a162
fix(session): persist defaultTerminalTabsAppliedByWorktreeId
innocarpe Sep 2, 2026
1621f83
fix(runtime): stop re-seeding a runtime-owned workspace the user emptied
nwparker Sep 3, 2026
0c7ce39
fix(runtime): harden the runtime-owned initial-terminal bootstrap latch
nwparker Sep 8, 2026
957064f
fix(runtime): key the initial-terminal bootstrap latch per environment
github-actions[bot] Sep 8, 2026
2ce07dc
fix(runtime): release the bootstrap latch when the create returns failed
nwparker Sep 8, 2026
e42fbfc
fix(runtime): release a parked bootstrap on the mirror's next frame
nwparker Sep 8, 2026
e0ce8c8
fix(runtime): let a failed create retry inside its own subscription
nwparker Sep 10, 2026
1857814
fix(runtime): close the second re-seed door on an emptied workspace
nwparker Sep 10, 2026
e1458bf
fix(runtime): stop the worktree-id re-key inventing a tombstone
nwparker Sep 10, 2026
22bd6bd
fix(runtime): reconcile session tab bootstrap integration
nwparker Sep 19, 2026
bbd56bb
fix(runtime): preserve terminal wake launch options
nwparker Sep 19, 2026
bbb88de
test(session): annotate cross-project persistence fixture
nwparker Sep 19, 2026
a5d5a4e
test(session): remove obsolete typecheck suppression
nwparker Sep 19, 2026
c50aff0
test(session): keep persistence regression in renderer project
nwparker Sep 19, 2026
bc8e026
refactor(runtime): remove inert snapshot recovery wrapper
nwparker Sep 19, 2026
afc62a4
fix(runtime): keep removal-frame import merge-safe
nwparker Sep 19, 2026
03f0d29
Merge remote-tracking branch 'origin/main' into nwparker/rmemeber-tabs
nwparker Sep 19, 2026
0e5b4f4
fix(runtime): deduplicate merged removal-frame import
nwparker Sep 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/main/persistence-host-admitted-terminal-membership.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,29 @@ describe('host-admitted terminal membership survives a stale renderer replay', (
expect(persistedTabIds(store.getWorkspaceSession(), OTHER_WORKTREE)).toContain('host-tab-other')
})

it('stamps default-terminal-tab markers so a renderer persist snapshot cannot un-apply them', async () => {
const store = await createStore()
store.setWorkspaceSession(rendererSession())

expect(
store.persistPtyBinding({
worktreeId: WORKTREE,
tabId: 'host-tab',
leafId: TEST_LEAF_2,
ptyId: 'host-pty',
hostAdmittedMembership: true
})
).toBe(true)
expect(store.getWorkspaceSession().defaultTerminalTabsAppliedByWorktreeId?.[WORKTREE]).toBe(
true
)

store.setWorkspaceSession(rendererSession())
expect(store.getWorkspaceSession().defaultTerminalTabsAppliedByWorktreeId?.[WORKTREE]).toBe(
true
)
})

// Polarity: without the flag the renderer still owns membership, so a renderer
// spawn racing its own writer must not freeze the tab list.
it('leaves renderer-owned membership alone when the binding is not host-admitted', async () => {
Expand Down
5 changes: 5 additions & 0 deletions src/main/persistence/loading-store/pty-binding-persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ function applyPtyBinding(
[bindingWorktreeId]: session.activeTabIdByWorktree?.[bindingWorktreeId] ?? args.tabId
}
}
// Why: host-initiated persist snapshots used to omit this write-once guard, so every launch or reattach treated the worktree as never having default terminals applied.
session.defaultTerminalTabsAppliedByWorktreeId = {
...session.defaultTerminalTabsAppliedByWorktreeId,
[bindingWorktreeId]: true
}
Comment on lines +222 to +225

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 15 '\bevaluatePtyBindingFastLane\s*\(' src/main/persistence

Repository: stablyai/orca

Length of output: 14046


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- fast-lane implementation ---'
cat -n src/main/persistence/loading-store/pty-binding-fast-lane.ts
printf '%s\n' '--- persistence marker references ---'
rg -n -C 12 'defaultTerminalTabsAppliedByWorktreeId|applyPtyBinding|persistPtyBinding|writePtyBinding' src/main/persistence/loading-store/pty-binding-persistence.ts src/main/persistence/loading-store src/main | head -n 320

Repository: stablyai/orca

Length of output: 35737


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 12 'defaultTerminalTabsAppliedByWorktreeId|default terminal|defaultTerminal' src shared tests 2>/dev/null | head -n 360

Repository: stablyai/orca

Length of output: 31497


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 18 'defaultTerminalTabsAppliedByWorktreeId|defaultTerminalTabsAppliedByWorktreePath' src
printf '%s\n' '--- default terminal application symbols ---'
rg -n -C 12 'apply.*default|default.*terminal|createDefault.*Terminal|defaultTerminalTabs' src/main src/renderer src/shared

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 10 --glob '!**/*.test.*' --glob '!**/i18n/**' --glob '!**/locales/**' 'defaultTerminalTabsAppliedByWorktreeId' src
printf '%s\n' '--- likely activation guards ---'
rg -n -C 8 --glob '!**/*.test.*' --glob '!**/i18n/**' --glob '!**/locales/**' 'defaultTabs|defaultTerminal|initialTerminal|applyDefault' src/renderer/src/lib src/renderer/src/hooks src/renderer/src/components src/main

Repository: stablyai/orca

Length of output: 50370


Ensure the fast lane does not bypass the marker write.

evaluatePtyBindingFastLane does not check defaultTerminalTabsAppliedByWorktreeId. A durable binding with matching state can take the fast lane while its marker is absent. persistPtyBinding then returns before applyPtyBinding writes the marker, so a later activation can apply the default terminals again.

Make a missing marker force the durable write path.

if (!isTerminalLeafId(args.leafId)) {
// Why: keep legacy renderer-local pane ids out of durable leaf-keyed layout state after the UUID migration.
advanceTopologyFence()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,27 @@ describe('preserving runtime-authored workspace session fields', () => {
expect(preserveRuntimeAuthoredWorkspaceSessionFields(next, session())).toBe(next)
expect(preserveRuntimeAuthoredWorkspaceSessionFields(next, null)).toBe(next)
})

it('keeps default-terminal-tab markers a persist snapshot omitted', () => {
const next = preserveRuntimeAuthoredWorkspaceSessionFields(session(), {
...session(),
defaultTerminalTabsAppliedByWorktreeId: { 'repo-1::/wt-a': true }
})

expect(next.defaultTerminalTabsAppliedByWorktreeId).toEqual({ 'repo-1::/wt-a': true })
})

it('unions default-terminal-tab markers instead of replacing with a partial snapshot', () => {
const next = preserveRuntimeAuthoredWorkspaceSessionFields(
{ ...session(), defaultTerminalTabsAppliedByWorktreeId: { 'repo-1::/wt-b': true } },
{ ...session(), defaultTerminalTabsAppliedByWorktreeId: { 'repo-1::/wt-a': true } }
)

expect(next.defaultTerminalTabsAppliedByWorktreeId).toEqual({
'repo-1::/wt-a': true,
'repo-1::/wt-b': true
})
})
})

function session(
Expand Down
50 changes: 34 additions & 16 deletions src/main/persistence/runtime-authored-workspace-session-fields.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'

/**
* Keeps runtime-authored session state alive across a renderer's full write.
* Keeps session fields the renderer persist snapshot does not author across a full write.
*
* A session write replaces the stored object, and the renderer builds its payload from Zustand --
* which has no idea the runtime authority sharing this profile also persists the client-hosted
* pages it owns. Without this, every ordinary desktop session write erases them, and the loss only
* shows up a restart later when there is nothing left to rehydrate.
* A session write replaces the stored object. Zustand-built payloads omit runtime-owned
* client-hosted pages, and they omit-when-empty the write-once default-terminal-tab marker.
* Without this, those slices vanish on the next desktop write and only show up missing after
* restart.
*
* Callers do not opt in: the Store applies this inside setLocalWorkspaceSession and
* setHostWorkspaceSession, so the before-unload stage path inherits it too. Guarding the individual
* writers instead is what let the quit write -- the most common one there is -- erase the field.
* setHostWorkspaceSession, so the before-unload stage path inherits it too.
*
* A runtime clearing its own rows writes an empty map, not `undefined`, so this can never pin a
* stale set: only an author that never mentioned the field inherits the previous one.
* A runtime clearing client-hosted pages writes an empty map, not `undefined`. The default-tab
* marker is write-once: omission means "never stamped", not "never applied".
*/
function unionWriteOnceDefaultTerminalTabsApplied(
next: WorkspaceSessionState,
prior: WorkspaceSessionState | null | undefined
): WorkspaceSessionState {
const priorMarks = prior?.defaultTerminalTabsAppliedByWorktreeId
if (!priorMarks || Object.keys(priorMarks).length === 0) {
return next
}
const nextMarks = next.defaultTerminalTabsAppliedByWorktreeId
const merged = { ...priorMarks, ...nextMarks }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ The union cannot distinguish "field omitted" from "key intentionally removed", so it resurrects markers the renderer deliberately dropped. The bulk worktree-purge path removes a key from the store and the resulting patch carries the reduced map (e.g. {B:true}), so merged re-adds A; because the field is owner-keyed, the resurrected key then pins the removed worktree's metadata against pruning.

Technical details
# Write-once union resurrects explicitly removed markers

## Affected sites
- `src/main/persistence/runtime-authored-workspace-session-fields.ts:26` — unconditional union over prior marks.
- `src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts:243-245` — bulk purge drops the key (callers `fetch-all-worktrees.ts`, `fetched-worktree-merge.ts`, `worktree-event-runtime.ts`).
- `src/renderer/src/lib/workspace-session-patch.ts:189-195` — sends the whole reduced map when non-empty.
- `src/main/persistence/loading-store/session-snapshot-operations.ts:65-75` — a patch containing `tabsByWorktree` routes through `setLocalWorkspaceSession`/`setHostWorkspaceSession`, where `preserveRuntimeAuthoredWorkspaceSessionFields` runs.
- `src/main/persistence/restoring-sessions/session-worktree-ownership.ts:51` — field is `owner-keyed`, so the stale key counts as a live owner.

## Required outcome
- An explicit key removal by a writer must survive the union, while a writer that omits the field entirely still inherits prior marks.

## Suggested approach (optional)
- Only union when `next.defaultTerminalTabsAppliedByWorktreeId === undefined` (the clientHostedBrowserPages precedent), or have the purge path also clear the key through the owner-keyed prune that the explicit single-worktree removal already uses.

## Open questions for the human
- Is the permanent-write-once behaviour intended even across worktree removal, given the marker's `owner-keyed` role in metadata pruning?

if (nextMarks && Object.keys(merged).length === Object.keys(nextMarks).length) {
return next
}
return { ...next, defaultTerminalTabsAppliedByWorktreeId: merged }
}

export function preserveRuntimeAuthoredWorkspaceSessionFields(
next: WorkspaceSessionState,
prior: WorkspaceSessionState | null | undefined
): WorkspaceSessionState {
let result = next
if (
next.clientHostedBrowserPagesByWorktree !== undefined ||
prior?.clientHostedBrowserPagesByWorktree === undefined
next.clientHostedBrowserPagesByWorktree === undefined &&
prior?.clientHostedBrowserPagesByWorktree !== undefined
) {
return next
}
return {
...next,
clientHostedBrowserPagesByWorktree: prior.clientHostedBrowserPagesByWorktree
result = {
...result,
clientHostedBrowserPagesByWorktree: prior.clientHostedBrowserPagesByWorktree
}
}
// Why union: persist snapshots omit this write-once map (empty Zustand slice, omit-when-empty
// payload). Treating omission as "never applied" re-spawns default terminals on every attach.
return unionWriteOnceDefaultTerminalTabsApplied(result, prior)
}
62 changes: 62 additions & 0 deletions src/main/runtime/workspace-session-worktree-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Re-keying a workspace's session rows must not invent state the user never created.
*
* `tabsByWorktree` carries three states, not two: an absent row is "never initialized" and an
* explicit empty row is the closed-last-terminal tombstone that `shouldAutoCreateInitialTerminal`
* honours. Writing `[]` for a source that had no row turns the first into the second, and the
* workspace then never gets its initial terminal.
*/
import { describe, expect, it } from 'vitest'
import { getDefaultWorkspaceSession } from '../../shared/constants'
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
import { canonicalizeTerminalSessionWorktreeId } from './workspace-session-worktree-id'

const SOURCE = 'repo-1::/old/path'
const TARGET = 'repo-1::/new/path'

function tab(id: string, worktreeId: string) {
return {
id,
ptyId: `pty-${id}`,
worktreeId,
title: id,
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
}

function session(overrides: Partial<WorkspaceSessionState> = {}): WorkspaceSessionState {
return { ...getDefaultWorkspaceSession(), ...overrides }
}

describe('canonicalizeTerminalSessionWorktreeId', () => {
it('does not fabricate a tombstone for a workspace that had no tabs row', () => {
const next = session({ tabsByWorktree: {} })

canonicalizeTerminalSessionWorktreeId(next, SOURCE, TARGET)

expect(Object.hasOwn(next.tabsByWorktree, TARGET)).toBe(false)
})

it('carries a real closed-last-terminal tombstone across the re-key', () => {
// The other direction: an explicit empty row is user intent and must survive, so the guard has
// to be about the source row's presence and not about the tabs being empty.
const next = session({ tabsByWorktree: { [SOURCE]: [] } })

canonicalizeTerminalSessionWorktreeId(next, SOURCE, TARGET)

expect(Object.hasOwn(next.tabsByWorktree, TARGET)).toBe(true)
expect(next.tabsByWorktree[TARGET]).toEqual([])
expect(Object.hasOwn(next.tabsByWorktree, SOURCE)).toBe(false)
})

it('re-keys the tabs and their own worktree ids', () => {
const next = session({ tabsByWorktree: { [SOURCE]: [tab('tab-1', SOURCE)] } })

canonicalizeTerminalSessionWorktreeId(next, SOURCE, TARGET)

expect(next.tabsByWorktree[TARGET]?.map((entry) => entry.worktreeId)).toEqual([TARGET])
})
})
18 changes: 12 additions & 6 deletions src/main/runtime/workspace-session-worktree-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ export function canonicalizeTerminalSessionWorktreeId(
if (sourceWorktreeId === targetWorktreeId) {
return
}
const tabs = session.tabsByWorktree[sourceWorktreeId] ?? []
delete session.tabsByWorktree[sourceWorktreeId]
session.tabsByWorktree[targetWorktreeId] = tabs.map((tab) => ({
...tab,
worktreeId: targetWorktreeId
}))
// Why presence and not `?? []`: an absent row means never initialized, an explicit empty one is
// the closed-last-terminal tombstone. Writing `[]` for a source that had no row invents that
// tombstone, and the workspace then never gets its initial terminal. Same guard the keyed maps
// below already use.
if (Object.hasOwn(session.tabsByWorktree, sourceWorktreeId)) {
const tabs = session.tabsByWorktree[sourceWorktreeId] ?? []
delete session.tabsByWorktree[sourceWorktreeId]
session.tabsByWorktree[targetWorktreeId] = tabs.map((tab) => ({
...tab,
worktreeId: targetWorktreeId
}))
}
Comment on lines +15 to +22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,140p' src/main/runtime/workspace-session-worktree-id.ts
rg -n 'canonicalizeTerminalSessionWorktreeId|defaultTerminalTabsAppliedByWorktreeId' src/main/runtime src/main/persistence src/renderer/src | head -200

Repository: stablyai/orca

Length of output: 15008


🏁 Script executed:

set -eu
printf '%s\n' '--- runtime caller ---'
cat -n src/main/runtime/runtime-terminal-orphan-session-adoption.ts
printf '%s\n' '--- canonicalization tests ---'
cat -n src/main/runtime/workspace-session-worktree-id.test.ts
printf '%s\n' '--- identity migration ---'
sed -n '150,245p' src/main/persistence/tracking-repos/worktree-identity-migration.ts
printf '%s\n' '--- identity migration coverage tests ---'
sed -n '120,210p' src/main/persistence/tracking-repos/worktree-identity-migration-field-coverage.test.ts
printf '%s\n' '--- migration callers ---'
rg -n -C 5 'canonicalizeTerminalSessionWorktreeId|worktree-identity-migration|moveSessionKey' src/main src/renderer/src | head -260
printf '%s\n' '--- default terminal application ---'
cat -n src/renderer/src/lib/worktree-default-terminal-tabs.ts
printf '%s\n' '--- marker writes and relevant activation flow ---'
rg -n -C 8 'markDefaultTerminalTabsApplied|applyDefaultTerminalTabs|defaultTerminalTabsAppliedByWorktreeId' src/renderer/src/lib src/renderer/src/store | head -320

Repository: stablyai/orca

Length of output: 50369


🏁 Script executed:

set -eu
printf '%s\n' '--- moveSessionKey definition ---'
sed -n '95,150p' src/main/persistence/tracking-repos/worktree-identity-migration.ts
printf '%s\n' '--- migration test references to default marker and collisions ---'
rg -n -C 10 'defaultTerminalTabsAppliedByWorktreeId|target|collision|existing' src/main/persistence/tracking-repos/*worktree-identity-migration*.test.ts
printf '%s\n' '--- relevant workspace session state defaults/types ---'
rg -n -C 5 'defaultTerminalTabsAppliedByWorktreeId' src/shared src/main/runtime src/main/persistence/tracking-repos/worktree-identity-migration.ts

Repository: stablyai/orca

Length of output: 19933


Re-key the default-terminal marker with the terminal row.

This runtime migration moves tabsByWorktree[sourceWorktreeId] but does not move defaultTerminalTabsAppliedByWorktreeId[sourceWorktreeId]. The separate persistence migration does not run on this caller path.

If the source marker is true, the target ID remains unmarked. applyDefaultTerminalTabs can then create the default terminals again. Move the marker to targetWorktreeId, preserve an existing target marker, and delete the source marker. Add this case to workspace-session-worktree-id.test.ts.


const groups = session.tabGroups?.[sourceWorktreeId]
if (groups) {
Expand Down
132 changes: 132 additions & 0 deletions src/renderer/src/lib/default-terminal-tabs-applied-persist.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getDefaultWorkspaceSession } from '../../../shared/constants'
import { parseWorkspaceSession } from '../../../shared/workspace-session-schema'
import { useAppStore, type AppState } from '@/store'
import { applyDefaultTerminalTabs } from './worktree-default-terminal-tabs'
import {
createSessionWriteSubscriber,
type WorkspaceSessionWrite
} from './session-write-subscriber'
import { createTestStore, makeWorktree, seedStore } from '@/store/slices/store-test-helpers'

let initialState: AppState

const WORKTREE_ID = 'repo1::/wt-1'
const DEFAULT_TABS = {
runCommands: false,
tabs: [{ title: 'Claude' }]
}

function preserveRuntimeAuthoredWorkspaceSessionFields<T extends Record<string, unknown>>(
next: T,
prior: T
): T {
const marks = prior.defaultTerminalTabsAppliedByWorktreeId
if (!marks || typeof marks !== 'object') {
return next
}
return {
...next,
defaultTerminalTabsAppliedByWorktreeId: {
...marks,
...(next.defaultTerminalTabsAppliedByWorktreeId as Record<string, boolean> | undefined)

Check failure on line 32 in src/renderer/src/lib/default-terminal-tabs-applied-persist.test.ts

View workflow job for this annotation

GitHub Actions / static analysis

typescript(consistent-type-assertions)

Do not use any type assertions.
}
}
}

describe('defaultTerminalTabsAppliedByWorktreeId persist round-trip', () => {
beforeEach(() => {
initialState = useAppStore.getState()
vi.useFakeTimers()
})

afterEach(() => {
vi.useRealTimers()
useAppStore.setState(initialState, true)
})

it('keeps the marker through mark, persist patch, omitted snapshot, and hydration', () => {
const persist = vi.fn<(payload: WorkspaceSessionWrite) => void>()
const cleanup = createSessionWriteSubscriber({ store: useAppStore, persist })

useAppStore.setState({ workspaceSessionReady: true, hydrationSucceeded: true })
vi.advanceTimersByTime(200)
persist.mockClear()

useAppStore.getState().markDefaultTerminalTabsApplied(WORKTREE_ID)
vi.advanceTimersByTime(200)

expect(persist.mock.calls[0][0].patch.defaultTerminalTabsAppliedByWorktreeId).toEqual({
[WORKTREE_ID]: true
})

const patched = {
...getDefaultWorkspaceSession(),
...persist.mock.calls[0][0].patch
}
// Persist snapshots omit this write-once slice (empty payload / renderer never mentioned it).
const omittedSnapshot = getDefaultWorkspaceSession()
const preserved = preserveRuntimeAuthoredWorkspaceSessionFields(omittedSnapshot, patched)
const parsed = parseWorkspaceSession(JSON.parse(JSON.stringify(preserved)))
expect(parsed.ok).toBe(true)
if (!parsed.ok) {
return
}

const store = createTestStore()
seedStore(store, {
worktreesByRepo: {
repo1: [makeWorktree({ id: WORKTREE_ID, repoId: 'repo1', path: '/wt-1' })]
}
})
store.getState().hydrateWorkspaceSession(parsed.value)

expect(store.getState().defaultTerminalTabsAppliedByWorktreeId[WORKTREE_ID]).toBe(true)
expect(
applyDefaultTerminalTabs(
store.getState(),
WORKTREE_ID,
undefined,
undefined,
undefined,
DEFAULT_TABS,
undefined,
undefined
)
).toBeNull()
expect(store.getState().tabsByWorktree[WORKTREE_ID]).toBeUndefined()
cleanup()
})

it('does not drop in-memory markers when hydration omits the map', () => {
const store = createTestStore()
seedStore(store, {
worktreesByRepo: {
repo1: [makeWorktree({ id: WORKTREE_ID, repoId: 'repo1', path: '/wt-1' })]
}
})
store.getState().markDefaultTerminalTabsApplied(WORKTREE_ID)

store.getState().hydrateWorkspaceSession({
activeRepoId: 'repo1',
activeWorktreeId: WORKTREE_ID,
activeTabId: null,
terminalLayoutsByTabId: {},
tabsByWorktree: {}
})

expect(store.getState().defaultTerminalTabsAppliedByWorktreeId[WORKTREE_ID]).toBe(true)
expect(
applyDefaultTerminalTabs(
store.getState(),
WORKTREE_ID,
undefined,
undefined,
undefined,
DEFAULT_TABS,
undefined,
undefined
)
).toBeNull()
})
})
19 changes: 19 additions & 0 deletions src/renderer/src/lib/session-write-subscriber.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,25 @@ describe('createSessionWriteSubscriber', () => {
cleanup()
})

it('persists defaultTerminalTabsAppliedByWorktreeId after markDefaultTerminalTabsApplied', () => {
const persist = vi.fn<(payload: WorkspaceSessionWrite) => void>()
const cleanup = createSessionWriteSubscriber({ store: useAppStore, persist })

useAppStore.setState({ workspaceSessionReady: true, hydrationSucceeded: true })
vi.advanceTimersByTime(200)
persist.mockClear()

const worktreeId = 'repo1::/wt-1'
useAppStore.getState().markDefaultTerminalTabsApplied(worktreeId)
vi.advanceTimersByTime(200)

expect(persist).toHaveBeenCalledTimes(1)
expect(persist.mock.calls[0][0].patch.defaultTerminalTabsAppliedByWorktreeId).toEqual({
[worktreeId]: true
})
cleanup()
})

it('writes exactly once when a relevant field changes', () => {
const persist = vi.fn<(payload: WorkspaceSessionWrite) => void>()
const cleanup = createSessionWriteSubscriber({ store: useAppStore, persist })
Expand Down
Loading
Loading