Skip to content

Commit 5d522d4

Browse files
committed
fix(restore): preserve saved tab order during startup
Bind saved tabs to workspaces that are still starting instead of treating them as missing until readiness. This preserves each workspace identity and saved position before the active workspace can be placed first, while terminal stopped and error workspaces remain excluded from restoration. Add a regression case matching the observed DreamX and CodeNomad startup order. Validated with 46 UI restoration tests, UI typecheck and production build, plus 38 targeted Tauri client-state tests.
1 parent 53ab27b commit 5d522d4

3 files changed

Lines changed: 22 additions & 4 deletions

File tree

packages/ui/src/lib/hooks/use-app-session-restore.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,7 @@ async function restoreTabs(context: RestoreContext): Promise<void> {
110110
const matches = reconcileWorkspaceTabs(snapshot.tabs.map((tab) => tab.kind === "workspace"
111111
? { kind: tab.kind, folderPath: tab.folder, occurrence: tab.occurrence }
112112
: { kind: tab.kind }), Array.from(instances().values())
113-
.filter(({ status }) => status === "ready")
114-
.map(({ id, folder }) => ({ id, folderPath: folder })))
113+
.map(({ id, folder, status }) => ({ id, folderPath: folder, status })))
115114
const existing = matches.filter(({ existingWorkspaceId }) => existingWorkspaceId)
116115
const missing = matches.filter(({ existingWorkspaceId }) => !existingWorkspaceId)
117116
existing.forEach(({ tabIndex, existingWorkspaceId }) =>

packages/ui/src/stores/app-session-reconciliation.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ describe("app session reconciliation", () => {
1616
for (const [label, saved, live, expected] of workspaceCases) it(label, () => assert.deepEqual(
1717
reconcileWorkspaceTabs([...saved], [...live]).map(({ existingWorkspaceId }) => existingWorkspaceId), expected,
1818
))
19+
it("keeps saved order while a restored workspace is still starting", () => {
20+
const saved = [
21+
{ kind: "workspace", folderPath: "D:/DreamX-World" },
22+
{ kind: "workspace", folderPath: "D:/CodeNomad" },
23+
{ kind: "workspace", folderPath: "D:/stale" },
24+
]
25+
const live = [
26+
{ id: "codenomad", folderPath: "D:/CodeNomad", status: "ready" },
27+
{ id: "dreamx", folderPath: "D:/DreamX-World", status: "starting" },
28+
{ id: "stale", folderPath: "D:/stale", status: "stopped" },
29+
] as const
30+
31+
assert.deepEqual(
32+
reconcileWorkspaceTabs(saved, live).map(({ existingWorkspaceId }) => existingWorkspaceId),
33+
["dreamx", "codenomad", null],
34+
)
35+
})
1936
const selectionCases = [
2037
["falls back to a valid parent when the active session is stale", [{ id: "parent", parentId: null }, { id: "child", parentId: "parent" }], "parent", "deleted-child", { parentSessionId: "parent", activeSessionId: "parent" }],
2138
["restores a grandchild under its root session", [{ id: "root", parentId: null }, { id: "child", parentId: "root" }, { id: "grandchild", parentId: "child" }], "root", "grandchild", { parentSessionId: "root", activeSessionId: "grandchild" }],

packages/ui/src/stores/app-session-reconciliation.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export type ReconcileTabDescriptor = { kind: string; folderPath?: string; occurrence?: number }
2+
type LiveWorkspaceDescriptor = { id: string; folderPath: string; status?: string }
23
export type SessionDescriptor = { id: string; parentId?: string | null }
34
export interface RestoredSessionReferences {
45
activeParentSessionId?: string
@@ -16,10 +17,11 @@ export function normalizeWorkspacePath(folderPath: string): string {
1617
}
1718
export function reconcileWorkspaceTabs(
1819
tabs: readonly ReconcileTabDescriptor[],
19-
liveWorkspaces: readonly { id: string; folderPath: string }[],
20+
liveWorkspaces: readonly LiveWorkspaceDescriptor[],
2021
) {
21-
const liveByPath = new Map<string, Array<{ id: string; folderPath: string }>>()
22+
const liveByPath = new Map<string, LiveWorkspaceDescriptor[]>()
2223
for (const workspace of liveWorkspaces) {
24+
if (workspace.status === "stopped" || workspace.status === "error") continue
2325
const path = normalizeWorkspacePath(workspace.folderPath)
2426
liveByPath.set(path, [...(liveByPath.get(path) ?? []), workspace])
2527
}

0 commit comments

Comments
 (0)