Layout-owner builds: a phone's tab move is always the server's own; no ghost editor tabs after restart (#31925)

* Layout groundwork (switched off): main derives agent tab titles from its own hook status

With the layout-owner switch on, main gives agent tabs their generated titles from
the hook-server status it owns, on the desktop, headless serve and orcad: one
renameTabs per status batch, so one write and one frame. It keeps the window's
rules (setting, custom and quick-command titles, first title wins, dispatch labels).
The window stops sending generated titles for tabs main holds; a paired server's
tab, or one main has no tab-bar entry for, is still titled by the window as today.

The title rules move to src/shared (agent-row-primary-text, and the new
agent-generated-tab-title-rules) so both sides share them; switch-off behaviour
is unchanged.

* feat(layout): main applies a phone's or paired client's tab move itself, and a tab move names the tab it follows so a stale index can't misplace it (layout-owner build, L193)

* fix(layout): a move in a group showing a tab main lacks goes the window's way, a twin's agent tab keeps its window title, and orcad subscribes titles through the store attach (layout-owner build)

* chore(layout): main's tab move reaches the old-client translation through its compat entry

* chore(layout): mark the window-only-row move fallback as bridge code (ledger L280)

* test(layout): attaching the hook store follows its rows for main's tab titles once

* chore(layout): the old-client move translation is a compat entry of its own, with no cyclic re-export

* fix(layout): every tab a group shows is main's, so a client's tab move never falls back to the window (layout-owner build)

- Main moves a client's tab itself in a workspace it owns; a row it holds no tab for (a combined
  diff) keeps no place, and an order must still name each row shown once (L280).
- The window publishes an editor row under its tab id only; a file no group shows gets none.
- Main's layout load gives a saved browser record with no tab its tab, as the window's hydration
  repair did, and the window no longer mints one for a workspace main owns.
- Main's layout load drops the saved tabs the window's restore drops (view-less diff, conflict and
  check tabs, old html previews), through one shared rule; the next save writes it back (L286).
- Main lists conflict-review and check-details tabs as the plain file rows the window lists, so
  older clients keep them once the window's rows are gone (L287).

* fix(lint): drop an import the shared restore rule left unused

* test: import the move translation through the folder's public path

* fix(layout): review follow-ups for the move fallback removal (layout-owner build)

- A transient tab stays at load while a view is held for its file, as the window's frame build
  keeps it, not only for its own tab id.
- The repaired browser tab goes to the saved active group only while it holds a tab, as
  selectHydratedActiveGroupId picks it; tests that the id is stable across loads and restarts,
  that a window-owned build restores it once, and that a paired server's record keeps the window's
  repair.
- Two switch-on tests keyed editor drafts by path; drafts are keyed by editorFileKey (an owned id
  in a layout-owner build), so they now use it.

* fix(layout): the repaired browser tab skips a saved active group this load empties

* test(layout): directly dialed SSH browser record is main's; a server's SSH workspace keeps the window repair

* fix(layout): drop view-less editor tabs only in loads that carry main's views, so main's own edits keep a live diff

* test(layout): a direct SSH partition gets the same browser repair and drop with no host contact
This commit is contained in:
Jinwoo Hong
2026-10-22 05:26:12 -04:00
committed by GitHub
parent 041183abae
commit ea60ec5d7c
21 changed files with 781 additions and 66 deletions
@@ -82,37 +82,73 @@ describe("a client's tab move in a workspace main owns", () => {
expect(groups().map((entry) => entry.tabOrder)).toEqual([[TAB_ID], ['a', 'b']])
})
it('leaves a move in a group showing a tab main lacks to the window, as today', async () => {
const { runtime, moveSessionTab, groups, group } = await setup({ window: true })
/** The client was shown a row main holds no tab for (drawn before main's frame) at the group's front. */
function showUnheldRow(runtime: unknown): void {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the move reads only this member, which OrcaRuntimeService implements.
const rowsOf = runtime as unknown as {
const rowsOf = runtime as {
toMobileSessionTabsResult: (shown: unknown) => {
tabGroups?: { id: string; tabOrder: string[] }[]
}
}
const real = rowsOf.toMobileSessionTabsResult.bind(runtime)
// The window shows a diff tab main's layout does not hold.
rowsOf.toMobileSessionTabsResult = (shown) => {
const rows = real(shown)
return {
...rows,
tabGroups: rows.tabGroups?.map((entry) => ({
...entry,
tabOrder: [...entry.tabOrder, 'diff-1']
tabOrder: ['diff-1', ...entry.tabOrder]
}))
}
}
const before = groups()
}
// Why (L280): main moves it itself, never by the window's path; a row it holds no tab for keeps
// no place of its own, and the client's indices count it as the client saw it.
it('moves in a group showing a row main holds no tab for, without asking the window', async () => {
const { runtime, moveSessionTab, groups, group } = await setup({ window: true })
showUnheldRow(runtime)
// Shown: diff-1, terminal, a, b. Index 1 is before the terminal.
await runtime.moveMobileSessionTab(WT, {
kind: 'move-to-group',
tabId: 'b',
targetGroupId: group,
index: 1
})
expect(groups().map((entry) => entry.tabOrder)).toEqual([['b', TAB_ID, 'a']])
await runtime.moveMobileSessionTab(WT, {
kind: 'reorder',
tabId: 'a',
targetGroupId: group,
index: 0
tabOrder: ['a', 'diff-1', 'b', TAB_ID]
})
expect(groups().map((entry) => entry.tabOrder)).toEqual([['a', 'b', TAB_ID]])
expect(moveSessionTab).not.toHaveBeenCalled()
})
expect(moveSessionTab).toHaveBeenCalledOnce()
it('refuses an order that does not name exactly the rows shown, moving nothing', async () => {
const { runtime, moveSessionTab, groups, group } = await setup({ window: true })
showUnheldRow(runtime)
const before = groups()
for (const tabOrder of [
['a', 'b', TAB_ID],
['a', 'other', 'b', TAB_ID],
['a', 'diff-1', 'diff-1', 'b', TAB_ID]
]) {
await expect(
runtime.moveMobileSessionTab(WT, {
kind: 'reorder',
tabId: 'a',
targetGroupId: group,
tabOrder
})
).rejects.toThrow('invalid_tab_order')
}
expect(groups()).toEqual(before)
expect(moveSessionTab).not.toHaveBeenCalled()
})
it("refuses with today's codes and moves nothing", async () => {
@@ -8,7 +8,6 @@ import type {
RuntimeMobileSessionTabsSnapshot
} from '../../../shared/runtime-types'
import { translateSessionTabMove } from '../../../shared/workspace-layout/old-client/old-client-method-translation'
import { resolveSessionTabRow } from '../../../shared/workspace-layout/old-client/old-client-session-tab-rows'
import { startedMainTerminalSurfaces } from '../terminal/surface/main-terminal-surfaces'
import { OrcaRuntimeWithMainLayoutRenames } from './main-layout-renames'
@@ -20,8 +19,8 @@ export abstract class OrcaRuntimeWithMainLayoutTabMoves extends OrcaRuntimeWithM
): ReturnType<OrcaRuntimeWithPruneMobileSessionTabGroupLayout['toMobileSessionTabsResult']>
/**
* The move in main's layout, read against the rows the client was shown. Null when main does not
* own the workspace or cannot read the move: today's path moves it, or refuses it with its codes.
* The move in main's layout, checked against the rows the client was shown and refused with
* today's codes. Null for a workspace main does not own: today's path moves it.
*/
protected async moveTabInMainLayout(
worktreeId: string,
@@ -41,18 +40,14 @@ export abstract class OrcaRuntimeWithMainLayoutTabMoves extends OrcaRuntimeWithM
if (!workspace) {
throw new Error('tab_not_found')
}
const rows = this.toMobileSessionTabsResult(shown)
// A group showing a tab main's layout lacks (a diff, a preview) moves as today, and so does a
// move main cannot read as one: today's path checks it against those rows and answers.
// Bridge (ledger L280): goes once main's layout holds every tab a group shows.
const groupsHeld = (rows.tabGroups ?? [])
.filter((group) => group.id === move.targetGroupId || group.tabOrder.includes(move.tabId))
.every((group) => group.tabOrder.every((rowId) => resolveSessionTabRow(workspace, rowId)))
const translation = groupsHeld
? translateSessionTabMove(worktreeId, workspace, rows, move)
: null
if (!translation?.ok) {
return null
const translation = translateSessionTabMove(
worktreeId,
workspace,
this.toMobileSessionTabsResult(shown),
move
)
if (!translation.ok) {
throw new Error(translation.code)
}
for (const command of translation.commands) {
if (command.type !== 'moveTab' && command.type !== 'splitGroup') {
@@ -0,0 +1,18 @@
import { describe, expect, it, vi } from 'vitest'
import { runtimeLayoutLoadContext } from './workspace-layout-projection'
vi.mock('../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
describe('runtimeLayoutLoadContext', () => {
// L286: a partition no view was opened in since the restart still loads as views-aware, so its
// saved view-less diff tabs are dropped, not published.
it('carries views for a partition that holds none yet', () => {
const context = runtimeLayoutLoadContext(
{ getPtyBindingGeneration: () => undefined, getLayoutEditorViews: () => undefined },
'local'
)
expect(context.editorViewsOf).toBeDefined()
expect(context.editorViewsOf?.('repo-1::/w')).toBeUndefined()
})
})
@@ -47,7 +47,8 @@ export function runtimeLayoutLoadContext(
...(generationOf
? { ptyGenerationOf: (leafId: string) => generationOf.call(store, hostId, leafId) }
: {}),
...(views ? { editorViewsOf: (key: string) => views[key] } : {})
// A partition with no view yet still loads as one whose views are known (none held).
...(store.getLayoutEditorViews ? { editorViewsOf: (key: string) => views?.[key] } : {})
}
}
@@ -1,4 +1,4 @@
// Diff and markdown-preview rows: main's projection of its layout model (the window's session plus
// Diff, markdown-preview, conflict-review and check-details rows: main's projection of its layout model (the window's session plus
// the editor views the window publishes) must equal the rows the window builds for the same tabs.
import { describe, expect, it, vi } from 'vitest'
@@ -98,6 +98,25 @@ const files: OpenFile[] = [
diffSource: 'branch',
isDirty: false
},
// L287: a conflict review and a check's details; older clients list both as plain file rows.
{
id: `${GIT_KEY}::conflict-review`,
filePath: '/Users/dev/orca',
relativePath: 'Merge conflicts',
worktreeId: GIT_KEY,
language: 'plaintext',
mode: 'conflict-review',
isDirty: false
},
{
id: `${GIT_KEY}::check-details::build`,
filePath: '/Users/dev/orca',
relativePath: 'build',
worktreeId: GIT_KEY,
language: 'plaintext',
mode: 'check-details',
isDirty: false
},
{
id: `${GIT_KEY}::all-diffs::uncommitted`,
filePath: '/Users/dev/orca',
@@ -121,7 +140,10 @@ function scenario(activeFileId: string) {
tabs: files.map((file) => ({
id: tabIdOf(file),
entityId: file.id,
kind: file.mode === 'diff' ? ('diff' as const) : ('editor' as const),
kind:
file.mode === 'diff' || file.mode === 'conflict-review' || file.mode === 'check-details'
? file.mode
: ('editor' as const),
entry: { color: file.mode === 'diff' ? '#00ff00' : null }
}))
}
@@ -177,7 +199,7 @@ function published(snapshot: Pick<RuntimeMobileSessionTabsSnapshot, 'tabs' | 'ta
return { tabs: snapshot.tabs, tabGroups: snapshot.tabGroups }
}
describe("main's diff and markdown-preview rows", () => {
describe("main's diff, markdown-preview and review rows", () => {
it.each([
['a diff', `${GIT_KEY}::diff::unstaged::src/app.ts`],
['a markdown preview', `markdown-preview::${README}`]
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
import { buildMobileSessionTabSnapshots } from './sync-runtime-graph/mobile-session-snapshots'
import { makeState } from './sync-runtime-graph-test-harness'
import type { AppState } from '../store/types'
@@ -498,15 +499,28 @@ describe('buildMobileSessionTabSnapshots', () => {
const snapshot = buildMobileSessionTabSnapshots(state)[0]
// Layout-owner build (L280): a row goes out under its tab id only, so a diff no group shows
// gets no row; the same-path edit tab keeps its own.
const diffRows = ORCA_LAYOUT_OWNER_IS_MAIN
? []
: [
{
type: 'file',
id: diffId,
mode: 'diff',
diffSource: 'unstaged',
relativePath: 'src/app.ts'
}
]
expect(snapshot?.tabs).toMatchObject([
{ type: 'file', id: 'editor-tab-1', mode: 'edit', relativePath: 'src/app.ts' },
{ type: 'file', id: diffId, mode: 'diff', diffSource: 'unstaged', relativePath: 'src/app.ts' }
...diffRows
])
expect(snapshot?.tabGroups).toEqual([
{
id: 'group-1',
activeTabId: 'editor-tab-1',
tabOrder: ['editor-tab-1', diffId],
tabOrder: ['editor-tab-1', ...(ORCA_LAYOUT_OWNER_IS_MAIN ? [] : [diffId])],
recentTabIds: ['editor-tab-1']
}
])
@@ -751,6 +765,12 @@ describe('buildMobileSessionTabSnapshots', () => {
})
)[0]
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
// Layout-owner build (L280): a file no group shows as a tab gets no row.
expect(terminalSnapshot?.tabs ?? []).toEqual([])
expect(editorSnapshot?.tabs ?? []).toEqual([])
return
}
expect(terminalSnapshot?.tabs).toMatchObject([
{ type: 'file', id: fileId, relativePath: 'src/app.ts', isActive: false }
])
@@ -34,6 +34,7 @@ import { buildMobileBrowserTab } from './mobile-session-browser-tabs'
import type { MobileSessionPublicationInputs } from './types'
import { canReuseMobileSessionSnapshot } from './mobile-session-capture'
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
export function buildMobileSessionTabSnapshots(
state: AppState,
@@ -172,7 +173,9 @@ export function buildMobileSessionTabSnapshots(
fallbackEditorTabs.push({ tabId: fallbackTab.id, groupId: unifiedTab.groupId })
emittedEditorTabIds.add(unifiedTab.id)
}
for (const file of openFilesForWorktree.values()) {
// Switch point: a row goes out under its tab id only, the id main's layout resolves (L280).
const tablessFiles = ORCA_LAYOUT_OWNER_IS_MAIN ? [] : openFilesForWorktree.values()
for (const file of tablessFiles) {
if (!isMobilePublishableOpenFile(file) || emittedEditorFileIds.has(file.id)) {
continue
}
@@ -10,6 +10,7 @@ import { findWorkspace } from '../browser-page-records'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../../shared/layout-owner-switch'
import { sendBrowserLayoutCommand } from './browser-layout-owner-commands'
import { selectFolderWorkspaces } from '../../workspace-record-selectors'
import { runtimeEnvironmentIdUnlessTwin } from '@/lib/resolve-owner'
export function createBrowserHydrationActions(
set: BrowserSliceSet,
@@ -60,6 +61,10 @@ export function createBrowserHydrationActions(
// desktop was hosting pages, and the runtime only hands them back once it sees an attach.
void ensureBrowserClientHostsForRestoredPages(state)
for (const [worktreeId, browserTabs] of Object.entries(state.browserTabsByWorktree)) {
// Switch point: main's layout load gives a tabless record its tab; its frame draws it.
if (ORCA_LAYOUT_OWNER_IS_MAIN && !runtimeEnvironmentIdUnlessTwin(state, worktreeId)) {
continue
}
for (const bt of browserTabs) {
const exists = (state.unifiedTabsByWorktree[worktreeId] ?? []).some(
(t) => t.contentType === 'browser' && t.entityId === bt.id
@@ -71,6 +71,108 @@ describe('hydrateBrowserSession', () => {
expect(s.activeBrowserTabId).toBe('browser-1')
})
// L280: in a layout-owner build main's layout load gives a tabless record its tab, so the window
// mints none of its own (an id main could not resolve); a window-owned build repairs it here.
// A directly dialed SSH workspace is main's too (its layout loads under its `ssh:` host).
it.each([
['local', {}, undefined],
[
'directly dialed SSH',
{ connectionId: 'box', executionHostId: 'ssh:box' as const },
'ssh:box' as const
]
])(
'leaves a tabless browser record to main in a layout-owner build, else gives it a tab (%s)',
(_name, repoHost, hostId) => {
const store = createTestStore()
const validWt = 'repo1::/path/wt1'
store.setState({
repos: [
{
id: 'repo1',
path: '/repo1',
displayName: 'Repo 1',
badgeColor: '#000',
addedAt: 0,
...repoHost
}
],
worktreesByRepo: {
repo1: [makeWorktree({ id: validWt, repoId: 'repo1', path: '/path/wt1', hostId })]
},
activeWorktreeId: validWt
})
store.getState().hydrateBrowserSession({
activeRepoId: 'repo1',
activeWorktreeId: validWt,
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
browserTabsByWorktree: {
[validWt]: [
makeBrowserTab({ id: 'browser-1', worktreeId: validWt, url: 'https://example.com' })
]
}
})
const browserTabs = (store.getState().unifiedTabsByWorktree[validWt] ?? []).filter(
(tab) => tab.contentType === 'browser'
)
expect(browserTabs.map((tab) => tab.entityId)).toEqual(
ORCA_LAYOUT_OWNER_IS_MAIN ? [] : ['browser-1']
)
}
)
// Main repairs only the workspaces it is asked about (sendLayoutApply's owner test); a paired
// server's workspace keeps today's window repair in every build.
it.each([
['workspace', {}],
['SSH workspace', { connectionId: 'vm1' }]
])("still gives a paired server's tabless browser record a tab (%s)", (_name, sshTarget) => {
const store = createTestStore()
const serverWt = 'repo1::/srv/wt1'
store.setState({
repos: [
{
id: 'repo1',
path: '/srv',
displayName: 'Server repo',
badgeColor: '#000',
addedAt: 0,
executionHostId: 'runtime:env-1',
...sshTarget
}
],
worktreesByRepo: {
repo1: [
makeWorktree({ id: serverWt, repoId: 'repo1', path: '/srv/wt1', hostId: 'runtime:env-1' })
]
},
activeWorktreeId: serverWt
})
store.getState().hydrateBrowserSession({
activeRepoId: 'repo1',
activeWorktreeId: serverWt,
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
browserTabsByWorktree: {
[serverWt]: [
makeBrowserTab({ id: 'browser-1', worktreeId: serverWt, url: 'https://example.com' })
]
}
})
expect(
(store.getState().unifiedTabsByWorktree[serverWt] ?? [])
.filter((tab) => tab.contentType === 'browser')
.map((tab) => tab.entityId)
).toEqual(['browser-1'])
})
it('synthesizes a page for a browser workspace whose persisted page list is empty', () => {
// Why: session salvage drops a corrupt page by rebuilding the array, so the
// key survives holding []. Treating that as "has pages" restores a workspace
@@ -1,4 +1,4 @@
import type { Tab, TabContentType, TabGroup } from '../../../../shared/tab-types'
import type { Tab, TabGroup } from '../../../../shared/tab-types'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types'
import { createBrowserUuid } from '@/lib/browser-uuid'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
@@ -163,11 +163,7 @@ export function updateGroup(groups: TabGroup[], updated: TabGroup): TabGroup[] {
return groups.map((g) => (g.id === updated.id ? updated : g))
}
export function isTransientEditorContentType(contentType: TabContentType): boolean {
return (
contentType === 'diff' || contentType === 'conflict-review' || contentType === 'check-details'
)
}
export { isTransientEditorContentType } from '../../../../shared/workspace-layout/saved-editor-tab-restore'
export function getPersistedEditFileIdsByWorktree(
session: WorkspaceSessionState
@@ -0,0 +1,120 @@
// L286: a layout-owner build's main drops view-less saved editor tabs at load and writes that back.
// A window-owned build reading the written session must restore exactly what it did before.
import { describe, expect, it, vi } from 'vitest'
import { getDefaultWorkspaceSession } from '../../../../shared/constants'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import type { Tab, TabContentType } from '../../../../shared/tab-types'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types'
import { loadWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-load'
import { nameBasedLoadContext } from '../../../../shared/workspace-layout/workspace-layout-minted-ids'
import { saveWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-save'
import { buildHydratedTabState } from './tabs-hydration'
const layoutOwner = vi.hoisted(() => ({ isMain: true }))
vi.mock('../../../../shared/layout-owner-switch', () => ({
get ORCA_LAYOUT_OWNER_IS_MAIN() {
return layoutOwner.isMain
}
}))
const KEY = 'repo-1::/w'
function entry(id: string, contentType: TabContentType, entityId: string, sortOrder: number): Tab {
return {
id,
entityId,
groupId: 'g1',
worktreeId: KEY,
contentType,
label: id,
customLabel: null,
color: null,
sortOrder,
createdAt: 1
}
}
function savedSession(): WorkspaceSessionState {
const tabs = [
entry('ghost-diff', 'diff', '/w/gone.ts', 0),
entry('ed-a', 'editor', '/w/a.ts', 1),
entry('ghost-review', 'conflict-review', '/w/conflict', 2),
entry('kept-diff', 'diff', '/w/b.ts', 3),
entry('ed-b', 'editor', '/w/b.ts', 4)
]
return {
...getDefaultWorkspaceSession(),
unifiedTabs: { [KEY]: tabs },
tabGroups: {
[KEY]: [{ id: 'g1', worktreeId: KEY, activeTabId: 'ed-a', tabOrder: tabs.map((t) => t.id) }]
},
openFilesByWorktree: {
[KEY]: ['/w/a.ts', '/w/b.ts'].map((filePath) => ({
filePath,
relativePath: filePath.slice(3),
worktreeId: KEY,
language: 'typescript'
}))
}
}
}
describe("a window-owned build reading a session main's load rewrote", () => {
it('restores the same tabs and groups as from the session before the rewrite', () => {
layoutOwner.isMain = true
const rewritten = saveWorkspaceLayout(
// As main's command applies load it after a restart: its views carried, none held.
loadWorkspaceLayout(LOCAL_EXECUTION_HOST_ID, savedSession(), {
...nameBasedLoadContext(),
editorViewsOf: () => undefined
})
)
expect(rewritten.unifiedTabs?.[KEY]).toHaveLength(3)
layoutOwner.isMain = false
const restore = (session: WorkspaceSessionState) => {
const state = buildHydratedTabState(session, new Set([KEY]))
return {
tabs: state.unifiedTabsByWorktree[KEY]?.map((tab) => [tab.id, tab.contentType]),
groups: state.groupsByWorktree[KEY]?.map((group) => group.tabOrder)
}
}
expect(restore(rewritten)).toEqual(restore(savedSession()))
expect(restore(rewritten).groups).toEqual([['ed-a', 'kept-diff', 'ed-b']])
})
// L280: main's load also gives a tabless browser record its tab and saves it; a window-owned
// build then restores that one tab and has nothing left to repair.
it("restores the browser tab main's load added, once", () => {
layoutOwner.isMain = true
const withRecord: WorkspaceSessionState = {
...savedSession(),
browserTabsByWorktree: {
[KEY]: [
{
id: 'browser-1',
worktreeId: KEY,
url: 'https://example.com',
title: 'Example',
loading: false,
faviconUrl: null,
canGoBack: false,
canGoForward: false,
loadError: null,
createdAt: 5
}
]
}
}
const loaded = loadWorkspaceLayout(LOCAL_EXECUTION_HOST_ID, withRecord, nameBasedLoadContext())
const mainId = loaded.layout.workspaces[KEY]!.tabs.find((tab) => tab.kind === 'browser')!.id
const rewritten = saveWorkspaceLayout(loaded)
layoutOwner.isMain = false
const browserTabs = buildHydratedTabState(rewritten, new Set([KEY])).unifiedTabsByWorktree[
KEY
]!.filter((tab) => tab.contentType === 'browser')
expect(browserTabs.map((tab) => [tab.id, tab.entityId])).toEqual([[mainId, 'browser-1']])
})
})
@@ -11,11 +11,11 @@ import {
import {
dedupeEditorTabsWithinGroups,
getPersistedEditFileIdsByWorktree,
isTransientEditorContentType,
sanitizeRecentTabIds,
selectHydratedActiveGroupId
} from './tab-group-state'
import { dedupeTabOrder } from '../../../../shared/workspace-layout/tab-order'
import { restoresSavedContentTab } from '../../../../shared/workspace-layout/saved-editor-tab-restore'
type HydratedTabState = {
unifiedTabsByWorktree: Record<string, Tab[]>
@@ -127,22 +127,10 @@ export function buildHydratedTabState(
// containing "::"; those are invalid pane-key tab ids.
return isValidTerminalTabId(tab.id) && isValidTerminalTabId(tab.entityId)
}
// Why dropped rather than converted: a preview used to be an editor tab whose id encoded
// the document, and its document was never persisted — so this chrome has always come back
// naming a file no restore produces, which is the empty pane the surface was reported for.
// A preview is a browser tab now, and the worktree id inside that encoded id can itself
// contain the separator, so re-deriving the document from it is guesswork. The reader
// reopens the preview; nothing is left pointing at a surface that cannot exist.
if (tab.contentType === 'editor' && tab.entityId.startsWith('html-preview::')) {
return false
}
if (!isTransientEditorContentType(tab.contentType)) {
return true
}
// Why: restore skips backing editor state for transient diff/conflict
// items. Hydration must drop their tab chrome too or the split group
// comes back pointing at a document that no longer exists.
return persistedEditFileIds.has(tab.entityId) || keepsTransientTab(worktreeId, tab.entityId)
// The same rule main's layout load applies, so neither holds a tab the other drops.
return restoresSavedContentTab(tab, persistedEditFileIds, () =>
keepsTransientTab(worktreeId, tab.entityId)
)
})
.sort((a, b) => a.sortOrder - b.sortOrder || a.createdAt - b.createdAt)
// Why after the sort: the surviving record is the one the strip renders first.
@@ -195,12 +195,12 @@ export function translateSessionTabMove(
}
const shownOrder = toTabIds(workspace, shown.tabOrder)
if (params.kind === 'move-to-group') {
const shownIndex = Math.max(0, Math.min(params.index ?? shownOrder.length, shownOrder.length))
// A shown index lands before the same tab in the full order; the end stays the end.
const index =
shownIndex === shownOrder.length
? target.tabOrder.length
: target.tabOrder.indexOf(shownOrder[shownIndex]!)
const rowCount = shown.tabOrder.length
const shownIndex = Math.max(0, Math.min(params.index ?? rowCount, rowCount))
// A shown index lands before the first tab main holds from that row on (a row it holds no tab
// for, such as one the window drew before main's frame, places nothing); past the last, the end.
const before = toTabIds(workspace, shown.tabOrder.slice(shownIndex))[0]
const index = before === undefined ? target.tabOrder.length : target.tabOrder.indexOf(before)
return to({
...on,
type: 'moveTab',
@@ -209,11 +209,23 @@ export function translateSessionTabMove(
afterTabId: tabOrderAnchor(target.tabOrder, index)
})
}
const sent = params.tabOrder.map((rowId) => resolveSessionTabRow(workspace, rowId)?.tab.id)
// A row main holds no tab for (one drawn before main's frame) is named once, and keeps no place.
const unheld = (rowIds: readonly string[]) =>
rowIds.filter((rowId) => !resolveSessionTabRow(workspace, rowId))
const shownUnheld = new Set(unheld(shown.tabOrder))
const sentUnheld = unheld(params.tabOrder)
if (
sentUnheld.length !== shownUnheld.size ||
new Set(sentUnheld).size !== sentUnheld.length ||
sentUnheld.some((rowId) => !shownUnheld.has(rowId))
) {
return refused('invalid_tab_order')
}
const sent = toTabIds(workspace, params.tabOrder)
if (
sent.length !== shownOrder.length ||
new Set(sent).size !== sent.length ||
sent.some((id) => id === undefined || !shownOrder.includes(id)) ||
sent.some((id) => !shownOrder.includes(id)) ||
!sent.includes(tab.id)
) {
return refused('invalid_tab_order')
@@ -140,3 +140,30 @@ export function diffTabRow(
...tabRowProps(tab, scope)
}
}
/**
* A conflict-review or check-details tab's row from its view: a plain file row, as the window lists
* it (buildMobileFileTab), so an older client keeps seeing the tab once the window's rows are gone.
*/
export function reviewTabRow(
tab: LayoutContentTab & { kind: 'conflict-review' | 'check-details' },
scope: EditorRowScope
): RuntimeMobileSessionFileTab | null {
const editorView = scope.workspace.editorViews?.[tab.id]
if (editorView?.view.mode !== tab.kind) {
return null
}
const { file } = editorView
return {
type: 'file',
id: tab.id,
title: fileName(file.relativePath, 'File'),
filePath: file.filePath,
relativePath: file.relativePath,
language: file.language,
mode: 'edit',
// The model holds no unsaved edit of a review; the window's row it lists wins.
isDirty: false,
...tabRowProps(tab, scope)
}
}
@@ -7,6 +7,7 @@ import {
type SessionTabsView
} from './old-client-session-tabs-projection'
import { BLANK_BROWSER_TAB_STATE } from '../workspace-layout-beside'
import { editorFileKey } from '../workspace-layout-editor-tab-file'
import { build, testContext, WS } from '../workspace-layout-command.test-fixture'
import type { WorkspaceLayoutModel } from '../workspace-layout-model'
@@ -115,7 +116,14 @@ describe('projectSessionTabs', () => {
}
},
panes: { [apiTab]: { activeLeafId: apiSplit, expandedLeafId: null } },
editorDrafts: { [WS]: { '/Users/dev/orca/README.md': { dirtyDraftContent: '# draft' } } }
// Keyed as editorFileKey keys it (an owned id in a layout-owner build), as the Loader does.
editorDrafts: {
[WS]: {
[editorFileKey({ filePath: '/Users/dev/orca/README.md' })]: {
dirtyDraftContent: '# draft'
}
}
}
})
const facts: SessionTabsFacts = {
tabLabels: {},
@@ -18,7 +18,7 @@ import type {
} from '../../runtime-session-contracts'
import type { TerminalLayoutSnapshot } from '../../terminal-tab-types'
import { pruneGroupLayout } from '../../workspace-session-terminal-tab-close'
import { diffTabRow, editorTabRow } from './old-client-session-editor-rows'
import { diffTabRow, editorTabRow, reviewTabRow } from './old-client-session-editor-rows'
import { agentSessionRowId, publishedTabOrderId, terminalRowId } from './old-client-session-row-ids'
import { collectLayoutLeafIdsInOrder } from '../terminal-pane-tree'
import { groupsInLayoutOrder } from './old-client-group-order'
@@ -157,9 +157,12 @@ function projectTab(tab: LayoutTab, scope: Scope): RuntimeMobileSessionSnapshotT
const row = agentSessionRow(tab, scope)
return row ? [row] : []
}
// These need the desktop to draw them.
case 'conflict-review':
case 'check-details':
case 'check-details': {
const row = reviewTabRow({ ...tab, kind: tab.kind }, scope)
return row ? [row] : []
}
// A simulator needs the desktop to draw it; the window lists none either.
case 'simulator':
return []
}
@@ -0,0 +1,31 @@
// Which saved editor-family tabs a restore brings back. One rule for the window's hydration and
// main's layout load, so both drop the same tabs and neither holds a tab the other cannot draw.
import type { Tab, TabContentType } from '../tab-types'
/** Diff, conflict and check tabs: their documents live in memory only and are never saved. */
export function isTransientEditorContentType(contentType: TabContentType): boolean {
return (
contentType === 'diff' || contentType === 'conflict-review' || contentType === 'check-details'
)
}
/**
* Whether a saved non-terminal tab comes back. An old html preview's tab names a document no restore
* produces (a preview is a browser tab now, and re-deriving its document from the id is guesswork).
* A transient tab's document is never saved, so it comes back only for a saved edit file of its
* entity, or while its view is still held (`keepsView`); else its group would point at nothing.
*/
export function restoresSavedContentTab(
tab: Pick<Tab, 'contentType' | 'entityId'>,
savedEditFilePaths: ReadonlySet<string>,
keepsView: () => boolean
): boolean {
if (tab.contentType === 'editor' && tab.entityId.startsWith('html-preview::')) {
return false
}
if (!isTransientEditorContentType(tab.contentType)) {
return true
}
return savedEditFilePaths.has(tab.entityId) || keepsView()
}
@@ -0,0 +1,147 @@
// Layout-owner build (L280): a saved browser record with no tab-bar entry gets its tab from main's
// load, as the window's browser hydration repairs it, so no window mints an id main cannot resolve.
import { describe, expect, it, vi } from 'vitest'
import { getDefaultWorkspaceSession } from '../constants'
import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../execution-host'
import type { Tab } from '../tab-types'
import type { WorkspaceSessionState } from '../workspace-session-state-types'
import { resolveSessionTabRow } from './old-client/old-client-session-tab-rows'
import { loadWorkspaceLayout } from './workspace-layout-load'
import { nameBasedLoadContext } from './workspace-layout-minted-ids'
import { saveWorkspaceLayout } from './workspace-layout-save'
vi.mock('../layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
const KEY = 'repo-1::/w'
function editor(id: string, groupId: string, sortOrder: number): Tab {
return {
id,
entityId: `/w/${id}.ts`,
groupId,
worktreeId: KEY,
contentType: 'editor',
label: id,
customLabel: null,
color: null,
sortOrder,
createdAt: 1
}
}
/** Two groups, the second active, and a browser record no tab-bar entry names. */
function savedSession(): WorkspaceSessionState {
return {
...getDefaultWorkspaceSession(),
unifiedTabs: { [KEY]: [editor('a', 'g1', 0), editor('b', 'g2', 0)] },
tabGroups: {
[KEY]: [
{ id: 'g1', worktreeId: KEY, activeTabId: 'a', tabOrder: ['a'] },
{ id: 'g2', worktreeId: KEY, activeTabId: 'b', tabOrder: ['b'] }
]
},
activeGroupIdByWorktree: { [KEY]: 'g2' },
openFilesByWorktree: {
[KEY]: ['a', 'b'].map((name) => ({
filePath: `/w/${name}.ts`,
relativePath: `${name}.ts`,
worktreeId: KEY,
language: 'typescript'
}))
},
browserTabsByWorktree: {
[KEY]: [
{
id: 'browser-1',
worktreeId: KEY,
url: 'https://example.com',
title: 'Example',
loading: false,
faviconUrl: null,
canGoBack: false,
canGoForward: false,
loadError: null,
createdAt: 5
}
]
}
}
}
// As main's projection loads it after a restart: its views carried, none held.
const load = (session: WorkspaceSessionState, hostId: ExecutionHostId = LOCAL_EXECUTION_HOST_ID) =>
loadWorkspaceLayout(hostId, session, {
...nameBasedLoadContext(),
editorViewsOf: () => undefined
})
describe("main's load of a browser record with no tab", () => {
it('gives it a tab at the end of the active group, which a row id resolves to', () => {
const workspace = load(savedSession()).layout.workspaces[KEY]!
const tab = workspace.tabs.find((entry) => entry.kind === 'browser')!
expect(tab).toMatchObject({ entityId: 'browser-1', createdAt: 5 })
expect(workspace.groups.map((group) => group.tabOrder)).toEqual([['a'], ['b', tab.id]])
// The window publishes a browser row under its record id.
expect(resolveSessionTabRow(workspace, 'browser-1')?.tab.id).toBe(tab.id)
})
it('saves the tab, so the next load mints nothing new', () => {
const saved = saveWorkspaceLayout(load(savedSession()))
expect(
saved.unifiedTabs?.[KEY]?.find((entry) => entry.contentType === 'browser')
).toMatchObject({ entityId: 'browser-1', groupId: 'g2', label: 'Example' })
expect(load(saved).changes).toEqual([])
})
it('mints the same id on every load, and a restart after a save keeps one tab', () => {
const ids = (session: WorkspaceSessionState) =>
load(session)
.layout.workspaces[KEY]!.tabs.filter((tab) => tab.kind === 'browser')
.map((tab) => tab.id)
const first = ids(savedSession())
expect(ids(savedSession())).toEqual(first)
expect(ids(saveWorkspaceLayout(load(savedSession())))).toEqual(first)
expect(first).toHaveLength(1)
})
// The load reads only the saved partition: no host contact, so a direct SSH host that is down
// (unverifiable, not gone) gets the same repair and loses nothing but the window's drop.
it('repairs and drops the same in a direct SSH partition, whose host it never contacts', () => {
const session = savedSession()
session.unifiedTabs![KEY]!.push({
...editor('ghost', 'g2', 1),
contentType: 'diff',
entityId: '/w/gone.ts'
})
const tabsOf = (hostId: ExecutionHostId) =>
load(session, hostId).layout.workspaces[KEY]!.tabs.map((tab) => [tab.id, tab.kind])
expect(tabsOf('ssh:box')).toEqual(tabsOf(LOCAL_EXECUTION_HOST_ID))
expect(tabsOf('ssh:box')).toContainEqual([expect.any(String), 'browser'])
expect(tabsOf('ssh:box').map(([id]) => id)).toEqual(expect.arrayContaining(['a', 'b']))
expect(tabsOf('ssh:box').map(([id]) => id)).not.toContain('ghost')
})
it('skips a saved active group this load empties, as the window picks a group that holds a tab', () => {
const session = savedSession()
// g1 holds only a diff tab whose document was never saved, so the load drops it.
session.unifiedTabs![KEY] = [
{ ...editor('a', 'g1', 0), id: 'ghost', contentType: 'diff', entityId: '/w/gone.ts' },
editor('b', 'g2', 0)
]
session.tabGroups![KEY] = [
{ id: 'g1', worktreeId: KEY, activeTabId: 'ghost', tabOrder: ['ghost'] },
{ id: 'g2', worktreeId: KEY, activeTabId: 'b', tabOrder: ['b'] }
]
session.activeGroupIdByWorktree = { [KEY]: 'g1' }
const workspace = load(session).layout.workspaces[KEY]!
const tab = workspace.tabs.find((entry) => entry.kind === 'browser')!
expect(workspace.groups).toEqual([{ id: 'g2', tabOrder: ['b', tab.id] }])
})
})
@@ -0,0 +1,124 @@
// Layout-owner build (L286): main's layout load, given its views, drops the saved tabs the window's
// restore drops (restoresSavedContentTab), so main never holds a tab no window draws.
import { describe, expect, it, vi } from 'vitest'
import { LOCAL_EXECUTION_HOST_ID } from '../execution-host'
import type { Tab, TabContentType } from '../tab-types'
import type { WorkspaceSessionState } from '../workspace-session-state-types'
import { getDefaultWorkspaceSession } from '../constants'
import { translateSessionTabMove } from './old-client/old-client-method-translation'
import { loadWorkspaceLayout } from './workspace-layout-load'
import { nameBasedLoadContext } from './workspace-layout-minted-ids'
import { saveWorkspaceLayout } from './workspace-layout-save'
vi.mock('../layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
const KEY = 'repo-1::/w'
function entry(id: string, contentType: TabContentType, entityId: string, sortOrder: number): Tab {
return {
id,
entityId,
groupId: 'g1',
worktreeId: KEY,
contentType,
label: id,
customLabel: null,
color: null,
sortOrder,
createdAt: 1
}
}
/** A saved session after a restart: two edit files, and tabs whose documents were never saved. */
function savedSession(): WorkspaceSessionState {
const tabs = [
entry('ghost-diff', 'diff', '/w/gone.ts', 0),
entry('ed-a', 'editor', '/w/a.ts', 1),
entry('ghost-review', 'conflict-review', '/w/conflict', 2),
entry('ghost-checks', 'check-details', '/w/checks', 3),
entry('ghost-html', 'editor', 'html-preview::/w/page.html', 4),
entry('kept-diff', 'diff', '/w/b.ts', 5),
entry('ed-b', 'editor', '/w/b.ts', 6)
]
return {
...getDefaultWorkspaceSession(),
unifiedTabs: { [KEY]: tabs },
tabGroups: {
[KEY]: [{ id: 'g1', worktreeId: KEY, activeTabId: 'ed-a', tabOrder: tabs.map((t) => t.id) }]
},
openFilesByWorktree: {
[KEY]: ['/w/a.ts', '/w/b.ts'].map((filePath) => ({
filePath,
relativePath: filePath.slice(3),
worktreeId: KEY,
language: 'typescript'
}))
}
}
}
// Main's projection and command applies carry its views; after a restart it holds none.
const load = (session: WorkspaceSessionState) =>
loadWorkspaceLayout(LOCAL_EXECUTION_HOST_ID, session, {
...nameBasedLoadContext(),
editorViewsOf: () => undefined
})
describe("main's layout load of view-less saved editor tabs", () => {
it('drops the tabs the window drops and keeps a diff of a saved edit file', () => {
const workspace = load(savedSession()).layout.workspaces[KEY]!
expect(workspace.groups.map((group) => group.tabOrder)).toEqual([['ed-a', 'kept-diff', 'ed-b']])
expect(workspace.tabs.map((tab) => tab.id).sort()).toEqual(['ed-a', 'ed-b', 'kept-diff'])
})
it('writes the drop back on its next save, and a reload does not bring them back', () => {
const saved = saveWorkspaceLayout(load(savedSession()))
expect(saved.unifiedTabs?.[KEY]?.map((tab) => tab.id)).toEqual(['ed-a', 'kept-diff', 'ed-b'])
expect(saved.tabGroups?.[KEY]?.[0]?.tabOrder).toEqual(['ed-a', 'kept-diff', 'ed-b'])
expect(load(saved).changes).toEqual([])
})
// Main's own edits (a phone's browser tab, an agent tab) load without its views: they must not
// drop a live diff whose view only main's memory holds.
it('keeps every saved tab in a load that carries no views', () => {
const workspace = loadWorkspaceLayout(
LOCAL_EXECUTION_HOST_ID,
savedSession(),
nameBasedLoadContext()
).layout.workspaces[KEY]!
expect(workspace.tabs).toHaveLength(7)
})
it('keeps a transient tab while main still holds its view', () => {
const workspace = loadWorkspaceLayout(LOCAL_EXECUTION_HOST_ID, savedSession(), {
...nameBasedLoadContext(),
editorViewsOf: () => ({
'ghost-diff': {
view: { mode: 'diff' },
file: { filePath: '/w/gone.ts', relativePath: 'gone.ts', language: 'typescript' }
}
})
}).layout.workspaces[KEY]!
expect(workspace.groups[0]?.tabOrder).toEqual(['ghost-diff', 'ed-a', 'kept-diff', 'ed-b'])
})
it("does not count a view-less tab in a client's move index", () => {
const workspace = load(savedSession()).layout.workspaces[KEY]!
// The rows a client was shown: the window draws no view-less tab either.
const shown = { id: 'g1', activeTabId: 'ed-a', tabOrder: ['ed-a', 'kept-diff', 'ed-b'] }
const translation = translateSessionTabMove(
KEY,
workspace,
{ tabGroups: [shown] },
{ kind: 'move-to-group', tabId: 'ed-b', targetGroupId: 'g1', index: 0 }
)
expect(translation).toMatchObject({ ok: true, commands: [{ type: 'moveTab', index: 0 }] })
})
})
@@ -7,6 +7,7 @@ import type { WorkspaceSessionState } from '../workspace-session-state-types'
import { childRecord, pickStoredFields } from './stored-record-fields'
import { readCanonicalPtyOwners, resolveCanonicalPtyClaim } from './legacy-terminal-row-claims'
import { resolveGroupOrder, type OrderCandidate } from './workspace-layout-load-order'
import { resolveTabGroupOwners } from './tab-group-owners'
import {
loadBrowserTabs,
loadEditorFiles,
@@ -18,6 +19,7 @@ import {
loadTerminalTab
} from './workspace-layout-load-tabs'
import type { WorkspaceLoadArgs } from './workspace-layout-load-types'
import { restoresSavedContentTab } from './saved-editor-tab-restore'
import type { LayoutTab, LayoutTerminalTab, WorkspaceLayout } from './workspace-layout-model'
import { pruneGroupLayout } from '../workspace-session-terminal-tab-close'
@@ -100,6 +102,18 @@ function loadTabs(args: WorkspaceLoadArgs): {
const rows = takeRows(args)
const rowByEntity = new Map(rows.map((row) => [row.id, row]))
const merged = new Set<string>()
const savedEditFilePaths = new Set(
(session.openFilesByWorktree?.[key] ?? []).map((file) => file.filePath)
)
// A transient tab stays while a view is held for its file, as the window's frame build keeps it.
// Only a load that carries main's views drops one: a load without them keeps every stored tab.
const viewsOf = ORCA_LAYOUT_OWNER_IS_MAIN ? args.context.editorViewsOf : undefined
const heldViews = viewsOf?.(key) ?? {}
const viewedEntities = new Set(
(session.unifiedTabs?.[key] ?? []).flatMap((entry) =>
heldViews[entry.id] ? [entry.entityId] : []
)
)
const tabs: LayoutTab[] = []
const candidates: OrderCandidate[] = []
const tabIds = new Set<string>()
@@ -128,6 +142,14 @@ function loadTabs(args: WorkspaceLoadArgs): {
if (tabIds.has(entry.id) || aliases.has(entry.id)) {
continue
}
// Switch point: main drops the saved tabs the window's restore drops (L286).
if (
viewsOf &&
entry.contentType !== 'terminal' &&
!restoresSavedContentTab(entry, savedEditFilePaths, () => viewedEntities.has(entry.entityId))
) {
continue
}
if (entry.contentType === 'terminal') {
const row = rowByEntity.get(entry.entityId)
if (!row || merged.has(row.id)) {
@@ -151,6 +173,39 @@ function loadTabs(args: WorkspaceLoadArgs): {
childRecord(args.view.lastFocusedAt, key)[entry.id] = entry.lastFocusedAt
}
}
// Switch point: a saved browser record with no tab gets one at the end of the active group (the
// saved one while it holds a tab, as selectHydratedActiveGroupId picks it), the repair the
// window's browser hydration makes (createUnifiedTab), so no window mints its own id.
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
const storedGroups = session.tabGroups?.[key] ?? []
const heldGroupIds = new Set(resolveTabGroupOwners(candidates, storedGroups).values())
const savedActive = session.activeGroupIdByWorktree?.[key]
const activeGroupId =
savedActive && heldGroupIds.has(savedActive)
? savedActive
: storedGroups.find((group) => heldGroupIds.has(group.id))?.id
for (const record of session.browserTabsByWorktree?.[key] ?? []) {
if (tabs.some((tab) => tab.kind === 'browser' && tab.entityId === record.id)) {
continue
}
const id = args.context.mintId(`browser-tab:${record.id}`)
tabs.push({
id,
kind: 'browser',
entityId: record.id,
createdAt: record.createdAt,
customTitle: null,
color: null
})
tabIds.add(id)
childRecord(args.facts.tabLabels, key)[id] = record.title
candidates.push({
id,
groupId: activeGroupId,
createdAt: record.createdAt
})
}
}
// A row outside the tab bar whose every terminal a tab-bar tab holds is a stale copy of it.
const canonical = readCanonicalPtyOwners(session, key, rows)
for (const row of rows) {
@@ -5,6 +5,7 @@ import { loadWorkspaceLayout } from './workspace-layout-load'
import { checkWorkspaceLayoutModelRules } from './workspace-layout-model-rules'
import { checkWorkspaceLayoutRules } from './workspace-layout-rules'
import { saveWorkspaceLayout } from './workspace-layout-save'
import { editorFileKey } from './workspace-layout-editor-tab-file'
import {
localDesktopSession,
WORKSPACE_LAYOUT_PROFILES
@@ -105,8 +106,9 @@ describe('workspace layout Loader and Serializer', () => {
activeTabId: 'tab-agent',
recentTabIds: ['tab-shell', 'tab-agent']
})
// Keyed as editorFileKey keys it: a layout-owner build keys by owned file id.
expect(loaded.desktopView.editorDrafts['repo-1::/Users/dev/orca']).toEqual({
'/Users/dev/orca/src/app.ts': {
[editorFileKey({ filePath: '/Users/dev/orca/src/app.ts' })]: {
dirtyDraftContent: 'export {}\n',
lastKnownDiskSignature: 'sig-1'
}