Centralize connected client acquisition

This commit is contained in:
murashit 2026-06-21 15:15:48 +09:00
parent fe7f63e669
commit dbc00031b0
11 changed files with 73 additions and 66 deletions

View file

@ -22,7 +22,7 @@ export interface ComposerSubmitActionsHost {
sendTurnText(text: string, codexInputOverride?: CodexInput, referencedThread?: ReferencedThreadMetadata): Promise<void>;
};
connection: {
ensureConnected: () => Promise<void>;
connectedClient: () => Promise<AppServerClient | null>;
currentClient: () => AppServerClient | null;
};
status: {
@ -52,11 +52,9 @@ async function sendMessage(host: ComposerSubmitActionsHost): Promise<void> {
const text = host.composer.trimmedDraft;
if (!text) return;
await host.connection.ensureConnected();
if (!host.connection.currentClient()) return;
const slashCommand = parseSlashCommand(text);
if (slashCommand) {
if (!(await host.connection.connectedClient())) return;
host.composer.setDraft("", { clearSuggestions: true });
const result = await host.slashCommandExecutor.execute(slashCommand.command, slashCommand.args);
if (result?.composerDraft !== undefined) {

View file

@ -19,7 +19,7 @@ export interface ConversationTurnActionsContext {
localItemIds: LocalIdSource;
client: {
currentClient: () => AppServerClient | null;
ensureConnected: () => Promise<void>;
connectedClient: () => Promise<AppServerClient | null>;
};
status: {
set: (status: string) => void;
@ -64,8 +64,7 @@ interface ConversationThreadStarter {
export interface PlanImplementationHost {
stateStore: ChatStateStore;
currentClient(): AppServerClient | null;
ensureConnected(): Promise<void>;
connectedClient(): Promise<AppServerClient | null>;
sendTurnText(text: string): Promise<void>;
requestDefaultCollaborationModeForNextTurn(): void;
}
@ -88,7 +87,7 @@ export function createConversationTurnActions(
stateStore,
vaultPath,
localItemIds,
currentClient: client.currentClient,
connectedClient: client.connectedClient,
ensureRestoredThreadLoaded: thread.ensureRestoredThreadLoaded,
startThread: (preview) => refs.threadStarter.startThread(preview),
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
@ -121,8 +120,7 @@ export function createConversationTurnActions(
};
const planImplementationHost: PlanImplementationHost = {
stateStore,
currentClient: client.currentClient,
ensureConnected: client.ensureConnected,
connectedClient: client.connectedClient,
sendTurnText: (text) => turnSubmission.sendTurnText(text),
requestDefaultCollaborationModeForNextTurn: () => {
refs.runtimeSettings.requestDefaultCollaborationModeForNextTurn();
@ -141,8 +139,8 @@ export function createConversationTurnActions(
},
turnSubmission,
connection: {
connectedClient: client.connectedClient,
currentClient: client.currentClient,
ensureConnected: client.ensureConnected,
},
status: {
setStatus: status.set,
@ -168,8 +166,7 @@ async function startThreadForGoal(starter: ConversationThreadStarter, objective:
export async function implementPlan(host: PlanImplementationHost, itemId: string): Promise<void> {
if (!canImplementPlanItemId(host.stateStore.getState(), itemId)) return;
await host.ensureConnected();
if (!host.currentClient() || !activeThreadId(host.stateStore.getState())) return;
if (!(await host.connectedClient()) || !activeThreadId(host.stateStore.getState())) return;
host.requestDefaultCollaborationModeForNextTurn();
host.stateStore.dispatch({ type: "ui/panel-set", panel: null });

View file

@ -19,7 +19,7 @@ export interface TurnSubmissionActionsHost {
stateStore: ChatStateStore;
vaultPath: string;
localItemIds: LocalIdSource;
currentClient: () => AppServerClient | null;
connectedClient: () => Promise<AppServerClient | null>;
ensureRestoredThreadLoaded: () => Promise<boolean>;
startThread: (preview?: string) => Promise<unknown>;
notifyActiveThreadIdentityChanged: () => void;
@ -61,9 +61,9 @@ async function sendTurnText(
codexInputOverride?: CodexInput,
referencedThread?: ReferencedThreadMetadata,
): Promise<void> {
if (!(await host.ensureRestoredThreadLoaded())) return;
const client = host.currentClient();
const client = await host.connectedClient();
if (!client) return;
if (!(await host.ensureRestoredThreadLoaded())) return;
const initialState = submissionStateSnapshot(host.stateStore.getState());
const plan = planTurnSubmission(initialState);

View file

@ -16,7 +16,7 @@ export interface ThreadGoalSyncHost {
}
export interface GoalActionsHost extends ThreadGoalSyncHost {
ensureConnected: () => Promise<void>;
connectedClient: () => Promise<AppServerClient | null>;
startThread: (preview?: string, options?: { syncGoal?: boolean }) => Promise<{ threadId: string } | null>;
}
@ -125,8 +125,7 @@ function setGoalStatus(host: GoalActionsHost, threadId: string, status: ThreadGo
}
async function clearGoal(host: GoalActionsHost, threadId: string): Promise<boolean> {
await host.ensureConnected();
const client = host.currentClient();
const client = await host.connectedClient();
if (!client) return false;
try {
await client.clearThreadGoal(threadId);
@ -139,8 +138,7 @@ async function clearGoal(host: GoalActionsHost, threadId: string): Promise<boole
}
async function setGoal(host: GoalActionsHost, threadId: string, params: ThreadGoalUpdate): Promise<boolean> {
await host.ensureConnected();
const client = host.currentClient();
const client = await host.connectedClient();
if (!client) return false;
try {
return applyGoalIfActive(host, threadId, await setThreadGoal(client, threadId, params), { reportChange: true });
@ -193,7 +191,8 @@ async function startThreadAndSaveObjective(
plan: Extract<GoalObjectiveSavePlan, { kind: "start-thread-and-save" }>,
): Promise<boolean> {
try {
await host.ensureConnected();
const client = await host.connectedClient();
if (!client) return false;
const response = await host.startThread(plan.objective, { syncGoal: false });
const threadId = response?.threadId ?? null;
return threadId ? await setObjective(host, threadId, plan.objective, plan.tokenBudget) : false;

View file

@ -17,7 +17,7 @@ export interface ThreadManagementActionsHost {
stateStore: ChatStateStore;
vaultPath: string;
operations: ThreadOperations;
ensureConnected: () => Promise<void>;
connectedClient: () => Promise<AppServerClient | null>;
currentClient: () => AppServerClient | null;
addSystemMessage: (text: string) => void;
setStatus: (status: string) => void;
@ -101,8 +101,7 @@ async function compactActiveThread(host: ThreadManagementActionsHost): Promise<v
}
async function compactThread(host: ThreadManagementActionsHost, threadId: string): Promise<void> {
await host.ensureConnected();
const client = host.currentClient();
const client = await host.connectedClient();
if (!client) return;
const initialActiveThreadId = threadManagementState(host).activeThread.id;
try {
@ -148,8 +147,7 @@ async function forkThreadFromTurn(
host.addSystemMessage(finishBeforeForkingThreadsMessage());
return;
}
await host.ensureConnected();
const client = host.currentClient();
const client = await host.connectedClient();
if (!client) return;
const initialActiveThreadId = threadManagementState(host).activeThread.id;
@ -215,8 +213,7 @@ async function rollbackThread(host: ThreadManagementActionsHost, threadId: strin
host.addSystemMessage(interruptBeforeRollbackMessage());
return;
}
await host.ensureConnected();
const client = host.currentClient();
const client = await host.connectedClient();
if (!client) return;
const candidate = messageStreamRollbackCandidate(threadManagementState(host).messageStream);

View file

@ -170,6 +170,7 @@ interface ChatPanelComposerAndTurnInput {
connection: ConnectionManager;
localItemIds: LocalIdSource;
ensureConnected: () => Promise<void>;
connectedClient: () => Promise<ReturnType<CurrentAppServerClient>>;
currentClient: CurrentAppServerClient;
status: ChatPanelSessionStatus;
inboundHandler: ChatInboundHandler;
@ -256,10 +257,14 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
const connectionController = controller;
const { threads: serverThreads } = connectionBundle.serverActions;
const ensureConnected = () => connectionController.ensureConnected();
const connectedClient = async () => {
await ensureConnected();
return currentClient();
};
const refreshActiveThreads = () => connectionController.refreshActiveThreads();
const threadOperations = createSessionThreadOperations(environment, currentClient);
const runtimeSettings = createSessionRuntimeSettingsActions(host, currentClient, status);
const goals = createSessionGoalActions(host, currentClient, localItemIds, ensureConnected, status, serverThreads);
const goals = createSessionGoalActions(host, currentClient, localItemIds, connectedClient, status, serverThreads);
const rename = createSessionThreadRenameEditorActions(stateStore, threadOperations, titleService, ensureConnected, status);
const threadLifecycle = createSessionThreadLifecycle(
host,
@ -286,7 +291,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
};
const threadActionParts = createThreadActionParts(host, {
operations: threadOperations,
ensureConnected,
connectedClient,
currentClient,
status,
composerController,
@ -297,6 +302,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
connection,
localItemIds,
ensureConnected,
connectedClient,
currentClient,
status,
inboundHandler,
@ -579,7 +585,7 @@ function createSessionGoalActions(
host: ChatPanelSessionGraphHost,
currentClient: CurrentAppServerClient,
localItemIds: LocalIdSource,
ensureConnected: () => Promise<void>,
connectedClient: () => Promise<ReturnType<CurrentAppServerClient>>,
status: ChatPanelSessionStatus,
serverThreads: ChatServerThreadActions,
): ChatPanelGoalActions {
@ -587,7 +593,7 @@ function createSessionGoalActions(
stateStore: host.stateStore,
currentClient,
localItemIds,
ensureConnected,
connectedClient,
startThread: (preview, options) => serverThreads.startThread(preview, options),
addSystemMessage: (text) => {
status.addSystemMessage(text);
@ -719,7 +725,7 @@ function createThreadActionParts(
host: ChatPanelSessionGraphHost,
input: {
operations: ThreadOperations;
ensureConnected: () => Promise<void>;
connectedClient: () => Promise<ReturnType<CurrentAppServerClient>>;
currentClient: CurrentAppServerClient;
status: ChatPanelSessionStatus;
composerController: ChatComposerController;
@ -727,13 +733,13 @@ function createThreadActionParts(
refreshActiveThreads: () => Promise<void>;
},
): ChatPanelThreadActionParts {
const { operations, ensureConnected, currentClient, status, composerController, resume, refreshActiveThreads } = input;
const { operations, connectedClient, currentClient, status, composerController, resume, refreshActiveThreads } = input;
const { environment, stateStore } = host;
const threadManagementHost: ThreadManagementActionsHost = {
stateStore,
vaultPath: environment.plugin.settingsRef.vaultPath,
operations,
ensureConnected,
connectedClient,
currentClient,
addSystemMessage: status.addSystemMessage,
setStatus: status.set,
@ -777,6 +783,7 @@ function createComposerAndTurnActions(
connection,
localItemIds,
ensureConnected,
connectedClient,
currentClient,
status,
inboundHandler,
@ -831,7 +838,7 @@ function createComposerAndTurnActions(
localItemIds,
client: {
currentClient,
ensureConnected,
connectedClient,
},
status,
runtime: {

View file

@ -25,6 +25,7 @@ function createHost(draft: string) {
const sendTurnText = vi.fn().mockResolvedValue(undefined);
const execute = vi.fn().mockResolvedValue(undefined);
const followBottom = vi.fn();
const connectedClient = vi.fn().mockResolvedValue(client);
const host = {
stateStore,
composer: {
@ -36,8 +37,8 @@ function createHost(draft: string) {
slashCommandExecutor: { execute },
turnSubmission: { sendTurnText },
connection: {
connectedClient,
currentClient: () => client,
ensureConnected: vi.fn().mockResolvedValue(undefined),
},
status: {
setStatus: vi.fn(),
@ -45,16 +46,17 @@ function createHost(draft: string) {
},
scroll: { followBottom },
};
return { host, execute, followBottom, interruptTurn, sendTurnText, setDraft, stateStore };
return { host, connectedClient, execute, followBottom, interruptTurn, sendTurnText, setDraft, stateStore };
}
describe("submitComposer", () => {
it("sends plain drafts as turn text", async () => {
const { host, followBottom, sendTurnText } = createHost("hello");
const { host, connectedClient, followBottom, sendTurnText } = createHost("hello");
await submitComposer(host);
expect(followBottom).toHaveBeenCalledOnce();
expect(connectedClient).not.toHaveBeenCalled();
expect(sendTurnText).toHaveBeenCalledWith("hello");
const [followBottomOrder] = followBottom.mock.invocationCallOrder;
const [sendTurnTextOrder] = sendTurnText.mock.invocationCallOrder;
@ -65,24 +67,26 @@ describe("submitComposer", () => {
});
it("executes slash commands and forwards command send results", async () => {
const { host, execute, followBottom, sendTurnText, setDraft } = createHost("/clear hello");
const { host, connectedClient, execute, followBottom, sendTurnText, setDraft } = createHost("/clear hello");
execute.mockResolvedValue({ sendText: "hello" });
await submitComposer(host);
expect(setDraft).toHaveBeenCalledWith("", { clearSuggestions: true });
expect(connectedClient).toHaveBeenCalledOnce();
expect(execute).toHaveBeenCalledWith("clear", "hello");
expect(followBottom).toHaveBeenCalledOnce();
expect(sendTurnText).toHaveBeenCalledWith("hello", undefined, undefined);
});
it("restores slash command composer drafts from command results", async () => {
const { host, execute, followBottom, sendTurnText, setDraft } = createHost("/goal edit");
const { host, connectedClient, execute, followBottom, sendTurnText, setDraft } = createHost("/goal edit");
execute.mockResolvedValue({ composerDraft: "/goal set Current objective" });
await submitComposer(host);
expect(setDraft).toHaveBeenCalledWith("", { clearSuggestions: true });
expect(connectedClient).toHaveBeenCalledOnce();
expect(setDraft).toHaveBeenCalledWith("/goal set Current objective", { focus: true, clearSuggestions: true });
expect(followBottom).not.toHaveBeenCalled();
expect(sendTurnText).not.toHaveBeenCalled();

View file

@ -43,20 +43,19 @@ function resumeThread(stateStore: ChatStateStore, items: readonly MessageStreamI
function createController({ client = {} as AppServerClient } = {}) {
const stateStore = createChatStateStore(createChatState());
const ensureConnected = vi.fn().mockResolvedValue(undefined);
const connectedClient = vi.fn().mockResolvedValue(client);
const sendTurnText = vi.fn().mockResolvedValue(undefined);
const requestDefaultCollaborationModeForNextTurn = vi.fn(() => {
stateStore.dispatch({ type: "runtime/requested-collaboration-mode-set", collaborationMode: "default" });
});
const host: PlanImplementationHost = {
stateStore,
currentClient: () => client,
ensureConnected,
connectedClient,
sendTurnText,
requestDefaultCollaborationModeForNextTurn,
};
return {
ensureConnected,
connectedClient,
host,
requestDefaultCollaborationModeForNextTurn,
sendTurnText,
@ -88,14 +87,14 @@ describe("implementPlan", () => {
});
it("switches out of plan mode and submits the implementation prompt", async () => {
const { host, ensureConnected, requestDefaultCollaborationModeForNextTurn, sendTurnText, stateStore } = createController();
const { host, connectedClient, requestDefaultCollaborationModeForNextTurn, sendTurnText, stateStore } = createController();
const plan = planItem("plan");
resumeThread(stateStore, [plan]);
stateStore.dispatch({ type: "ui/panel-set", panel: "status-panel" });
await implementPlan(host, plan.id);
expect(ensureConnected).toHaveBeenCalledOnce();
expect(connectedClient).toHaveBeenCalledOnce();
expect(requestDefaultCollaborationModeForNextTurn).toHaveBeenCalledOnce();
expect(stateStore.getState().runtime.selectedCollaborationMode).toBe("default");
expect(stateStore.getState().ui.toolbarPanel).toBeNull();
@ -103,14 +102,14 @@ describe("implementPlan", () => {
});
it("ignores stale plan items", async () => {
const { host, ensureConnected, sendTurnText, stateStore } = createController();
const { host, connectedClient, sendTurnText, stateStore } = createController();
const first = planItem("first");
const latest = planItem("latest");
resumeThread(stateStore, [first, latest]);
await implementPlan(host, first.id);
expect(ensureConnected).not.toHaveBeenCalled();
expect(connectedClient).not.toHaveBeenCalled();
expect(sendTurnText).not.toHaveBeenCalled();
});
});

View file

@ -39,7 +39,7 @@ function createHost(overrides: TurnSubmissionHostOverrides = {}) {
const host: TurnSubmissionActionsHost = {
stateStore,
vaultPath: "/vault",
currentClient: () => client,
connectedClient: vi.fn().mockResolvedValue(client),
ensureRestoredThreadLoaded: vi.fn().mockResolvedValue(true),
startThread: vi.fn().mockImplementation(async () => {
stateStore.dispatch({

View file

@ -20,7 +20,7 @@ describe("createGoalActions", () => {
stateStore,
currentClient: () => client,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
ensureConnected: vi.fn().mockResolvedValue(undefined),
connectedClient: vi.fn().mockResolvedValue(client),
startThread: vi.fn().mockResolvedValue({ threadId: "thread" }),
addSystemMessage: vi.fn(),
addGoalEvent: vi.fn(),
@ -43,7 +43,7 @@ describe("createGoalActions", () => {
stateStore,
currentClient: () => client,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
ensureConnected: vi.fn().mockResolvedValue(undefined),
connectedClient: vi.fn().mockResolvedValue(client),
startThread: vi.fn().mockResolvedValue({ threadId: "thread" }),
addSystemMessage,
addGoalEvent: vi.fn(),
@ -76,7 +76,7 @@ describe("createGoalActions", () => {
stateStore,
currentClient: () => client,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
ensureConnected: vi.fn().mockResolvedValue(undefined),
connectedClient: vi.fn().mockResolvedValue(client),
startThread: vi.fn().mockResolvedValue({ threadId: "thread" }),
addSystemMessage,
addGoalEvent,
@ -109,7 +109,7 @@ describe("createGoalActions", () => {
stateStore,
currentClient: () => client,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
ensureConnected: vi.fn().mockResolvedValue(undefined),
connectedClient: vi.fn().mockResolvedValue(client),
startThread: vi.fn().mockResolvedValue({ threadId: "thread" }),
addSystemMessage,
addGoalEvent: vi.fn(),
@ -137,7 +137,7 @@ describe("createGoalActions", () => {
stateStore,
currentClient: () => client,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
ensureConnected: vi.fn().mockResolvedValue(undefined),
connectedClient: vi.fn().mockResolvedValue(client),
startThread: vi.fn().mockResolvedValue({ threadId: "thread" }),
addSystemMessage,
addGoalEvent: vi.fn(),
@ -166,7 +166,7 @@ describe("createGoalActions", () => {
stateStore,
currentClient: () => client,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
ensureConnected: vi.fn().mockResolvedValue(undefined),
connectedClient: vi.fn().mockResolvedValue(client),
startThread: vi.fn().mockResolvedValue({ threadId: "thread" }),
addSystemMessage,
addGoalEvent,
@ -218,7 +218,7 @@ describe("createGoalActions", () => {
stateStore,
currentClient: () => client,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
ensureConnected: vi.fn().mockResolvedValue(undefined),
connectedClient: vi.fn().mockResolvedValue(client),
startThread,
addSystemMessage: vi.fn(),
addGoalEvent: vi.fn(),
@ -240,14 +240,14 @@ describe("createGoalActions", () => {
it("rejects empty goal objective saves before connecting or starting a thread", async () => {
const stateStore = createChatStateStore(chatStateFixture());
const ensureConnected = vi.fn().mockResolvedValue(undefined);
const connectedClient = vi.fn().mockResolvedValue({ setThreadGoal: vi.fn() } as unknown as AppServerClient);
const startThread = vi.fn().mockResolvedValue({ threadId: "thread" });
const addSystemMessage = vi.fn();
const controller = createGoalActions({
stateStore,
currentClient: () => ({ setThreadGoal: vi.fn() }) as unknown as AppServerClient,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
ensureConnected,
connectedClient,
startThread,
addSystemMessage,
addGoalEvent: vi.fn(),
@ -257,7 +257,7 @@ describe("createGoalActions", () => {
await expect(controller.saveObjective(" ", null)).resolves.toBe(false);
expect(addSystemMessage).toHaveBeenCalledWith("Goal objective cannot be empty.");
expect(ensureConnected).not.toHaveBeenCalled();
expect(connectedClient).not.toHaveBeenCalled();
expect(startThread).not.toHaveBeenCalled();
});
@ -273,7 +273,7 @@ describe("createGoalActions", () => {
stateStore,
currentClient: () => client,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
ensureConnected: vi.fn().mockResolvedValue(undefined),
connectedClient: vi.fn().mockResolvedValue(client),
startThread: vi.fn().mockResolvedValue({ threadId: "thread" }),
addSystemMessage,
addGoalEvent: vi.fn(),
@ -300,7 +300,7 @@ describe("createGoalActions", () => {
stateStore,
currentClient: () => client,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
ensureConnected: vi.fn().mockResolvedValue(undefined),
connectedClient: vi.fn().mockResolvedValue(client),
startThread: vi.fn().mockResolvedValue({ threadId: "thread" }),
addSystemMessage,
addGoalEvent: vi.fn(),
@ -325,7 +325,7 @@ describe("createGoalActions", () => {
stateStore,
currentClient: () => client,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
ensureConnected: vi.fn().mockResolvedValue(undefined),
connectedClient: vi.fn().mockResolvedValue(client),
startThread: vi.fn().mockResolvedValue({ threadId: "thread" }),
addSystemMessage: vi.fn(),
addGoalEvent,
@ -351,7 +351,7 @@ describe("createGoalActions", () => {
stateStore,
currentClient: () => client,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
ensureConnected: vi.fn().mockResolvedValue(undefined),
connectedClient: vi.fn().mockResolvedValue(client),
startThread: vi.fn().mockResolvedValue({ threadId: "thread" }),
addSystemMessage,
addGoalEvent,
@ -375,7 +375,7 @@ describe("createGoalActions", () => {
stateStore,
currentClient: () => client,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
ensureConnected: vi.fn().mockResolvedValue(undefined),
connectedClient: vi.fn().mockResolvedValue(client),
startThread: vi.fn().mockResolvedValue({ threadId: "thread" }),
addSystemMessage,
addGoalEvent: vi.fn(),

View file

@ -31,6 +31,7 @@ describe("thread management actions", () => {
await controller.compactThread("source");
expect(host.connectedClient).toHaveBeenCalledOnce();
expect(host.ensureConnected).toHaveBeenCalledOnce();
expect(client.compactThread).toHaveBeenCalledWith("source");
expect(host.addSystemMessage).toHaveBeenCalledWith("Compaction requested.");
@ -531,10 +532,15 @@ function hostMock({
const notifyThreadRenamed = vi.fn();
const showNotice = vi.fn();
const ensureConnected = vi.fn().mockResolvedValue(undefined);
const connectedClient = vi.fn(async () => {
await ensureConnected();
return (currentClient ?? (() => client as unknown as AppServerClient))();
});
return {
stateStore,
vaultPath: "/vault",
ensureConnected,
connectedClient,
currentClient: currentClient ?? (() => client as unknown as AppServerClient),
addSystemMessage: vi.fn(),
setStatus: vi.fn(),