Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
131 commits
Select commit Hold shift + click to select a range
0ff240a
test(terminal): pin layout invariants the mirror refactor must keep
Jinwoo-H Oct 7, 2026
cd70160
refactor(terminal): renderer senders attach placement to pty spawn, r…
Jinwoo-H Oct 7, 2026
97a4165
fix(terminal): background agent placement row claims no launchAgent t…
Jinwoo-H Oct 7, 2026
bd6890e
fix(terminal): a before-split publishes its pane with the final tree …
Jinwoo-H Oct 7, 2026
4a9367e
refactor(terminal): main applies the placement a spawn carries
Jinwoo-H Oct 7, 2026
b398319
test(terminal): type the background-terminal tab patch precisely
Jinwoo-H Oct 7, 2026
08c5569
Merge remote-tracking branch 'origin/topo-b1-5-renderer-placement' in…
Jinwoo-H Oct 7, 2026
b7687b9
refactor(terminal): check the proposed tree's leaves only when there …
Jinwoo-H Oct 7, 2026
36d21f8
refactor(terminal): publish main's terminal layout slices to the wind…
Jinwoo-H Oct 7, 2026
91d0da3
Merge remote-tracking branch 'origin/main' into topo-c2-publisher-con…
Jinwoo-H Oct 7, 2026
606e50e
refactor(terminal): name the topology push on pty:spawn replies too
Jinwoo-H Oct 7, 2026
9381afa
Merge remote-tracking branch 'origin/test/terminal-topology-b2-gates'…
Jinwoo-H Oct 7, 2026
c502ff8
feat(terminal): window starts from and follows main's terminal layout
Jinwoo-H Oct 7, 2026
d96b587
refactor(terminal): declare spawn's publishSeq inline
Jinwoo-H Oct 7, 2026
b1b5590
fix(terminal): a placed new tab takes startupCwd only from the window…
Jinwoo-H Oct 7, 2026
54a5c18
Merge remote-tracking branch 'origin/topo-c1-placement-applies' into …
Jinwoo-H Oct 7, 2026
7f7f2e3
refactor(terminal): mounted panes follow the layout main publishes, f…
Jinwoo-H Oct 7, 2026
c0fe642
refactor(terminal): a layout gesture commits to main and holds until …
Jinwoo-H Oct 7, 2026
cb6c279
refactor(terminal): pending panes; drag-out and closes follow main's …
Jinwoo-H Oct 7, 2026
c02b311
Merge remote-tracking branch 'origin/topo-c5a-pending-panes' into top…
Jinwoo-H Oct 7, 2026
3035bc3
fix(terminal): a layout gesture writes only the worktree's home parti…
Jinwoo-H Oct 7, 2026
9baccd2
refactor(terminal): a layout gesture is a pending change, beside pend…
Jinwoo-H Oct 7, 2026
d11d78b
refactor(terminal): setLayout writes its one partition without a shar…
Jinwoo-H Oct 7, 2026
2a9ea62
fix(terminal): a pane drag-out writes only the worktree's home partition
Jinwoo-H Oct 7, 2026
791ba27
refactor(terminal): sleeping-agent records commit to main (core C5c)
Jinwoo-H Oct 7, 2026
1c8ecc4
refactor(terminal): binding sites keep liveness; main owns which pane…
Jinwoo-H Oct 7, 2026
414e00e
fix(terminal): a running agent re-attached to a split pane keeps its …
Jinwoo-H Oct 7, 2026
e2de4ae
fix(terminal): a divider drag right after a split is kept
Jinwoo-H Oct 7, 2026
3ca6e5f
refactor(ssh): main exports SSH workspaces after its own session writ…
Jinwoo-H Oct 7, 2026
e877702
refactor(terminal): main resolves a worktree's one home partition
Jinwoo-H Oct 7, 2026
0d918b7
refactor(terminal): sleeping records commit through one channel to th…
Jinwoo-H Oct 7, 2026
a0072aa
test(ssh): admit main's import commit at the session-patch boundary; …
Jinwoo-H Oct 7, 2026
72586d0
fix(terminal): a live agent re-attaches to its pane over an exited pr…
Jinwoo-H Oct 7, 2026
2a62f9e
fix(ssh): report only exports the current agreement still owns; pin t…
Jinwoo-H Oct 7, 2026
85196e0
fix(ssh): an import that kept local state exports it, as the window's…
Jinwoo-H Oct 7, 2026
740a09d
Merge remote-tracking branch 'origin/main' into topo-c1-placement-app…
Jinwoo-H Oct 7, 2026
d49fd67
Merge branch 'topo-c1-placement-applies' of ../topo-c1 into topo-c2-p…
Jinwoo-H Oct 7, 2026
602a14b
Merge branch 'topo-c2-publisher-connected' of ../topo-c2 into topo-c3…
Jinwoo-H Oct 7, 2026
43f35c2
Merge branch 'topo-c3-mirror-apply' of ../topo-c3 into topo-c4-reconc…
Jinwoo-H Oct 7, 2026
94fad63
Merge branch 'topo-c4-reconciler' of ../topo-c4 into topo-c5a-pending…
Jinwoo-H Oct 7, 2026
61f2869
Merge branch 'topo-c5a-pending-panes' of ../topo-c5a into topo-c6-set…
Jinwoo-H Oct 7, 2026
d537566
Merge branch 'topo-c6-set-layout' of ../topo-c6 into topo-c5b-binding…
Jinwoo-H Oct 7, 2026
878f459
Merge branch 'topo-c5b-binding-split' of ../topo-c5b into topo-c5c-sl…
Jinwoo-H Oct 7, 2026
50ef763
Merge branch 'topo-c5c-sleep-wake' of ../topo-c5c into topo-c5d-ssh-i…
Jinwoo-H Oct 7, 2026
68b18b3
refactor(terminal): window saves carry presentation only; delete the …
Jinwoo-H Oct 7, 2026
9304237
test(terminal): make the setup-split parity scenario reproduce STA-94…
Jinwoo-H Oct 8, 2026
d2f6df0
fix(terminal): main clears a tab's launched agent and keeps creation …
Jinwoo-H Oct 8, 2026
fba1399
fix(terminal): a pane respawned over an absent process keeps its plac…
Jinwoo-H Oct 8, 2026
a3831f6
Merge branch 'topo-c3-mirror-apply' of ../topo-c3 into topo-c4-reconc…
Jinwoo-H Oct 8, 2026
2f76ea4
Merge branch 'topo-c4-reconciler' of ../topo-c4 into topo-c5a-pending…
Jinwoo-H Oct 8, 2026
d844432
Merge branch 'topo-c5a-pending-panes' of ../topo-c5a into topo-c6-set…
Jinwoo-H Oct 8, 2026
609cc17
Merge branch 'topo-c6-set-layout' of ../topo-c6 into topo-c5b-binding…
Jinwoo-H Oct 8, 2026
ef94aa5
Merge branch 'topo-c5b-binding-split' of ../topo-c5b into topo-c5c-sl…
Jinwoo-H Oct 8, 2026
695f194
fix(ssh): an export agreement holds only over the connection it was m…
Jinwoo-H Oct 8, 2026
629b47e
fix(relay): a shutting-down relay records no workspace patch
Jinwoo-H Oct 8, 2026
9ba6615
Merge branch 'topo-c5c-sleep-wake' of ../topo-c5c into topo-c5d-ssh-i…
Jinwoo-H Oct 8, 2026
dd76721
Merge remote-tracking branch 'origin/topo-c5d-ssh-import-export' into…
Jinwoo-H Oct 8, 2026
7770aff
Merge remote-tracking branch 'origin/topo-c5d-ssh-import-export' into…
Jinwoo-H Oct 8, 2026
b9a4ee5
Merge branch 'topo-c5d-ssh-import-export' of ../topo-c5d into topo-c7…
Jinwoo-H Oct 8, 2026
197646f
fix(ssh): a window's import writes only its target's partition
Jinwoo-H Oct 8, 2026
6ee0de3
Merge remote-tracking branch 'origin/topo-c5d-ssh-import-export' into…
Jinwoo-H Oct 8, 2026
0a92a89
fix(terminal): hold pending entries only for tabs main's push settles
Jinwoo-H Oct 8, 2026
94c8c9e
Merge remote-tracking branch 'origin/topo-c5a-pending-panes' into top…
Jinwoo-H Oct 8, 2026
4f88696
Merge remote-tracking branch 'origin/topo-c6-set-layout' into topo-c5…
Jinwoo-H Oct 8, 2026
31dcee2
Merge remote-tracking branch 'origin/topo-c5b-binding-split' into top…
Jinwoo-H Oct 8, 2026
6d6a59d
Merge remote-tracking branch 'origin/topo-c5c-sleep-wake' into topo-c…
Jinwoo-H Oct 8, 2026
1f055aa
Merge remote-tracking branch 'origin/topo-c5d-ssh-import-export' into…
Jinwoo-H Oct 8, 2026
eee6544
refactor(terminal): state the core's rules in its comments; reuse the…
Jinwoo-H Oct 8, 2026
fed9395
test(terminal): type the absent-respawn fixture and drop the optional…
Jinwoo-H Oct 8, 2026
b9d474a
Merge topo-c3 review fixes into topo-c4-reconciler
Jinwoo-H Oct 8, 2026
aa026c0
Merge topo-c3 review fixes into topo-c5a-pending-panes
Jinwoo-H Oct 8, 2026
1caac05
Merge topo-c3 review fixes into topo-c6-set-layout
Jinwoo-H Oct 8, 2026
f25df70
Merge topo-c3 review fixes into topo-c5b-binding-split
Jinwoo-H Oct 8, 2026
050da56
Merge topo-c3 review fixes into topo-c5c-sleep-wake
Jinwoo-H Oct 8, 2026
6879d32
Merge topo-c3 review fixes into topo-c5d-ssh-import-export
Jinwoo-H Oct 8, 2026
b992c29
Merge topo-c3 review fixes into topo-c7-presentation-save
Jinwoo-H Oct 8, 2026
e87d8ec
Merge remote-tracking branch 'origin/main' into HEAD
Jinwoo-H Oct 8, 2026
b3fd831
Merge commit 'e87d8ecd830c800f333bd5dd84bafbd474049d4c' into HEAD
Jinwoo-H Oct 8, 2026
9af28ea
Merge commit 'b3fd83197580239cb34197afb3c55e4c0b558253' into HEAD
Jinwoo-H Oct 8, 2026
f21dc53
Merge commit '9af28ea702d59cd207cbc635f65b3f6c0fef7dec' into HEAD
Jinwoo-H Oct 8, 2026
a916b3b
Merge commit 'f21dc536c1ff25f313a04c6052543c8372025954' into HEAD
Jinwoo-H Oct 8, 2026
7169a5c
Merge commit 'a916b3bbb424a338aedd96c43b991dca0207930e' into HEAD
Jinwoo-H Oct 8, 2026
668262c
Merge commit '7169a5cb69520a1a982b0e06307aaa230572a3c9' into HEAD
Jinwoo-H Oct 8, 2026
4f5130e
Merge commit '668262c82b62ae8de7ff5de95d062e636c98a5bb' into HEAD
Jinwoo-H Oct 8, 2026
4357a99
Merge commit '4f5130ee56448a6938976edcf2025a86a362ac4d' into HEAD
Jinwoo-H Oct 8, 2026
68bd385
Merge commit '4357a9936a1f1b56dcf95fc8b4b5035beb38d60a' into HEAD
Jinwoo-H Oct 8, 2026
1903489
fix(terminal): a window save drops local residue of an SSH-homed work…
Jinwoo-H Oct 8, 2026
b0e8a0b
fix(terminal): a woken split's tab row stays on the pane it named
Jinwoo-H Oct 8, 2026
de32ad8
fix(terminal): a reply names no push for an unresolved owner and the …
Jinwoo-H Oct 8, 2026
229ebb3
test(e2e): record the woken split's pane order as unstable on main
Jinwoo-H Oct 8, 2026
a31bb10
test(e2e): count main's woken-split pane order over 15 parity runs
Jinwoo-H Oct 8, 2026
4da0ae0
Merge remote-tracking branch 'origin/topo-c7-presentation-save' into …
Jinwoo-H Oct 8, 2026
67903ba
refactor(terminal): home an uncatalogued repo in the one partition ho…
Jinwoo-H Oct 8, 2026
5ada98f
fix(terminal): a tab or pane the window creates is committed to main …
Jinwoo-H Oct 8, 2026
a61b1ab
fix(ssh): an SSH import keeps main's rows newer than the window's mer…
Jinwoo-H Oct 8, 2026
b6a77c6
fix(terminal): a paired client cannot adopt the PTY of a pane the des…
Jinwoo-H Oct 8, 2026
7c8c7d5
fix(terminal): a tab main creates shows the title its creator gave it
Jinwoo-H Oct 8, 2026
ba197a4
fix(terminal): main keeps one pane per PTY in a tab it loads
Jinwoo-H Oct 8, 2026
84f4647
test(e2e): seed the retention control record beside a tab main holds
Jinwoo-H Oct 8, 2026
9a257dc
fix(terminal): a new tab's pending entry publishes with the tab, in o…
Jinwoo-H Oct 8, 2026
e0a71be
Merge remote-tracking branch 'origin/topo-c7-presentation-save' into …
Jinwoo-H Oct 8, 2026
2540b63
Merge remote-tracking branch 'origin/topo-c7-presentation-save' into …
Jinwoo-H Oct 8, 2026
c050b26
Merge remote-tracking branch 'origin/topo-c7-presentation-save' into …
Jinwoo-H Oct 8, 2026
fcd89e0
fix(terminal): a new tab's placement carries its host-runtime fallbac…
Jinwoo-H Oct 8, 2026
ccb412e
test(terminal): pin window-save presentation fields and pruning the c…
Jinwoo-H Oct 8, 2026
bc7384c
fix(terminal): a push keeps the pane titles the window shows
Jinwoo-H Oct 8, 2026
3988406
fix(terminal): a save keeps main's scrollback for a pane the window h…
Jinwoo-H Oct 8, 2026
4d26079
test(persistence): pin main-side save steps the core keeps unchanged
Jinwoo-H Oct 8, 2026
f5a6ed3
fix(terminal): group sleeping records once per publish and apply, res…
Jinwoo-H Oct 8, 2026
27ef3bd
Merge remote-tracking branch 'origin/topo-c7-presentation-save' into …
Jinwoo-H Oct 8, 2026
f71d741
test(terminal): pin pushes and saves of main's direct session writers
Jinwoo-H Oct 8, 2026
0ec5723
fix(terminal): pass the mirror's active-leaf resolution its fields by…
Jinwoo-H Oct 8, 2026
d19658a
test(terminal): pin create-before-spawn persistence for launch, adopt…
Jinwoo-H Oct 8, 2026
b654e91
fix(terminal): a pane main's normalizer drops takes its PTY incarnati…
Jinwoo-H Oct 8, 2026
3d01e93
Merge remote-tracking branch 'origin/topo-c7-presentation-save' into …
Jinwoo-H Oct 8, 2026
13d0ca1
test(terminal): a worktree rename reaches the window before either id…
Jinwoo-H Oct 8, 2026
c8a5b91
Merge remote-tracking branch 'origin/topo-c7-presentation-save' into …
Jinwoo-H Oct 8, 2026
78971db
fix(terminal): a tab minted for a live agent PTY is bound in main at …
Jinwoo-H Oct 8, 2026
913f684
fix(persistence): apply old close records on the first write, as main…
Jinwoo-H Oct 8, 2026
01b70c3
Merge remote-tracking branch 'origin/topo-c7-presentation-save' into …
Jinwoo-H Oct 8, 2026
6a95d6e
fix(ssh): in a repo main fenced, a host pull keeps main's tabs and pa…
Jinwoo-H Oct 8, 2026
1766c44
Merge remote-tracking branch 'origin/topo-c7-presentation-save' into …
Jinwoo-H Oct 8, 2026
9a5c925
test(terminal): a push never puts back an SSH attachment the window c…
Jinwoo-H Oct 8, 2026
fc74b9a
fix(terminal): the window saves the tab-bar entry it gives a tab main…
Jinwoo-H Oct 8, 2026
8a02a45
fix(ssh): main moves an SSH workspace's rows an older build left in l…
Jinwoo-H Oct 8, 2026
0eed5ef
test: run the new real-SQLite restart tests in the Node runtime project
Jinwoo-H Oct 8, 2026
fe56085
fix(terminal): a window's own tabs advance an existing fence but neve…
Jinwoo-H Oct 8, 2026
5dcb46e
test(e2e): close and move scenarios now save main's fence and incarna…
Jinwoo-H Oct 8, 2026
4a850cf
fix(terminal): fence a window's topology commits, follow main's topol…
Jinwoo-H Oct 8, 2026
773388a
Merge remote-tracking branch 'origin/main' into topo-c7-presentation-…
Jinwoo-H Oct 8, 2026
2a50856
test(terminal): remove a repo through main's host-scoped removeProjec…
Jinwoo-H Oct 8, 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
55 changes: 26 additions & 29 deletions config/reliability-gates.jsonc

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion config/scripts/ci-shard-timings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7294,7 +7294,7 @@
"src/renderer/src/components/terminal-pane/terminal-layout-leaf-detach.test.ts": 62,
"src/renderer/src/components/terminal-pane/terminal-layout-leaf-ids.test.ts": 39,
"src/renderer/src/components/terminal-pane/terminal-layout-overlay-focus.test.tsx": 328,
"src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership-depth.test.ts": 103,
"src/shared/terminal-layout-pty-ownership-depth.test.ts": 103,
"src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership.test.ts": 111,
"src/renderer/src/components/terminal-pane/terminal-link-action-routing.test.ts": 1448,
"src/renderer/src/components/terminal-pane/terminal-link-activation.test.ts": 226,
Expand Down
4 changes: 4 additions & 0 deletions config/scripts/vitest-sqlite-runtime-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/runtime/agent-session-store-transactions.test.ts',
'src/main/runtime/agent-session-unsaved-creation-supersession.test.ts',
'src/main/runtime/automation-change-publication.test.ts',
'src/main/runtime/headless-editor-tab-close-persistence.test.ts',
'src/main/runtime/headless-terminal-tab-order-persistence.test.ts',
'src/main/runtime/host-terminal-close-persistence-durability.test.ts',
'src/main/runtime/orca-runtime-terminal-close-records.test.ts',
'src/main/runtime/orca-runtime-terminal-surface-close.test.ts',
Expand Down Expand Up @@ -201,6 +203,7 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/runtime/structured-agent-session-startup-reconcile.test.ts',
'src/main/runtime/structured-claude-attachment-grant.test.ts',
'src/main/runtime/structured-claude-pending-rewind.test.ts',
'src/main/legacy-terminal-surface-tombstones-write.test.ts',
'src/main/ssh-expired-lease-pane-readoption.test.ts',
'src/main/ssh-reattach-pane-cardinality.test.ts',
'src/main/ssh/orcad-migration-cutover-coordinator.test.ts',
Expand All @@ -211,6 +214,7 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/ssh/orcad-runtime-conversion.test.ts',
'src/main/ssh/orcad-unreachable-setup-release.test.ts',
'src/main/ssh/ssh-target-orcad-preflight.test.ts',
'src/main/terminal-topology-main-writer-pushes.test.ts',
'src/main/worktree-identity-persistence.test.ts',
'src/main/worktree-removal-close-records.test.ts',
'src/main/worktree-removal-session-partition-fencing.test.ts',
Expand Down
53 changes: 53 additions & 0 deletions src/main/agent-hooks/first-work-folder-rename.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { GlobalSettings } from '../../shared/global-settings-types'
import type { Repo } from '../../shared/repo-types'
import { getDefaultWorkspaceSession } from '../../shared/constants'
import {
TerminalTopologyPublisher,
type TerminalTopologyOwners
} from '../runtime/terminal-topology-publisher'
import {
renameWorktreeFolderOnFirstWork,
type FirstWorkFolderRenameDeps
Expand Down Expand Up @@ -97,4 +102,52 @@ describe('renameWorktreeFolderOnFirstWork', () => {
expect(await renameWorktreeFolderOnFirstWork(OLD_ID, 'fix-auth', deps)).toBe(false)
expect(deps.moveWorktree).not.toHaveBeenCalled()
})

it("tells the window about the rename before main pushes either id's topology", async () => {
const NEW_ID = 'repo1::/ws/worktree-creation-spinner'
const tab = (worktreeId: string) => ({
id: 'tab',
ptyId: 'pty',
worktreeId,
title: 'Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
})
const ownedBy = (worktreeId: string): TerminalTopologyOwners =>
new Map([
[
worktreeId,
{
hostId: 'local',
session: {
...getDefaultWorkspaceSession(),
tabsByWorktree: { [worktreeId]: [tab(worktreeId)] }
}
}
]
])
let owners = ownedBy(OLD_ID)
const order: string[] = []
const publisher = new TerminalTopologyPublisher(
() => owners,
(slice) => order.push(`${slice.worktreeId}: ${slice.tabs.length} tab(s)`)
)
publisher.snapshot()
order.length = 0

const deps = makeDeps({
migrateWorktreeIdentity: vi.fn(() => {
owners = ownedBy(NEW_ID)
publisher.markDirty()
}),
notifyWorktreeRenamed: vi.fn(() => order.push('renamed'))
})
await renameWorktreeFolderOnFirstWork(OLD_ID, 'worktree-creation-spinner', deps)
await Promise.resolve()

// The window re-keys its tabs first, so the old id's empty slice finds none to drop.
expect(order).toEqual(['renamed', `${OLD_ID}: 0 tab(s)`, `${NEW_ID}: 1 tab(s)`])
})
})
8 changes: 6 additions & 2 deletions src/main/ipc/pty-dead-owner-respawn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ describe('registerPtyHandlers', () => {
session = next
}),
flushOrThrow: vi.fn(),
retirePtyBinding: vi.fn(async () => true),
persistPtyBinding: vi.fn(),
getFolderWorkspace: vi.fn(() => undefined),
getFolderWorkspaces: vi.fn(() => []),
Expand Down Expand Up @@ -171,8 +172,9 @@ describe('registerPtyHandlers', () => {
'inc-proven-absent-owner',
{ hostExitConfirmed: true }
)
expect(store.setWorkspaceSession).toHaveBeenCalledOnce()
expect(store.flushOrThrow).toHaveBeenCalledOnce()
// The pane, its binding and its row stay for the fresh spawn's bind to swap.
expect(store.setWorkspaceSession).not.toHaveBeenCalled()
expect(store.retirePtyBinding).not.toHaveBeenCalled()
})
it('does not poll after the routed provider confirms absence', async () => {
const worktreeId = 'repo-1::/tmp/probe-blip-owner'
Expand Down Expand Up @@ -233,6 +235,7 @@ describe('registerPtyHandlers', () => {
session = next
}),
flushOrThrow: vi.fn(),
retirePtyBinding: vi.fn(async () => true),
persistPtyBinding: vi.fn(),
getFolderWorkspace: vi.fn(() => undefined),
getFolderWorkspaces: vi.fn(() => []),
Expand Down Expand Up @@ -357,6 +360,7 @@ describe('registerPtyHandlers', () => {
session = next
}),
flushOrThrow: vi.fn(),
retirePtyBinding: vi.fn(async () => true),
persistPtyBinding: vi.fn(),
getFolderWorkspace: vi.fn(() => undefined),
getFolderWorkspaces: vi.fn(() => []),
Expand Down
2 changes: 0 additions & 2 deletions src/main/ipc/pty-pane-claim-arbitration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ describe('registerPtyHandlers', () => {
leafId,
ptyId: expect.any(String),
incarnationId: expect.any(String),
hostAdmittedMembership: true,
origin: 'spawn'
})
})
Expand Down Expand Up @@ -519,7 +518,6 @@ describe('registerPtyHandlers', () => {
leafId,
ptyId: 'pty-shared',
startupCwd: '/tmp',
hostAdmittedMembership: true,
origin: 'spawn'
})
})
Expand Down
22 changes: 11 additions & 11 deletions src/main/ipc/pty-pane-reservation-settlement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ describe('registerPtyHandlers', () => {
session = next
}),
flushOrThrow: vi.fn(),
retirePtyBinding: vi.fn(async () => true),
persistPtyBinding: vi.fn(),
upsertSshRemotePtyLease: vi.fn(),
supersedeSshRemotePtyLeasesForBoundPane: vi.fn(),
Expand Down Expand Up @@ -184,16 +185,16 @@ describe('registerPtyHandlers', () => {
expect(remoteSpawn.mock.calls[1]?.[0]).toMatchObject({
command: 'codex resume exact-dead-ssh-provider-session'
})
expect(store.setWorkspaceSession).toHaveBeenCalledWith(expect.anything(), hostId)
expect(store.persistPtyBinding).toHaveBeenCalledWith(
expect.objectContaining({
worktreeId,
tabId,
leafId,
ptyId: freshPtyId
}),
hostId
)
expect(store.setWorkspaceSession).not.toHaveBeenCalled()
// The fresh spawn replaces the absent owner, so its bind swaps the binding naming it.
const [input, persistHostId] = store.persistPtyBinding.mock.calls[0] ?? []
expect(persistHostId).toBe(hostId)
expect(typeof input === 'function' ? input() : input).toMatchObject({
worktreeId,
tabId,
leafId,
ptyId: freshPtyId
})
} finally {
unregisterSshPtyProvider(connectionId)
}
Expand Down Expand Up @@ -542,7 +543,6 @@ describe('registerPtyHandlers', () => {
tabId: 'tab-remote',
leafId,
ptyId: 'ssh:ssh-1@@relay-pty',
hostAdmittedMembership: true,
origin: 'spawn'
},
'ssh:ssh-1'
Expand Down
8 changes: 5 additions & 3 deletions src/main/ipc/pty-persisted-incarnation-repair.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ describe('registerPtyHandlers', () => {
session = next
}),
flushOrThrow: vi.fn(),
retirePtyBinding: vi.fn(async () => true),
persistPtyBinding: vi.fn(),
getFolderWorkspace: vi.fn(() => ({
id: 'dead-persisted-owner',
Expand Down Expand Up @@ -350,7 +351,7 @@ describe('registerPtyHandlers', () => {
sessionId: 'pty-dead-persisted-owner',
command: undefined
})
expect(store.setWorkspaceSession).toHaveBeenCalledOnce()
expect(store.setWorkspaceSession).not.toHaveBeenCalled()
expect(runtime.onPtyExit).toHaveBeenCalledWith(
'pty-dead-persisted-owner',
-1,
Expand All @@ -374,8 +375,8 @@ describe('registerPtyHandlers', () => {
expect(providerSpawn.mock.calls[1]?.[0]).toMatchObject({
command: 'codex resume exact-dead-provider-session'
})
expect(store.setWorkspaceSession).toHaveBeenCalledOnce()
expect(store.flushOrThrow).toHaveBeenCalledOnce()
// The pane and its binding stay for the fresh spawn's bind to swap.
expect(store.setWorkspaceSession).not.toHaveBeenCalled()
expect(runtime.onPtyExit).toHaveBeenCalledWith(
'pty-dead-persisted-owner',
-1,
Expand Down Expand Up @@ -441,6 +442,7 @@ describe('registerPtyHandlers', () => {
session = next
}),
flushOrThrow: vi.fn(),
retirePtyBinding: vi.fn(async () => true),
persistPtyBinding: vi.fn(),
getFolderWorkspace: vi.fn(() => undefined),
getFolderWorkspaces: vi.fn(() => []),
Expand Down
1 change: 0 additions & 1 deletion src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@ describe('registerPtyHandlers', () => {
tabId: 'tab-remote',
leafId,
ptyId: 'ssh:ssh-reattach-ok@@relay-pty',
hostAdmittedMembership: true,
origin: 'reattach'
},
'ssh:ssh-reattach-ok'
Expand Down
14 changes: 10 additions & 4 deletions src/main/ipc/pty-spawn-placement-threading.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ describe('pty spawn placement threading', () => {
noteTerminalSpawnCommand: vi.fn(),
onPtySpawned: vi.fn(),
onPtyExit: vi.fn(),
onPtyData: vi.fn()
onPtyData: vi.fn(),
// Names a push only once the binding is written.
settleTerminalTopology: vi.fn(() => (store.persistPtyBinding.mock.calls.length > 0 ? 9 : 0))
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: spawn reads only the window, runtime and store members these fakes define.
const args = [
Expand All @@ -104,8 +106,8 @@ describe('pty spawn placement threading', () => {
}
}

async function ipcSpawn(extra: Record<string, unknown>): Promise<void> {
await handlers.get('pty:spawn')!(null, {
async function ipcSpawn(extra: Record<string, unknown>): Promise<unknown> {
return await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: '/tmp',
Expand Down Expand Up @@ -142,6 +144,11 @@ describe('pty spawn placement threading', () => {
])
})

it('replies with the publishSeq of the push that carries the binding', async () => {
register()
await expect(ipcSpawn({ placement: SPLIT })).resolves.toMatchObject({ publishSeq: 9 })
})

it('drops a malformed or future-kind placement instead of failing the spawn', async () => {
const { store } = register()
await ipcSpawn({ placement: { kind: 'floating-window', anchor: 'x' } })
Expand Down Expand Up @@ -170,7 +177,6 @@ describe('pty spawn placement threading', () => {
leafId: LEAF,
ptyId: expect.any(String),
incarnationId: expect.any(String),
hostAdmittedMembership: true,
placement: SPLIT,
origin: 'spawn'
})
Expand Down
64 changes: 59 additions & 5 deletions src/main/ipc/pty/ipc/leaf-move.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { agentHookServer } from '../../../agent-hooks/server'
import type { Store } from '../../../persistence'
import type { OrcaRuntimeService } from '../../../runtime/orca-runtime'
import type { TerminalLeafMoveResult } from '../../../../shared/terminal-leaf-move'
import { commitLeafMoveAndRekey } from './leaf-move'
import { setPtyHostBindings } from '../../pty-host-bindings'
import { commitLeafMoveAndRekey, installPtyLeafMoveIpcHandler } from './leaf-move'

const LEAF = '22222222-2222-4222-8222-222222222222'
const request = {
Expand All @@ -14,19 +15,22 @@ const request = {
ptyId: 'pty-agent'
}

function deps(result: TerminalLeafMoveResult) {
function deps(result: TerminalLeafMoveResult, homeHostId: string | null = 'ssh:target-1') {
const rekeyWorkerTerminalResourcePaneKey = vi.fn(() => 1)
const moveTerminalLeafToNewTab = vi.fn(async () => result)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the move reads only this Store method.
const store = { moveTerminalLeafToNewTab: vi.fn(async () => result) } as unknown as Store
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the move reads only the orchestration DB accessor.
const store = { moveTerminalLeafToNewTab } as unknown as Store
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the move reads only the home resolver and the orchestration DB accessor.
const runtime = {
getTerminalTopologyHomeHostId: () => homeHostId,
getExistingOrchestrationDb: () => ({ rekeyWorkerTerminalResourcePaneKey })
} as unknown as OrcaRuntimeService
return { store, runtime, rekeyWorkerTerminalResourcePaneKey }
return { store, runtime, rekeyWorkerTerminalResourcePaneKey, moveTerminalLeafToNewTab }
}

afterEach(() => {
vi.restoreAllMocks()
setPtyHostBindings({})
})

describe('pty:moveLeafToNewTab', () => {
Expand Down Expand Up @@ -67,4 +71,54 @@ describe('pty:moveLeafToNewTab', () => {
expect(transfer).not.toHaveBeenCalled()
expect(rekeyWorkerTerminalResourcePaneKey).not.toHaveBeenCalled()
})

it("writes only the worktree's home partition", async () => {
vi.spyOn(agentHookServer, 'transferPaneAuthority').mockImplementation(() => {})
const { store, runtime, moveTerminalLeafToNewTab } = deps({ status: 'moved', ptyId: null })

await commitLeafMoveAndRekey({ store, runtime }, request)

expect(moveTerminalLeafToNewTab).toHaveBeenCalledExactlyOnceWith(request, 'ssh:target-1')
})

it('writes nothing when the home is unresolved', async () => {
const { store, runtime, moveTerminalLeafToNewTab } = deps(
{ status: 'moved', ptyId: null },
null
)

await expect(commitLeafMoveAndRekey({ store, runtime }, request)).resolves.toEqual({
status: 'refused',
reason: 'home_unresolved'
})
expect(moveTerminalLeafToNewTab).not.toHaveBeenCalled()
})

it('replies with the publishSeq of the push that carries the move', async () => {
vi.spyOn(agentHookServer, 'transferPaneAuthority').mockImplementation(() => {})
const handlers = new Map<string, (event: never, args: unknown) => unknown>()
setPtyHostBindings({
ipc: {
handle: (channel, listener) => handlers.set(channel, listener),
on: () => {},
removeHandler: () => {},
removeAllListeners: () => {}
}
})
const { store, runtime } = deps({ status: 'moved', ptyId: 'pty-agent' })
const settleTerminalTopology = vi.fn(() => 12)
installPtyLeafMoveIpcHandler({
store,
runtime: Object.assign(runtime, { settleTerminalTopology })
})

// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handler never reads the IPC event.
const reply = handlers.get('pty:moveLeafToNewTab')!({} as never, request)
await expect(reply).resolves.toEqual({
status: 'moved',
ptyId: 'pty-agent',
publishSeq: 12
})
expect(settleTerminalTopology).toHaveBeenCalledWith(request.worktreeId)
})
})
10 changes: 8 additions & 2 deletions src/main/ipc/pty/ipc/leaf-move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ export async function commitLeafMoveAndRekey(
if (!deps.store) {
return { status: 'not_held' }
}
const result = await deps.store.moveTerminalLeafToNewTab(request)
// One home per worktree; an unresolved one is unverifiable, so nothing is written.
const hostId = deps.runtime?.getTerminalTopologyHomeHostId(request.worktreeId)
if (!hostId) {
return { status: 'refused', reason: 'home_unresolved' }
}
const result = await deps.store.moveTerminalLeafToNewTab(request, hostId)
if (result.status !== 'moved') {
return result
}
Expand Down Expand Up @@ -53,6 +58,7 @@ export function installPtyLeafMoveIpcHandler(deps: {
if (!isTerminalLeafMoveRequest(args)) {
return { status: 'refused', reason: 'invalid_request' } satisfies TerminalLeafMoveResult
}
return commitLeafMoveAndRekey(deps, args)
const result = await commitLeafMoveAndRekey(deps, args)
return { ...result, publishSeq: deps.runtime?.settleTerminalTopology(args.worktreeId) }
})
}
Loading
Loading