mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Separate restored placeholders from active chat state
This commit is contained in:
parent
711e300f1b
commit
18327ff883
19 changed files with 52 additions and 183 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
};
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<boolean> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ export interface ResumeActionsHost {
|
|||
ensureConnected: () => Promise<void>;
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -223,7 +223,6 @@ describe("createChatPanelSessionGraph actions", () => {
|
|||
resumeWork,
|
||||
connectionWork: new ConnectionWorkTracker(),
|
||||
messageScrollController: createChatMessageScrollController(),
|
||||
getOpened: () => true,
|
||||
getClosing: () => false,
|
||||
viewWindow: () => window,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"]]) } });
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ConstructorParameters<typeof RestorationController>[0]> = {}) {
|
||||
return new RestorationController({
|
||||
deferredTasks: createChatViewDeferredTasks(() => window),
|
||||
opened: () => false,
|
||||
invalidateThreadWork: vi.fn(),
|
||||
stateStore: createChatStateStore(createChatState()),
|
||||
setStatus: vi.fn(),
|
||||
refreshTabHeader: vi.fn(),
|
||||
...overrides,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue