diff --git a/src/features/chat/application/lifecycle.ts b/src/features/chat/application/lifecycle.ts index 130c32e5..e11a0e4d 100644 --- a/src/features/chat/application/lifecycle.ts +++ b/src/features/chat/application/lifecycle.ts @@ -22,8 +22,6 @@ export type RestoredThreadLifecycleEvent = export interface ChatViewDeferredTasks { scheduleDiagnostics(callback: () => void): void; clearDiagnostics(): void; - scheduleRestoredThreadHydration(callback: () => void): void; - clearRestoredThreadHydration(): void; scheduleAppServerWarmup(callback: () => void): void; clearAppServerWarmup(): void; clearAll(): void; diff --git a/src/features/chat/application/state/actions.ts b/src/features/chat/application/state/actions.ts index 83973a7f..7d172276 100644 --- a/src/features/chat/application/state/actions.ts +++ b/src/features/chat/application/state/actions.ts @@ -93,11 +93,6 @@ export interface ThreadListAppliedAction { threadsLoaded?: boolean; } -export interface ActiveThreadRestoredPlaceholderAction { - type: "active-thread/restored-placeholder"; - threadId: string; -} - export interface DisclosureSetAction { type: "ui/disclosure-set"; bucket: "details" | "activityGroups" | "textDetails" | "userMessageExpanded" | "goalObjectiveExpanded" | "approvalDetails"; diff --git a/src/features/chat/application/state/root-reducer.ts b/src/features/chat/application/state/root-reducer.ts index ee5a3c9b..c9de9039 100644 --- a/src/features/chat/application/state/root-reducer.ts +++ b/src/features/chat/application/state/root-reducer.ts @@ -31,7 +31,6 @@ import type { ComposerSuggestion } from "../composer/suggestions"; import type { MessageStreamItem } from "../../domain/message-stream/items"; import type { ActiveThreadResumedAction, - ActiveThreadRestoredPlaceholderAction, ActiveThreadSettingsAppliedAction, ClearActiveThreadAction, ClearDisconnectedConnectionStateAction, @@ -233,7 +232,6 @@ type ChatTransitionAction = | ClearActiveThreadAction | ActiveThreadResumedAction | ActiveThreadSettingsAppliedAction - | ActiveThreadRestoredPlaceholderAction | { type: "active-thread/goal-set"; goal: ThreadGoal | null } | TurnAction | RequestResolvedAction @@ -269,7 +267,6 @@ export function chatReducer(state: ChatState, action: ChatAction): ChatState { case "active-thread/cleared": case "active-thread/resumed": case "active-thread/settings-applied": - case "active-thread/restored-placeholder": case "active-thread/goal-set": case "turn/started": case "turn/completed": @@ -295,8 +292,6 @@ function reduceChatTransition(state: ChatState, action: ChatTransitionAction): C return reduceActiveThreadResumedTransition(state, action); case "active-thread/settings-applied": return reduceActiveThreadSettingsAppliedTransition(state, action); - case "active-thread/restored-placeholder": - return reduceActiveThreadRestoredPlaceholderTransition(state, action); case "active-thread/goal-set": return reduceActiveThreadGoalSetTransition(state, action.goal); case "turn/started": @@ -374,22 +369,6 @@ function reduceActiveThreadSettingsAppliedTransition(state: ChatState, action: A }); } -function reduceActiveThreadRestoredPlaceholderTransition(state: ChatState, action: ActiveThreadRestoredPlaceholderAction): ChatState { - return clearTurnScopedState( - patchChatState(state, { - activeThread: { - id: action.threadId, - cwd: null, - goal: null, - tokenUsage: null, - }, - runtime: initialChatRuntimeState(), - messageStream: initialMessageStreamState(), - ui: initialUiState(), - }), - ); -} - function reduceActiveThreadGoalSetTransition(state: ChatState, goal: ThreadGoal | null): ChatState { return patchChatState(state, { activeThread: { diff --git a/src/features/chat/application/threads/active-thread-identity-sync.ts b/src/features/chat/application/threads/active-thread-identity-sync.ts index da9f517f..6aba6c36 100644 --- a/src/features/chat/application/threads/active-thread-identity-sync.ts +++ b/src/features/chat/application/threads/active-thread-identity-sync.ts @@ -6,7 +6,6 @@ export interface ActiveThreadIdentitySyncHost { stateStore: ChatStateStore; restoration: RestorationController; invalidateThreadWork: () => void; - clearDeferredRestoredThreadHydration: () => void; resetThreadTurnPresence: (hadTurns: boolean) => void; notifyActiveThreadIdentityChanged: () => void; refreshTabHeader: () => void; @@ -35,14 +34,13 @@ export function createActiveThreadIdentitySync(host: ActiveThreadIdentitySyncHos function clearActiveThreadIdentity(host: ActiveThreadIdentitySyncHost): void { host.invalidateThreadWork(); host.restoration.clear(); - host.clearDeferredRestoredThreadHydration(); host.stateStore.dispatch({ type: "active-thread/cleared" }); host.resetThreadTurnPresence(false); host.notifyActiveThreadIdentityChanged(); } function applyThreadArchiveToActiveIdentity(host: ActiveThreadIdentitySyncHost, threadId: string): void { - if (activeThreadId(host.stateStore.getState()) !== threadId) return; + if (activeThreadId(host.stateStore.getState()) !== threadId && !host.restoration.isPending(threadId)) return; clearActiveThreadIdentity(host); } diff --git a/src/features/chat/application/threads/lifecycle-parts.ts b/src/features/chat/application/threads/lifecycle-parts.ts index 2393a6e3..d49bb49a 100644 --- a/src/features/chat/application/threads/lifecycle-parts.ts +++ b/src/features/chat/application/threads/lifecycle-parts.ts @@ -1,6 +1,6 @@ import type { AppServerClient } from "../../../../app-server/connection/client"; import { recoverRolloutTokenUsage } from "../../../../app-server/services/rollout-token-usage"; -import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../lifecycle"; +import type { ChatResumeWorkTracker } from "../lifecycle"; import type { ChatStateStore } from "../state/store"; import type { GoalActions } from "./goal-actions"; import type { HistoryController } from "./history-controller"; @@ -16,11 +16,9 @@ export interface ThreadLifecyclePartsContext { ensureConnected: () => Promise; }; lifecycle: { - deferredTasks: ChatViewDeferredTasks; resumeWork: ChatResumeWorkTracker; history: HistoryController; invalidateThreadWork: () => void; - getOpened: () => boolean; getClosing: () => boolean; }; thread: { @@ -47,12 +45,9 @@ export interface ThreadLifecycleParts { export function createThreadLifecycleParts(context: ThreadLifecyclePartsContext): ThreadLifecycleParts { const { settingsRef, stateStore, client, lifecycle, thread, status, liveState, goals, resetThreadTurnPresence } = context; - const { deferredTasks, resumeWork, history, invalidateThreadWork } = lifecycle; + const { resumeWork, history, invalidateThreadWork } = lifecycle; const restoration = new RestorationController({ - deferredTasks, - opened: lifecycle.getOpened, invalidateThreadWork, - stateStore, setStatus: status.set, refreshTabHeader: thread.refreshTabHeader, }); @@ -66,9 +61,6 @@ export function createThreadLifecycleParts(context: ThreadLifecyclePartsContext) ensureConnected: client.ensureConnected, closing: lifecycle.getClosing, resetThreadTurnPresence, - clearDeferredRestoredThreadHydration: () => { - restoration.clearHydration(); - }, notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged, addSystemMessage: status.addSystemMessage, refreshLiveState: liveState.refresh, @@ -83,9 +75,6 @@ export function createThreadLifecycleParts(context: ThreadLifecyclePartsContext) stateStore, restoration, invalidateThreadWork, - clearDeferredRestoredThreadHydration: () => { - restoration.clearHydration(); - }, resetThreadTurnPresence, notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged, refreshTabHeader: thread.refreshTabHeader, diff --git a/src/features/chat/application/threads/restoration-controller.ts b/src/features/chat/application/threads/restoration-controller.ts index 712e25e9..ece1f8c6 100644 --- a/src/features/chat/application/threads/restoration-controller.ts +++ b/src/features/chat/application/threads/restoration-controller.ts @@ -1,19 +1,14 @@ -import type { ChatStateStore } from "../state/store"; import { transitionRestoredThreadLifecycle, type RestoredThreadLifecycleState, type RestoredThreadPlaceholderState, type RestoredThreadState, - type ChatViewDeferredTasks, } from "../lifecycle"; const STATUS_THREAD_READY_TO_RESUME = "Thread ready to resume."; export interface RestorationControllerHost { - deferredTasks: ChatViewDeferredTasks; - opened: () => boolean; invalidateThreadWork: () => void; - stateStore: ChatStateStore; setStatus: (status: string) => void; refreshTabHeader: () => void; } @@ -49,7 +44,6 @@ export class RestorationController { type: "placeholder-restored", restoredThread, }); - this.host.stateStore.dispatch({ type: "active-thread/restored-placeholder", threadId: restoredThread.threadId }); this.host.setStatus(STATUS_THREAD_READY_TO_RESUME); this.host.refreshTabHeader(); } @@ -57,7 +51,6 @@ export class RestorationController { async ensureLoaded(loadThread: RestoredThreadLoader): Promise { const restoredThread = this.placeholder(); if (!restoredThread) return true; - this.clearHydration(); if (restoredThread.loading) { const threadId = restoredThread.threadId; await restoredThread.loading; @@ -78,18 +71,4 @@ export class RestorationController { isPending(threadId: string): boolean { return this.placeholder()?.threadId === threadId; } - - scheduleHydration(loadThread: RestoredThreadLoader): void { - const restoredThread = this.placeholder(); - if (!this.host.opened() || !restoredThread) return; - const threadId = restoredThread.threadId; - this.host.deferredTasks.scheduleRestoredThreadHydration(() => { - if (!this.isPending(threadId)) return; - void this.ensureLoaded(loadThread); - }); - } - - clearHydration(): void { - this.host.deferredTasks.clearRestoredThreadHydration(); - } } diff --git a/src/features/chat/application/threads/resume-actions.ts b/src/features/chat/application/threads/resume-actions.ts index 09b1910b..aec4c56a 100644 --- a/src/features/chat/application/threads/resume-actions.ts +++ b/src/features/chat/application/threads/resume-actions.ts @@ -18,7 +18,6 @@ export interface ResumeActionsHost { ensureConnected: () => Promise; closing: () => boolean; resetThreadTurnPresence: (hadTurns: boolean) => void; - clearDeferredRestoredThreadHydration: () => void; notifyActiveThreadIdentityChanged: () => void; addSystemMessage: (text: string) => void; refreshLiveState: () => void; @@ -88,7 +87,6 @@ function applyResumedThread(host: ResumeActionsHost, response: ChatThreadResumeS }), ); host.restoration.clear(); - host.clearDeferredRestoredThreadHydration(); host.resetThreadTurnPresence(false); host.notifyActiveThreadIdentityChanged(); } diff --git a/src/features/chat/host/lifecycle.ts b/src/features/chat/host/lifecycle.ts index e9362d76..48784b94 100644 --- a/src/features/chat/host/lifecycle.ts +++ b/src/features/chat/host/lifecycle.ts @@ -3,7 +3,6 @@ import type { ChatViewDeferredTasks } from "../application/lifecycle"; export function createChatViewDeferredTasks(getWindow: () => DeferredTaskWindow): ChatViewDeferredTasks { const diagnosticsTask = new DeferredTask(getWindow, 1_000); - const restoredThreadHydrationTask = new DeferredTask(getWindow, 1_500); const appServerWarmupTask = new DeferredTask(getWindow, 0); return { @@ -15,14 +14,6 @@ export function createChatViewDeferredTasks(getWindow: () => DeferredTaskWindow) diagnosticsTask.clear(); }, - scheduleRestoredThreadHydration(callback): void { - restoredThreadHydrationTask.schedule(callback); - }, - - clearRestoredThreadHydration(): void { - restoredThreadHydrationTask.clear(); - }, - scheduleAppServerWarmup(callback): void { appServerWarmupTask.schedule(callback); }, @@ -32,7 +23,6 @@ export function createChatViewDeferredTasks(getWindow: () => DeferredTaskWindow) }, clearAll(): void { - restoredThreadHydrationTask.clear(); appServerWarmupTask.clear(); diagnosticsTask.clear(); }, diff --git a/src/features/chat/host/session-graph.ts b/src/features/chat/host/session-graph.ts index 56744182..e922b18c 100644 --- a/src/features/chat/host/session-graph.ts +++ b/src/features/chat/host/session-graph.ts @@ -135,7 +135,6 @@ interface ChatPanelSessionGraphHost { resumeWork: ChatResumeWorkTracker; connectionWork: ConnectionWorkTracker; messageScrollController: ChatMessageScrollController; - getOpened: () => boolean; getClosing: () => boolean; viewWindow: () => Window; } @@ -640,11 +639,9 @@ function createSessionThreadLifecycle( ensureConnected, }, lifecycle: { - deferredTasks: host.deferredTasks, resumeWork: host.resumeWork, history, invalidateThreadWork, - getOpened: host.getOpened, getClosing: host.getClosing, }, thread: { diff --git a/src/features/chat/host/session.ts b/src/features/chat/host/session.ts index ba2febb1..4a2c751a 100644 --- a/src/features/chat/host/session.ts +++ b/src/features/chat/host/session.ts @@ -4,7 +4,7 @@ import { appServerQueryContextRawEquals, type AppServerQueryContext } from "../. import { threadMeaningfulTitle, threadWindowTitle } from "../../../domain/threads/title"; import { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work"; import { createChatViewDeferredTasks } from "./lifecycle"; -import { ChatResumeWorkTracker, type ChatViewDeferredTasks } from "../application/lifecycle"; +import { ChatResumeWorkTracker, type ChatViewDeferredTasks, type RestoredThreadPlaceholderState } from "../application/lifecycle"; import { openPanelTurnLifecycle, parseRestoredThreadState, type ChatPanelSnapshot } from "../panel/snapshot"; import type { ChatState } from "../application/state/root-reducer"; import { pendingRequestCountsFromQueues } from "../../../domain/pending-requests/aggregate"; @@ -34,11 +34,11 @@ export class ChatPanelSession implements ChatSurfaceHandle { } displayTitle(): string { - return threadWindowTitle(this.state.activeThread.id, this.state.threadList.listedThreads, this.restoredThreadTitle()); + return threadWindowTitle(this.panelThreadId(), this.state.threadList.listedThreads, this.restoredThreadTitle()); } persistedState(): Record { - const threadId = this.state.activeThread.id; + const threadId = this.panelThreadId(); if (!threadId) return { version: 1 }; const threadTitle = this.restoredThreadTitle() ?? this.activeThreadTitle(); @@ -53,13 +53,11 @@ export class ChatPanelSession implements ChatSurfaceHandle { const restoredThread = parseRestoredThreadState(state); if (restoredThread) { this.graph.thread.restoration.restore(restoredThread); - this.scheduleRestoredThreadHydration(); return; } this.graph.actions.invalidateThreadWork(); this.graph.thread.restoration.clear(); - this.graph.thread.restoration.clearHydration(); this.scheduleWarmup(); } @@ -93,7 +91,7 @@ export class ChatPanelSession implements ChatSurfaceHandle { const pendingRequests = pendingRequestCountsFromQueues(this.state.requests); return { viewId: this.environment.obsidian.viewId, - threadId: this.closing ? null : this.state.activeThread.id, + threadId: this.closing ? null : this.panelThreadId(), turnLifecycle: openPanelTurnLifecycle(this.state.turn.lifecycle), pendingApprovals: pendingRequests.approvals, pendingUserInputs: pendingRequests.userInputs, @@ -109,7 +107,8 @@ export class ChatPanelSession implements ChatSurfaceHandle { } async focusThread(threadId: string | null = null): Promise { - if (threadId && this.graph.thread.restoration.isPending(threadId)) { + const restoredThreadId = this.restoredThread()?.threadId ?? null; + if ((threadId && this.graph.thread.restoration.isPending(threadId)) || (!threadId && restoredThreadId)) { await this.ensureRestoredThreadLoaded(); } this.focusComposer(); @@ -136,7 +135,6 @@ export class ChatPanelSession implements ChatSurfaceHandle { this.graph.runtime.sharedState.subscribe(); this.mountOrRepairShell(); this.scheduleWarmup(); - this.scheduleRestoredThreadHydration(); } close(): void { @@ -231,17 +229,21 @@ export class ChatPanelSession implements ChatSurfaceHandle { } private restoredThreadTitle(): string | null { - return this.graph.thread.restoration.title(); + return this.restoredThread()?.title ?? null; + } + + private restoredThread(): RestoredThreadPlaceholderState | null { + return this.graph.thread.restoration.placeholder(); + } + + private panelThreadId(): string | null { + return this.restoredThread()?.threadId ?? this.state.activeThread.id; } private ensureRestoredThreadLoaded(): Promise { return this.graph.thread.restoration.ensureLoaded((threadId) => this.graph.thread.resume.resumeThread(threadId)); } - private scheduleRestoredThreadHydration(): void { - this.graph.thread.restoration.scheduleHydration((threadId) => this.graph.thread.resume.resumeThread(threadId)); - } - private createSessionGraph(): ChatPanelSessionGraph { return createChatPanelSessionGraph({ environment: this.environment, @@ -250,7 +252,6 @@ export class ChatPanelSession implements ChatSurfaceHandle { resumeWork: this.resumeWork, connectionWork: this.connectionWork, messageScrollController: this.messageScrollController, - getOpened: () => this.opened, getClosing: () => this.closing, viewWindow: () => this.viewWindow(), }); diff --git a/src/features/chat/panel/surface/composer-projection.tsx b/src/features/chat/panel/surface/composer-projection.tsx index e8f89934..9ca4941a 100644 --- a/src/features/chat/panel/surface/composer-projection.tsx +++ b/src/features/chat/panel/surface/composer-projection.tsx @@ -223,7 +223,7 @@ function onOffLabel(active: boolean): string { } function activeComposerThreadName(state: ChatPanelComposerShellState, restoredThread: RestoredThreadTitleSnapshot | null): string | null { - const threadId = state.activeThread.id; + const threadId = restoredThread?.threadId ?? state.activeThread.id ?? null; if (!threadId) return null; const thread = state.threadList.listedThreads.find((item) => item.id === threadId); const listedName = thread ? explicitThreadName(thread) : null; diff --git a/tests/features/chat/host/session-graph.test.ts b/tests/features/chat/host/session-graph.test.ts index e976d1cd..d16a15cc 100644 --- a/tests/features/chat/host/session-graph.test.ts +++ b/tests/features/chat/host/session-graph.test.ts @@ -223,7 +223,6 @@ describe("createChatPanelSessionGraph actions", () => { resumeWork, connectionWork: new ConnectionWorkTracker(), messageScrollController: createChatMessageScrollController(), - getOpened: () => true, getClosing: () => false, viewWindow: () => window, }); diff --git a/tests/features/chat/state-reducer.test.ts b/tests/features/chat/state-reducer.test.ts index 3ac2f2e4..8ed89721 100644 --- a/tests/features/chat/state-reducer.test.ts +++ b/tests/features/chat/state-reducer.test.ts @@ -196,46 +196,6 @@ describe("chatReducer", () => { expect(chatStateMessageStreamItems(next)).toEqual([]); }); - it("keeps composer state when restoring a thread placeholder", () => { - let state = chatStateFixture(); - state = chatStateWith(state, { activeThread: { id: "previous-thread" } }); - state = chatStateWith(state, { turn: { lifecycle: { kind: "running", turnId: "previous-turn" } } }); - state = chatStateWith(state, { runtime: { activeModel: "gpt-5.1" } }); - state = chatStateWith(state, { activeThread: { goal: goal("previous-thread") } }); - state = chatStateWith(state, { messageStream: { historyCursor: "cursor" } }); - state = chatStateWith(state, { messageStream: { loadingHistory: true } }); - state = chatStateWith(state, { composer: { draft: "draft in this panel" } }); - state = chatStateWith(state, { composer: { suggestSelected: 1 } }); - state = chatStateWith(state, { composer: { suggestions: [suggestion("/resume")] } }); - state = chatStateWith(state, { composer: { suggestionsDismissedSignature: "dismissed" } }); - state = withChatStateMessageStreamItems(state, [message("previous-message")]); - state = chatStateWith(state, { messageStream: { turnDiffs: new Map([["previous-turn", "@@"]]) } }); - state = chatStateWith(state, { requests: { approvals: [approval(1)] } }); - state = chatStateWith(state, { requests: { pendingUserInputs: [userInput(2)] } }); - state = chatStateWith(state, { requests: { userInputDrafts: new Map([["2:note", "answer"]]) } }); - - const next = chatReducer(state, { - type: "active-thread/restored-placeholder", - threadId: "restored-thread", - }); - - expect(next.activeThread.id).toBe("restored-thread"); - expect(activeTurnId(next)).toBeNull(); - expect(next.runtime.activeModel).toBeNull(); - expect(next.activeThread.goal).toBeNull(); - expect(next.messageStream.historyCursor).toBeNull(); - expect(next.messageStream.loadingHistory).toBe(false); - expect(chatStateMessageStreamItems(next)).toEqual([]); - expect(next.messageStream.turnDiffs.size).toBe(0); - expect(next.requests.approvals).toEqual([]); - expect(next.requests.pendingUserInputs).toEqual([]); - expect(next.requests.userInputDrafts.size).toBe(0); - expect(next.composer.draft).toBe("draft in this panel"); - expect(next.composer.suggestSelected).toBe(1); - expect(next.composer.suggestions).toEqual([suggestion("/resume")]); - expect(next.composer.suggestionsDismissedSignature).toBe("dismissed"); - }); - it("updates map-backed turn diffs and toolbar panel state", () => { let state = chatStateFixture(); state = chatStateWith(state, { messageStream: { turnDiffs: new Map([["turn", "old"]]) } }); diff --git a/tests/features/chat/threads/active-thread-identity-sync.test.ts b/tests/features/chat/threads/active-thread-identity-sync.test.ts index 6fbd21ce..46fc4e79 100644 --- a/tests/features/chat/threads/active-thread-identity-sync.test.ts +++ b/tests/features/chat/threads/active-thread-identity-sync.test.ts @@ -33,7 +33,6 @@ function createController(options: { restoredThreadPending?: boolean } = {}) { stateStore, restoration, invalidateThreadWork: vi.fn(), - clearDeferredRestoredThreadHydration: vi.fn(), resetThreadTurnPresence: vi.fn(), notifyActiveThreadIdentityChanged: vi.fn(), refreshTabHeader: vi.fn(), @@ -61,7 +60,6 @@ describe("createActiveThreadIdentitySync", () => { expect(stateStore.getState().activeThread.id).toBeNull(); expect(host.invalidateThreadWork).toHaveBeenCalledOnce(); expect(restoredClear).toHaveBeenCalledOnce(); - expect(host.clearDeferredRestoredThreadHydration).toHaveBeenCalledOnce(); expect(host.resetThreadTurnPresence).toHaveBeenCalledWith(false); expect(host.notifyActiveThreadIdentityChanged).toHaveBeenCalledOnce(); expect(host.refreshTabHeader).not.toHaveBeenCalled(); @@ -86,12 +84,24 @@ describe("createActiveThreadIdentitySync", () => { expect(stateStore.getState().activeThread.id).toBe("active"); expect(host.invalidateThreadWork).not.toHaveBeenCalled(); expect(restoredClear).not.toHaveBeenCalled(); - expect(host.clearDeferredRestoredThreadHydration).not.toHaveBeenCalled(); expect(host.resetThreadTurnPresence).not.toHaveBeenCalled(); expect(host.notifyActiveThreadIdentityChanged).not.toHaveBeenCalled(); expect(host.refreshTabHeader).not.toHaveBeenCalled(); }); + it("clears pending restored thread identity when that thread is archived", () => { + const { controller, host, restoredClear, stateStore } = createController({ restoredThreadPending: true }); + + controller.applyThreadArchiveToActiveIdentity("thread"); + + expect(stateStore.getState().activeThread.id).toBeNull(); + expect(host.invalidateThreadWork).toHaveBeenCalledOnce(); + expect(restoredClear).toHaveBeenCalledOnce(); + expect(host.resetThreadTurnPresence).toHaveBeenCalledWith(false); + expect(host.notifyActiveThreadIdentityChanged).toHaveBeenCalledOnce(); + expect(host.refreshTabHeader).not.toHaveBeenCalled(); + }); + it("routes active thread rename notifications through active identity refresh", () => { const { controller, host, restoredRename, stateStore } = createController(); stateStore.dispatch({ diff --git a/tests/features/chat/threads/lifecycle-parts.test.ts b/tests/features/chat/threads/lifecycle-parts.test.ts index 274ca550..022f78a4 100644 --- a/tests/features/chat/threads/lifecycle-parts.test.ts +++ b/tests/features/chat/threads/lifecycle-parts.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import type { AppServerClient } from "../../../../src/app-server/connection/client"; -import { ChatResumeWorkTracker, type ChatViewDeferredTasks } from "../../../../src/features/chat/application/lifecycle"; +import { ChatResumeWorkTracker } from "../../../../src/features/chat/application/lifecycle"; import { createChatStateStore } from "../../../../src/features/chat/application/state/store"; import type { GoalActions } from "../../../../src/features/chat/application/threads/goal-actions"; import { HistoryController } from "../../../../src/features/chat/application/threads/history-controller"; @@ -27,11 +27,9 @@ describe("ThreadLifecycleParts", () => { ensureConnected: vi.fn().mockResolvedValue(undefined), }, lifecycle: { - deferredTasks: deferredTasks(), resumeWork, history, invalidateThreadWork, - getOpened: () => true, getClosing: () => false, }, thread: { @@ -61,15 +59,3 @@ describe("ThreadLifecycleParts", () => { expect(invalidateThreadWork).toHaveBeenCalledOnce(); }); }); - -function deferredTasks(): ChatViewDeferredTasks { - return { - scheduleDiagnostics: vi.fn(), - clearDiagnostics: vi.fn(), - scheduleRestoredThreadHydration: vi.fn(), - clearRestoredThreadHydration: vi.fn(), - scheduleAppServerWarmup: vi.fn(), - clearAppServerWarmup: vi.fn(), - clearAll: vi.fn(), - }; -} diff --git a/tests/features/chat/threads/restoration-controller.test.ts b/tests/features/chat/threads/restoration-controller.test.ts index 414d1dae..c4601cdd 100644 --- a/tests/features/chat/threads/restoration-controller.test.ts +++ b/tests/features/chat/threads/restoration-controller.test.ts @@ -2,34 +2,22 @@ import { describe, expect, it, vi } from "vitest"; -import { createChatState } from "../../../../src/features/chat/application/state/root-reducer"; -import { createChatStateStore } from "../../../../src/features/chat/application/state/store"; import { RestorationController } from "../../../../src/features/chat/application/threads/restoration-controller"; -import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/lifecycle"; import { deferred } from "../../../support/async"; describe("RestorationController", () => { - it("restores a placeholder and schedules deferred hydration", () => { - vi.useFakeTimers(); - const stateStore = createChatStateStore(createChatState()); + it("restores a placeholder for explicit hydration", async () => { const resumeThread = vi.fn().mockResolvedValue(undefined); - const controller = new RestorationController({ - deferredTasks: createChatViewDeferredTasks(() => window), - opened: () => true, - invalidateThreadWork: vi.fn(), - stateStore, - setStatus: vi.fn(), - refreshTabHeader: vi.fn(), - }); + const controller = restoredThreadControllerFixture(); controller.restore({ threadId: "thread", title: "Title", explicitName: null }); - controller.scheduleHydration(resumeThread); expect(controller.placeholder()).toMatchObject({ threadId: "thread", title: "Title" }); - expect(stateStore.getState().activeThread.id).toBe("thread"); - vi.advanceTimersByTime(1_500); + expect(resumeThread).not.toHaveBeenCalled(); + + await controller.ensureLoaded(resumeThread); + expect(resumeThread).toHaveBeenCalledWith("thread"); - vi.useRealTimers(); }); it("shares an in-flight restore load", async () => { @@ -61,10 +49,7 @@ describe("RestorationController", () => { function restoredThreadControllerFixture(overrides: Partial[0]> = {}) { return new RestorationController({ - deferredTasks: createChatViewDeferredTasks(() => window), - opened: () => false, invalidateThreadWork: vi.fn(), - stateStore: createChatStateStore(createChatState()), setStatus: vi.fn(), refreshTabHeader: vi.fn(), ...overrides, diff --git a/tests/features/chat/threads/resume-actions.test.ts b/tests/features/chat/threads/resume-actions.test.ts index 35be2f10..e8f19b56 100644 --- a/tests/features/chat/threads/resume-actions.test.ts +++ b/tests/features/chat/threads/resume-actions.test.ts @@ -50,7 +50,6 @@ function createActions(response: ChatThreadResumeSnapshot = activation("thread") closing: () => false, systemItem: (text: string) => ({ id: "system", kind: "system" as const, role: "system" as const, text }), resetThreadTurnPresence: vi.fn(), - clearDeferredRestoredThreadHydration: vi.fn(), notifyActiveThreadIdentityChanged: vi.fn(), addSystemMessage: vi.fn(), refreshLiveState: vi.fn(), diff --git a/tests/features/chat/view-connection.test.ts b/tests/features/chat/view-connection.test.ts index 787d0550..cc0b700e 100644 --- a/tests/features/chat/view-connection.test.ts +++ b/tests/features/chat/view-connection.test.ts @@ -347,7 +347,7 @@ describe("CodexChatView connection lifecycle", () => { expect(view.surface.openPanelSnapshot()).toMatchObject({ connected: false }); }); - it("restores the active thread from workspace state and hydrates it after a delay", async () => { + it("restores workspace thread state without hydrating it automatically", async () => { vi.useFakeTimers(); const client = connectedClient({ resumeThread: vi.fn().mockResolvedValue(resumedThread("thread-1")), @@ -360,6 +360,7 @@ describe("CodexChatView connection lifecycle", () => { expect(view.getDisplayText()).toBe("Codex: Restored thread"); expect(view.getState()).toEqual({ version: 1, threadId: "thread-1", threadTitle: "Restored thread" }); + expect(view.surface.openPanelSnapshot()).toMatchObject({ threadId: "thread-1" }); expect(connectionMock.state.connectCalls).toBe(0); expect(client.resumeThread).not.toHaveBeenCalled(); expect(view.containerEl.textContent).not.toContain("Thread restored. Send a message to resume it."); @@ -373,8 +374,8 @@ describe("CodexChatView connection lifecycle", () => { await vi.advanceTimersByTimeAsync(1_500); - expect(client.resumeThread).toHaveBeenCalledWith("thread-1", "/vault"); - expect(client.threadTurnsList).toHaveBeenCalledWith("thread-1", null, 20); + expect(client.resumeThread).not.toHaveBeenCalled(); + expect(client.threadTurnsList).not.toHaveBeenCalled(); }); it("formats the panel title from listed thread metadata", async () => { @@ -394,7 +395,7 @@ describe("CodexChatView connection lifecycle", () => { expect(view.getDisplayText()).toBe("Codex: 019e061e"); }); - it("hydrates a restored thread when workspace state arrives after open", async () => { + it("keeps late workspace thread state restored until explicit focus", async () => { vi.useFakeTimers(); const client = connectedClient(); connectionMock.state.client = client; @@ -410,6 +411,14 @@ describe("CodexChatView connection lifecycle", () => { await view.setState({ threadId: "thread-1", threadTitle: "Restored thread" }, {} as never); await vi.advanceTimersByTimeAsync(1_500); + expect(view.getDisplayText()).toBe("Codex: Restored thread"); + expect(view.getState()).toEqual({ version: 1, threadId: "thread-1", threadTitle: "Restored thread" }); + expect(view.surface.openPanelSnapshot()).toMatchObject({ threadId: "thread-1" }); + expect(client.resumeThread).not.toHaveBeenCalled(); + expect(client.threadTurnsList).not.toHaveBeenCalled(); + + await view.surface.focusThread("thread-1"); + expect(client.resumeThread).toHaveBeenCalledWith("thread-1", "/vault"); expect(client.threadTurnsList).toHaveBeenCalledWith("thread-1", null, 20); }); diff --git a/tests/features/chat/view-lifecycle.test.ts b/tests/features/chat/view-lifecycle.test.ts index 0c83d26a..e6ad3ff8 100644 --- a/tests/features/chat/view-lifecycle.test.ts +++ b/tests/features/chat/view-lifecycle.test.ts @@ -14,18 +14,15 @@ describe("createChatViewDeferredTasks", () => { it("clears scheduled deferred work", async () => { const tasks = createChatViewDeferredTasks(() => window); const diagnostics = vi.fn(); - const hydration = vi.fn(); const warmup = vi.fn(); tasks.scheduleDiagnostics(diagnostics); - tasks.scheduleRestoredThreadHydration(hydration); tasks.scheduleAppServerWarmup(warmup); tasks.clearAll(); await vi.advanceTimersByTimeAsync(1_500); expect(diagnostics).not.toHaveBeenCalled(); - expect(hydration).not.toHaveBeenCalled(); expect(warmup).not.toHaveBeenCalled(); }); });