Clarify controller-adjacent naming

This commit is contained in:
murashit 2026-07-02 10:01:25 +09:00
parent 8568b224dc
commit d5390d9752
29 changed files with 270 additions and 273 deletions

View file

@ -21,7 +21,7 @@ export interface ChatConnectionDiagnosticsActions {
refreshServerDiagnostics: (options?: { appServerMetadataSnapshot?: boolean; forceResourceProbes?: boolean }) => Promise<void>;
}
export interface ChatConnectionControllerHost {
export interface ChatConnectionActionsHost {
stateStore: ChatStateStore;
connection: ChatConnectionAdapter;
connectionWork: ConnectionWorkTracker;
@ -43,7 +43,7 @@ export interface ChatConnectionControllerHost {
}
type ChatConnectionExitHost = Pick<
ChatConnectionControllerHost,
ChatConnectionActionsHost,
"connectionWork" | "invalidateThreadWork" | "setStatus" | "stateStore" | "resetThreadTurnPresence" | "refreshLiveState"
>;
@ -56,7 +56,7 @@ export function handleChatConnectionExit(host: ChatConnectionExitHost): void {
host.refreshLiveState();
}
export interface ChatConnectionController {
export interface ChatConnectionActions {
ensureConnected(): Promise<void>;
invalidate(): void;
handleExit(): void;
@ -65,8 +65,8 @@ export interface ChatConnectionController {
refreshStatusPanel(): Promise<void>;
}
export function createChatConnectionController(host: ChatConnectionControllerHost): ChatConnectionController {
const controller: ChatConnectionController = {
export function createChatConnectionActions(host: ChatConnectionActionsHost): ChatConnectionActions {
const actions: ChatConnectionActions = {
ensureConnected: () => ensureConnected(host),
invalidate: () => {
host.connectionWork.invalidate();
@ -75,13 +75,13 @@ export function createChatConnectionController(host: ChatConnectionControllerHos
handleChatConnectionExit(host);
},
refreshActiveThreads: () => refreshActiveThreads(host),
refreshDiagnostics: () => refreshDiagnostics(host, controller),
refreshStatusPanel: () => refreshStatusPanel(host, controller),
refreshDiagnostics: () => refreshDiagnostics(host, actions),
refreshStatusPanel: () => refreshStatusPanel(host, actions),
};
return controller;
return actions;
}
async function ensureConnected(host: ChatConnectionControllerHost): Promise<void> {
async function ensureConnected(host: ChatConnectionActionsHost): Promise<void> {
const connecting = host.connectionWork.active();
if (connecting?.promise) return connecting.promise;
@ -99,7 +99,7 @@ async function ensureConnected(host: ChatConnectionControllerHost): Promise<void
}
}
async function refreshActiveThreads(host: ChatConnectionControllerHost): Promise<void> {
async function refreshActiveThreads(host: ChatConnectionActionsHost): Promise<void> {
if (!host.connection.isConnected()) return;
try {
await host.refreshSharedThreads();
@ -110,12 +110,9 @@ async function refreshActiveThreads(host: ChatConnectionControllerHost): Promise
}
}
async function refreshDiagnostics(
host: ChatConnectionControllerHost,
controller: Pick<ChatConnectionController, "ensureConnected">,
): Promise<void> {
async function refreshDiagnostics(host: ChatConnectionActionsHost, actions: Pick<ChatConnectionActions, "ensureConnected">): Promise<void> {
host.clearDeferredDiagnostics();
await controller.ensureConnected();
await actions.ensureConnected();
if (!host.connection.isConnected()) return;
host.clearDeferredDiagnostics();
await host.metadata.refreshAppServerMetadata();
@ -123,18 +120,18 @@ async function refreshDiagnostics(
}
async function refreshStatusPanel(
host: ChatConnectionControllerHost,
controller: Pick<ChatConnectionController, "refreshActiveThreads" | "refreshDiagnostics">,
host: ChatConnectionActionsHost,
actions: Pick<ChatConnectionActions, "refreshActiveThreads" | "refreshDiagnostics">,
): Promise<void> {
try {
await controller.refreshDiagnostics();
await actions.refreshDiagnostics();
} catch (error) {
host.addSystemMessage(error instanceof Error ? error.message : String(error));
}
await controller.refreshActiveThreads();
await actions.refreshActiveThreads();
}
async function initializeConnection(host: ChatConnectionControllerHost, connection: ActiveConnectionWork): Promise<void> {
async function initializeConnection(host: ChatConnectionActionsHost, connection: ActiveConnectionWork): Promise<void> {
host.setStatus(STATUS_CONNECTION_STARTING, { kind: "connecting" });
try {
const initialization = await host.connection.connect();

View file

@ -5,7 +5,7 @@ import { runtimeSnapshotForChatState } from "../../application/runtime/snapshot"
import type { ChatStateStore } from "../../application/state/store";
import { resolveRuntimeControls } from "../../domain/runtime/resolution";
import { ChatComposerController } from "../../panel/composer-controller";
import type { ChatMessageScrollController } from "../../panel/message-stream-scroll-controller";
import type { ChatMessageStreamScrollBinding } from "../../panel/message-stream-scroll-binding";
import { chatPanelComposerProjection } from "../../panel/surface/composer-projection";
import type { ChatPanelEnvironment } from "../contracts";
import { createVaultComposerAttachmentHandler } from "../obsidian/composer-attachments.obsidian";
@ -16,7 +16,7 @@ import type { ChatPanelRuntimeSettingsActions } from "./runtime-bundle";
interface ChatPanelComposerHost {
environment: ChatPanelEnvironment;
stateStore: ChatStateStore;
messageScrollController: ChatMessageScrollController;
messageScrollBinding: ChatMessageStreamScrollBinding;
}
export function createChatComposerController(
@ -52,7 +52,7 @@ export function createChatComposerController(
return resolveRuntimeControls(runtimeSnapshotForChatState(current), config).model.effective;
},
threadScrollFromComposer: (action) => {
host.messageScrollController.scrollFromComposer(action);
host.messageScrollBinding.scrollFromComposer(action);
},
togglePlan: () => void input.runtimeSettings.toggleCollaborationMode(),
toggleAutoReview: () => void input.runtimeSettings.toggleAutoReview(),

View file

@ -8,10 +8,10 @@ import { type ChatServerMetadataActions, createChatServerMetadataActions } from
import { type ChatServerThreadActions, createChatServerThreadActions } from "../../app-server/actions/threads";
import { type ChatInboundHandler, createChatInboundHandler } from "../../app-server/inbound/handler";
import {
type ChatConnectionController,
createChatConnectionController,
type ChatConnectionActions,
createChatConnectionActions,
handleChatConnectionExit,
} from "../../application/connection/connection-controller";
} from "../../application/connection/connection-actions";
import type { ConnectionWorkTracker } from "../../application/connection/connection-work";
import type { LocalIdSource } from "../../application/local-id-source";
import { runtimeSnapshotForChatState } from "../../application/runtime/snapshot";
@ -56,7 +56,7 @@ interface ChatPanelConnectionBundleHost {
export interface ChatPanelConnectionBundle {
connection: {
manager: ConnectionManager;
controller: ChatConnectionController;
actions: ChatConnectionActions;
};
inboundHandler: ChatInboundHandler;
serverActions: {
@ -192,7 +192,7 @@ export function createConnectionBundle(
host.refreshLiveState();
},
};
const connectionController = createChatConnectionController({
const connectionActions = createChatConnectionActions({
...connectionExitHost,
connection: {
connect: () =>
@ -255,7 +255,7 @@ export function createConnectionBundle(
return {
connection: {
manager: connection,
controller: connectionController,
actions: connectionActions,
},
inboundHandler,
serverActions: {

View file

@ -4,7 +4,7 @@ import type { ChatStateStore } from "../../application/state/store";
import type { HistoryController } from "../../application/threads/history-controller";
import type { ThreadRenameEditorActions } from "../../application/threads/rename-editor-actions";
import type { ChatComposerController } from "../../panel/composer-controller";
import type { ChatMessageScrollController } from "../../panel/message-stream-scroll-controller";
import type { ChatMessageStreamScrollBinding } from "../../panel/message-stream-scroll-binding";
import type { ChatPanelShellParts } from "../../panel/shell.dom";
import type { ChatPanelGoalSurface } from "../../panel/surface/goal-projection";
import { MessageStreamPresenter } from "../../panel/surface/message-stream-presenter";
@ -19,12 +19,12 @@ import type { ChatPanelTurnBundle } from "./turn-bundle";
interface ChatPanelShellBundleHost {
environment: ChatPanelEnvironment;
stateStore: ChatStateStore;
messageScrollController: ChatMessageScrollController;
messageScrollBinding: ChatMessageStreamScrollBinding;
}
interface ChatPanelShellBundleInput {
connection: ConnectionManager;
connectionController: ChatPanelConnectionBundle["connection"]["controller"];
connectionActions: ChatPanelConnectionBundle["connection"]["actions"];
goals: ChatPanelGoalActions;
rename: ThreadRenameEditorActions;
threadActions: ChatPanelThreadActions;
@ -46,7 +46,7 @@ export interface ChatPanelShellBundle {
export function createShellBundle(host: ChatPanelShellBundleHost, input: ChatPanelShellBundleInput): ChatPanelShellBundle {
const {
connection,
connectionController,
connectionActions,
goals,
rename,
threadActions,
@ -60,7 +60,7 @@ export function createShellBundle(host: ChatPanelShellBundleHost, input: ChatPan
} = input;
const { environment, stateStore } = host;
const toolbarActions = createToolbarUiActions({
connectionController,
connectionActions,
reconnectPanel: reconnect,
threadActions,
goals,
@ -97,9 +97,9 @@ export function createShellBundle(host: ChatPanelShellBundleHost, input: ChatPan
vaultPath: environment.plugin.settingsRef.vaultPath,
},
scroll: {
controller: host.messageScrollController,
portBinding: host.messageScrollBinding,
dispose: () => {
host.messageScrollController.dispose();
host.messageScrollBinding.dispose();
},
},
history: {

View file

@ -44,7 +44,7 @@ interface ChatPanelThreadHost {
environment: ChatPanelEnvironment;
stateStore: ChatStateStore;
resumeWork: ChatResumeWorkTracker;
messageScrollController: {
messageScrollBinding: {
showLatest(): void;
};
getClosing: () => boolean;
@ -268,7 +268,7 @@ function createSessionHistoryController(
historyTransport: appServer.threadHistory,
addSystemMessage: status.addSystemMessage,
showLatestPageAtBottom: () => {
host.messageScrollController.showLatest();
host.messageScrollBinding.showLatest();
},
setThreadTurnPresence: (hadTurns) => {
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);

View file

@ -28,7 +28,7 @@ interface ChatPanelTurnStatus {
interface ChatPanelTurnHost {
stateStore: ChatStateStore;
messageScrollController: {
messageScrollBinding: {
showLatest(): void;
};
}
@ -135,7 +135,7 @@ export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnIn
},
scroll: {
showLatest: () => {
host.messageScrollController.showLatest();
host.messageScrollBinding.showLatest();
},
},
},

View file

@ -12,7 +12,7 @@ import type { ChatResumeWorkTracker } from "../application/threads/resume-work";
import { createStructuredSystemItem, createSystemItem } from "../domain/message-stream/factories/system-items";
import type { MessageStreamNoticeSection } from "../domain/message-stream/items";
import type { ChatComposerController } from "../panel/composer-controller";
import type { ChatMessageScrollController } from "../panel/message-stream-scroll-controller";
import type { ChatMessageStreamScrollBinding } from "../panel/message-stream-scroll-binding";
import { createChatComposerController } from "./bundles/composer-bundle";
import { type ChatPanelConnectionBundle, createConnectionBundle } from "./bundles/connection-bundle";
import { createReconnectAction } from "./bundles/reconnect-bundle";
@ -27,7 +27,7 @@ import { type ChatPanelSharedStateBinding, createChatPanelSharedStateBinding } f
export interface ChatPanelSessionGraph {
connection: {
manager: ConnectionManager;
controller: ChatPanelConnectionBundle["connection"]["controller"];
actions: ChatPanelConnectionBundle["connection"]["actions"];
};
thread: {
resume: ResumeActions;
@ -63,7 +63,7 @@ interface ChatPanelSessionGraphHost {
deferredTasks: ChatViewDeferredTasks;
resumeWork: ChatResumeWorkTracker;
connectionWork: ConnectionWorkTracker;
messageScrollController: ChatMessageScrollController;
messageScrollBinding: ChatMessageStreamScrollBinding;
getClosing: () => boolean;
viewWindow: () => Window;
}
@ -74,7 +74,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
const connection = createConnectionManager(environment);
const currentClient = () => connection.currentClient();
let ensureConnected: () => Promise<void> = async () => {
throw new Error("Codex app-server connection controller is not initialized.");
throw new Error("Codex app-server connection actions are not initialized.");
};
const connectedClient = async () => {
await ensureConnected();
@ -130,12 +130,12 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
},
);
const {
connection: { controller: connectionController },
connection: { actions: connectionActions },
inboundHandler,
} = connectionBundle;
const { threads: serverThreads } = connectionBundle.serverActions;
ensureConnected = () => connectionController.ensureConnected();
const refreshActiveThreads = () => connectionController.refreshActiveThreads();
ensureConnected = () => connectionActions.ensureConnected();
const refreshActiveThreads = () => connectionActions.refreshActiveThreads();
const runtime = createRuntimeBundle(host, {
connection,
appServer,
@ -188,13 +188,13 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
autoTitleCoordinator: threadFoundation.autoTitleCoordinator,
reconnect,
runtimeProjection: runtime.projection,
refreshDiagnostics: () => connectionController.refreshDiagnostics(),
refreshDiagnostics: () => connectionActions.refreshDiagnostics(),
refreshLiveState,
notifyActiveThreadIdentityChanged,
});
const shell = createShellBundle(host, {
connection,
connectionController,
connectionActions,
goals: threadLifecycle.goals,
rename: threadLifecycle.rename,
threadActions: threadActions.actions,
@ -225,7 +225,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
return {
connection: {
manager: connection,
controller: connectionController,
actions: connectionActions,
},
thread: {
resume: threadLifecycle.resume,

View file

@ -7,7 +7,7 @@ import type { ChatState } from "../application/state/root-reducer";
import { type ChatStateStore, createChatStateStore } from "../application/state/store";
import { parseRestoredThreadState, type RestoredThreadPlaceholderState } from "../application/threads/restored-thread-lifecycle";
import { ChatResumeWorkTracker } from "../application/threads/resume-work";
import { type ChatMessageScrollController, createChatMessageScrollController } from "../panel/message-stream-scroll-controller";
import { type ChatMessageStreamScrollBinding, createChatMessageStreamScrollBinding } from "../panel/message-stream-scroll-binding";
import { renderChatPanelShell, unmountChatPanelShell } from "../panel/shell.dom";
import type { ChatPanelEnvironment, ChatPanelHandle, ChatWorkspacePanelSnapshot, ChatWorkspacePanelTurnLifecycle } from "./contracts";
import { type ChatViewDeferredTasks, createChatViewDeferredTasks } from "./session/deferred-work";
@ -20,7 +20,7 @@ export class ChatPanelSession implements ChatPanelHandle {
private readonly deferredTasks: ChatViewDeferredTasks;
private readonly connectionWork = new ConnectionWorkTracker();
private readonly resumeWork = new ChatResumeWorkTracker();
private readonly messageScrollController: ChatMessageScrollController = createChatMessageScrollController();
private readonly messageScrollBinding: ChatMessageStreamScrollBinding = createChatMessageStreamScrollBinding();
private observedAppServerContext: AppServerQueryContext;
private opened = false;
private closing = false;
@ -175,7 +175,7 @@ export class ChatPanelSession implements ChatPanelHandle {
}
async connect(): Promise<void> {
await this.graph.connection.controller.ensureConnected();
await this.graph.connection.actions.ensureConnected();
}
async startNewThread(): Promise<void> {
@ -202,7 +202,7 @@ export class ChatPanelSession implements ChatPanelHandle {
this.deferredTasks.scheduleAppServerWarmup(() => {
if (!shouldWarmup() || this.closing) return;
void this.graph.connection.controller.ensureConnected();
void this.graph.connection.actions.ensureConnected();
});
}
@ -251,7 +251,7 @@ export class ChatPanelSession implements ChatPanelHandle {
deferredTasks: this.deferredTasks,
resumeWork: this.resumeWork,
connectionWork: this.connectionWork,
messageScrollController: this.messageScrollController,
messageScrollBinding: this.messageScrollBinding,
getClosing: () => this.closing,
viewWindow: () => this.viewWindow(),
});

View file

@ -42,7 +42,7 @@ import {
composerTextBeforeCursor,
composerTransferHasFiles,
focusComposer,
} from "./composer-controller.dom";
} from "./composer-element.dom";
import type { ChatPanelComposerReadModel } from "./shell-read-model";
import type { ChatPanelComposerProjection } from "./surface/composer-projection";

View file

@ -1,17 +1,17 @@
import type { ComposerBoundaryScrollAction } from "../application/composer/boundary-scroll";
import type {
MessageStreamScrollCommand,
MessageStreamScrollControllerBinding,
MessageStreamScrollPort,
MessageStreamScrollPortBinding,
} from "../ui/message-stream/flow-scroll.measure";
export interface ChatMessageScrollController extends MessageStreamScrollControllerBinding {
export interface ChatMessageStreamScrollBinding extends MessageStreamScrollPortBinding {
showLatest(): void;
scrollFromComposer(action: ComposerBoundaryScrollAction): void;
dispose(): void;
}
export function createChatMessageScrollController(): ChatMessageScrollController {
export function createChatMessageStreamScrollBinding(): ChatMessageStreamScrollBinding {
let scrollPort: MessageStreamScrollPort | null = null;
const dispatch = (command: MessageStreamScrollCommand): void => {

View file

@ -6,7 +6,7 @@ import type { TurnDiffViewState } from "../../../turn-diff/model";
import type { PendingRequestBlockActions } from "../../application/pending-requests/block";
import type { ChatAction } from "../../application/state/root-reducer";
import type { ChatStateStore } from "../../application/state/store";
import type { MessageStreamScrollControllerBinding } from "../../ui/message-stream/flow-scroll.measure";
import type { MessageStreamScrollPortBinding } from "../../ui/message-stream/flow-scroll.measure";
import { MarkdownMessageRenderer, renderStreamMarkdown } from "../../ui/message-stream/markdown-renderer.obsidian";
import { MessageStreamViewport, type MessageStreamViewportState } from "../../ui/message-stream/stream-blocks";
import type { ChatPanelMessageStreamReadModel } from "../shell-read-model";
@ -59,7 +59,7 @@ interface MessageStreamPresenterWorkspaceContext {
}
interface MessageStreamPresenterScrollContext {
controller: MessageStreamScrollControllerBinding;
portBinding: MessageStreamScrollPortBinding;
dispose: () => void;
}
@ -102,7 +102,7 @@ export class MessageStreamPresenter {
return {
blocks: projection.blocks,
context: projection.context,
scrollController: this.options.scroll.controller,
scrollPortBinding: this.options.scroll.portBinding,
};
}

View file

@ -1,5 +1,5 @@
import { copyTextWithNotice } from "../../../shared/obsidian/clipboard.obsidian";
import type { ChatConnectionController } from "../application/connection/connection-controller";
import type { ChatConnectionActions } from "../application/connection/connection-actions";
import type { ChatAction, ChatState } from "../application/state/root-reducer";
import type { ChatStateStore } from "../application/state/store";
import type { GoalActions } from "../application/threads/goal-actions";
@ -26,7 +26,7 @@ export interface ToolbarPanelActions {
}
export interface ToolbarUiActionDependencies {
connectionController: ChatConnectionController;
connectionActions: ChatConnectionActions;
reconnectPanel: () => Promise<void>;
threadActions: ThreadManagementActions;
goals: GoalActions;
@ -146,7 +146,7 @@ export function createToolbarUiActions(deps: ToolbarUiActionDependencies): Toolb
void deps.reconnectPanel();
},
refreshStatus: () => {
void deps.connectionController.refreshStatusPanel();
void deps.connectionActions.refreshStatusPanel();
},
copyDebugDetails: (details) => {
void copyTextWithNotice(details, "Copied debug details.", "Could not copy debug details.");

View file

@ -17,7 +17,7 @@ export interface MessageStreamScrollPort {
dispatchScrollCommand(command: MessageStreamScrollCommand): void;
}
export interface MessageStreamScrollControllerBinding {
export interface MessageStreamScrollPortBinding {
mountScrollPort(port: MessageStreamScrollPort): () => void;
}
@ -28,7 +28,7 @@ export interface MessageStreamFlowBlockIdentity {
export interface MessageStreamFlowFrameProps<Block extends MessageStreamFlowBlockIdentity> {
blocks: readonly Block[];
rootAttributes?: Partial<Record<`data-${string}`, string>>;
scrollController: MessageStreamScrollControllerBinding;
scrollPortBinding: MessageStreamScrollPortBinding;
renderBlockContent: (block: Block) => UiNode;
}
@ -71,7 +71,7 @@ export class MessageStreamFlowFrame<Block extends MessageStreamFlowBlockIdentity
_previousState: Readonly<Record<string, never>>,
snapshot: MessageFlowSnapshot | null,
): void {
if (previousProps.scrollController !== this.props.scrollController) this.mountScrollPort();
if (previousProps.scrollPortBinding !== this.props.scrollPortBinding) this.mountScrollPort();
if (this.runtime.container !== this.scrollElement) attachMessageFlowContainer(this.runtime, this.scrollElement);
completeMessageFlowRender(this.runtime, snapshot);
}
@ -119,7 +119,7 @@ export class MessageStreamFlowFrame<Block extends MessageStreamFlowBlockIdentity
private mountScrollPort(): void {
this.unmountScrollPort?.();
this.unmountScrollPort = this.props.scrollController.mountScrollPort(this.scrollPort);
this.unmountScrollPort = this.props.scrollPortBinding.mountScrollPort(this.scrollPort);
}
}

View file

@ -7,7 +7,7 @@ import type {
} from "../../presentation/message-stream/view-model";
import type { MessageStreamContext, PendingRequestBlockContext } from "./context";
import { detailNode } from "./detail";
import { MessageStreamFlowFrame, type MessageStreamScrollControllerBinding } from "./flow-scroll.measure";
import { MessageStreamFlowFrame, type MessageStreamScrollPortBinding } from "./flow-scroll.measure";
import { pendingRequestBlockNode } from "./pending-request-block";
import { agentRunSummaryNode, statusNode } from "./status";
import { textNode } from "./text";
@ -15,7 +15,7 @@ import { textNode } from "./text";
export interface MessageStreamViewportState {
blocks: readonly MessageStreamViewBlock[];
context: MessageStreamContext;
scrollController: MessageStreamScrollControllerBinding;
scrollPortBinding: MessageStreamScrollPortBinding;
}
interface MessageStreamViewportProps {
@ -24,11 +24,11 @@ interface MessageStreamViewportProps {
}
export function MessageStreamViewport({ state, rootAttributes }: MessageStreamViewportProps): UiNode {
const { blocks, context, scrollController } = state;
const { blocks, context, scrollPortBinding } = state;
return (
<MessageStreamFlowFrame
blocks={blocks}
scrollController={scrollController}
scrollPortBinding={scrollPortBinding}
renderBlockContent={(block) => <MessageStreamBlockContent block={block} context={context} />}
{...(rootAttributes ? { rootAttributes } : {})}
/>

View file

@ -52,7 +52,7 @@ describe("chat app-server actions", () => {
}),
);
const controller = createChatServerThreadActions({
const actions = createChatServerThreadActions({
stateStore,
vaultPath: "/vault",
currentClient: () => client,
@ -61,7 +61,7 @@ describe("chat app-server actions", () => {
syncThreadGoal,
});
await controller.startThread("first prompt");
await actions.startThread("first prompt");
expect(stateStore.getState().threadList.listedThreads).toEqual([optimistic, existingThread]);
expect(applyThreadCatalogEvent).toHaveBeenCalledWith({ type: "thread-started", thread: optimistic });
@ -86,7 +86,7 @@ describe("chat app-server actions", () => {
});
const client = startThreadClient(startThread);
const controller = createChatServerThreadActions({
const actions = createChatServerThreadActions({
stateStore,
vaultPath: "/vault",
currentClient: () => client,
@ -95,7 +95,7 @@ describe("chat app-server actions", () => {
syncThreadGoal: vi.fn(),
});
await controller.startThread("first prompt");
await actions.startThread("first prompt");
expect(startThread).toHaveBeenCalledWith({
cwd: "/vault",
@ -125,7 +125,7 @@ describe("chat app-server actions", () => {
}),
);
const controller = createChatServerThreadActions({
const actions = createChatServerThreadActions({
stateStore,
vaultPath: "/vault",
currentClient: () => client,
@ -134,7 +134,7 @@ describe("chat app-server actions", () => {
syncThreadGoal,
});
await controller.startThread("first goal", { syncGoal: false });
await actions.startThread("first goal", { syncGoal: false });
expect(syncThreadGoal).not.toHaveBeenCalled();
});
@ -154,7 +154,7 @@ describe("chat app-server actions", () => {
});
const client = startThreadClient(startThread);
const controller = createChatServerThreadActions({
const actions = createChatServerThreadActions({
stateStore,
vaultPath: "/vault",
currentClient: () => client,
@ -163,7 +163,7 @@ describe("chat app-server actions", () => {
syncThreadGoal: vi.fn(),
});
await controller.startThread();
await actions.startThread();
expect(startThread).toHaveBeenCalledWith({
cwd: "/vault",
@ -187,7 +187,7 @@ describe("chat app-server actions", () => {
}),
);
const controller = createChatServerThreadActions({
const actions = createChatServerThreadActions({
stateStore,
vaultPath: "/vault",
currentClient: () => client,
@ -196,7 +196,7 @@ describe("chat app-server actions", () => {
syncThreadGoal: () => undefined,
});
await controller.startThread("local preview");
await actions.startThread("local preview");
expect(applyThreadCatalogEvent).toHaveBeenCalledWith({ type: "thread-started", thread: threadFromThreadRecord(started) });
});
@ -209,7 +209,7 @@ describe("chat app-server actions", () => {
let currentClient = firstClient;
const applyThreadCatalogEvent = vi.fn();
const syncThreadGoal = vi.fn();
const controller = createChatServerThreadActions({
const actions = createChatServerThreadActions({
stateStore,
vaultPath: "/vault",
currentClient: () => currentClient,
@ -218,7 +218,7 @@ describe("chat app-server actions", () => {
syncThreadGoal,
});
const starting = controller.startThread("local preview");
const starting = actions.startThread("local preview");
currentClient = secondClient;
start.resolve({
thread: threadFixture("stale-started"),
@ -419,7 +419,7 @@ describe("chat app-server actions", () => {
it("does not apply or publish app-server metadata when the client changes before refresh completes", async () => {
const stateStore = createChatStateStore(chatStateFixture());
const refreshAppServerMetadata = vi.fn().mockResolvedValue(null);
const controller = createChatServerMetadataActions({
const actions = createChatServerMetadataActions({
stateStore,
vaultPath: "/vault",
currentClient: () => ({}) as AppServerClient,
@ -427,7 +427,7 @@ describe("chat app-server actions", () => {
refreshAppServerMetadata,
});
const refreshing = controller.refreshAppServerMetadata();
const refreshing = actions.refreshAppServerMetadata();
await expect(refreshing).resolves.toBeNull();
expect(stateStore.getState().connection.availableModels).toEqual([]);
@ -441,7 +441,7 @@ describe("chat app-server actions", () => {
availableModels: modelMetadataFromCatalogModels([modelFixture("gpt-cached")]),
serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeError("models", new Error("offline"), 1)),
});
const controller = createChatServerMetadataActions({
const actions = createChatServerMetadataActions({
stateStore,
vaultPath: "/vault",
currentClient: () => ({}) as AppServerClient,
@ -449,7 +449,7 @@ describe("chat app-server actions", () => {
refreshAppServerMetadata: async () => metadata,
});
await controller.refreshAppServerMetadata();
await actions.refreshAppServerMetadata();
expect(stateStore.getState().connection.availableModels.map((model) => model.model)).toEqual(["gpt-cached"]);
expect(stateStore.getState().connection.serverDiagnostics.probes.models.status).toBe("failed");
@ -463,7 +463,7 @@ describe("chat app-server actions", () => {
availableModels: [],
serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeError("models", new Error("offline"), 1)),
});
const controller = createChatServerMetadataActions({
const actions = createChatServerMetadataActions({
stateStore,
vaultPath: "/vault",
currentClient: () => ({}) as AppServerClient,
@ -471,7 +471,7 @@ describe("chat app-server actions", () => {
refreshAppServerMetadata: async () => metadata,
});
await controller.refreshAppServerMetadata();
await actions.refreshAppServerMetadata();
expect(stateStore.getState().connection.availableModels).toEqual([]);
expect(stateStore.getState().connection.serverDiagnostics.probes.models.status).toBe("failed");
@ -485,7 +485,7 @@ describe("chat app-server actions", () => {
const secondClient = {} as unknown as AppServerClient;
let currentClient = firstClient;
const updateAppServerMetadata = vi.fn(() => null);
const controller = createChatServerMetadataActions({
const actions = createChatServerMetadataActions({
stateStore,
vaultPath: "/vault",
currentClient: () => currentClient,
@ -494,7 +494,7 @@ describe("chat app-server actions", () => {
refreshAppServerMetadata: async () => null,
});
const refreshing = controller.applyAppServerResourceEvent({ type: "skills-changed", forceReload: true });
const refreshing = actions.applyAppServerResourceEvent({ type: "skills-changed", forceReload: true });
currentClient = secondClient;
skillRefresh.resolve({ data: [{ skills: [skillFixture("stale-skill")] }] });
@ -512,7 +512,7 @@ describe("chat app-server actions", () => {
const stateStore = createChatStateStore(state);
const listSkills = vi.fn().mockRejectedValue(new Error("offline"));
const client = requestClient({ "skills/list": listSkills });
const controller = createChatServerMetadataActions({
const actions = createChatServerMetadataActions({
stateStore,
vaultPath: "/vault",
currentClient: () => client,
@ -520,7 +520,7 @@ describe("chat app-server actions", () => {
refreshAppServerMetadata: async () => null,
});
await controller.applyAppServerResourceEvent({ type: "skills-changed", forceReload: true });
await actions.applyAppServerResourceEvent({ type: "skills-changed", forceReload: true });
expect(listSkills).toHaveBeenCalledWith({ cwds: ["/vault"], forceReload: true });
expect(stateStore.getState().connection.availableSkills).toEqual(previousSkills);
@ -535,7 +535,7 @@ describe("chat app-server actions", () => {
const client = requestClient({
"account/rateLimits/read": vi.fn().mockResolvedValue({ rateLimits: rateLimit, rateLimitsByLimitId: null }),
});
const controller = createChatServerMetadataActions({
const actions = createChatServerMetadataActions({
stateStore,
vaultPath: "/vault",
currentClient: () => client,
@ -543,7 +543,7 @@ describe("chat app-server actions", () => {
refreshAppServerMetadata: async () => null,
});
await controller.applyAppServerResourceEvent({ type: "rate-limits-updated", preserveExistingOnFailure: true });
await actions.applyAppServerResourceEvent({ type: "rate-limits-updated", preserveExistingOnFailure: true });
expect(stateStore.getState().connection.rateLimit).toMatchObject({ primary: { usedPercent: 64 } });
expect(cachedMetadata.current?.rateLimit).toStrictEqual(rateLimit);
@ -560,7 +560,7 @@ describe("chat app-server actions", () => {
const client = requestClient({
"account/rateLimits/read": vi.fn().mockRejectedValue(new Error("offline")),
});
const controller = createChatServerMetadataActions({
const actions = createChatServerMetadataActions({
stateStore,
vaultPath: "/vault",
currentClient: () => client,
@ -568,7 +568,7 @@ describe("chat app-server actions", () => {
refreshAppServerMetadata: async () => null,
});
await controller.applyAppServerResourceEvent({ type: "rate-limits-updated", preserveExistingOnFailure: true });
await actions.applyAppServerResourceEvent({ type: "rate-limits-updated", preserveExistingOnFailure: true });
expect(stateStore.getState().connection.rateLimit).toBe(previousRateLimit);
expect(stateStore.getState().connection.serverDiagnostics.probes.rateLimits).toMatchObject({ status: "failed" });
@ -583,7 +583,7 @@ describe("chat app-server actions", () => {
const secondClient = {} as unknown as AppServerClient;
let currentClient = firstClient;
const updateAppServerMetadata = vi.fn(() => null);
const controller = createChatServerMetadataActions({
const actions = createChatServerMetadataActions({
stateStore,
vaultPath: "/vault",
currentClient: () => currentClient,
@ -592,7 +592,7 @@ describe("chat app-server actions", () => {
refreshAppServerMetadata: async () => null,
});
const refreshing = controller.applyAppServerResourceEvent({ type: "rate-limits-updated", preserveExistingOnFailure: true });
const refreshing = actions.applyAppServerResourceEvent({ type: "rate-limits-updated", preserveExistingOnFailure: true });
currentClient = secondClient;
rateLimitRefresh.resolve({
rateLimits: rateLimitFixture({ primary: { usedPercent: 88, windowDurationMins: 300, resetsAt: null } }),

View file

@ -2,17 +2,17 @@ import { describe, expect, it, vi } from "vitest";
import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config";
import {
type ChatConnectionActionsHost,
type ChatConnectionAdapter,
type ChatConnectionControllerHost,
type ChatConnectionDiagnosticsActions,
type ChatConnectionMetadataActions,
createChatConnectionController,
} from "../../../../../src/features/chat/application/connection/connection-controller";
createChatConnectionActions,
} from "../../../../../src/features/chat/application/connection/connection-actions";
import { ConnectionWorkTracker } from "../../../../../src/features/chat/application/connection/connection-work";
import { createChatState } from "../../../../../src/features/chat/application/state/root-reducer";
import { createChatStateStore } from "../../../../../src/features/chat/application/state/store";
function createController({ connected = false } = {}) {
function createActionsHarness({ connected = false } = {}) {
const stateStore = createChatStateStore(createChatState());
let isConnected = connected;
const connect = vi.fn().mockImplementation(async () => {
@ -31,7 +31,7 @@ function createController({ connected = false } = {}) {
const diagnostics = {
refreshServerDiagnostics,
} satisfies ChatConnectionDiagnosticsActions;
const host: ChatConnectionControllerHost = {
const host: ChatConnectionActionsHost = {
stateStore,
connection,
connectionWork: new ConnectionWorkTracker(),
@ -53,7 +53,7 @@ function createController({ connected = false } = {}) {
};
return {
connect,
controller: createChatConnectionController(host),
actions: createChatConnectionActions(host),
host,
refreshAppServerMetadata,
refreshServerDiagnostics,
@ -61,11 +61,11 @@ function createController({ connected = false } = {}) {
};
}
describe("ChatConnectionController", () => {
describe("ChatConnectionActions", () => {
it("connects once and publishes startup metadata", async () => {
const { connect, controller, host, refreshAppServerMetadata, stateStore } = createController();
const { connect, actions, host, refreshAppServerMetadata, stateStore } = createActionsHarness();
await controller.ensureConnected();
await actions.ensureConnected();
expect(connect).toHaveBeenCalledOnce();
expect(stateStore.getState().connection.initializeResponse).toEqual({
@ -81,9 +81,9 @@ describe("ChatConnectionController", () => {
});
it("refreshes metadata before server diagnostics", async () => {
const { controller, host, refreshAppServerMetadata, refreshServerDiagnostics } = createController({ connected: true });
const { actions, host, refreshAppServerMetadata, refreshServerDiagnostics } = createActionsHarness({ connected: true });
await controller.refreshDiagnostics();
await actions.refreshDiagnostics();
expect(host.clearDeferredDiagnostics).toHaveBeenCalledTimes(2);
expect(refreshAppServerMetadata).toHaveBeenCalledOnce();
@ -91,28 +91,28 @@ describe("ChatConnectionController", () => {
});
it("refreshes active threads without refreshing metadata", async () => {
const { controller, host, refreshAppServerMetadata } = createController({ connected: true });
const { actions, host, refreshAppServerMetadata } = createActionsHarness({ connected: true });
await controller.refreshActiveThreads();
await actions.refreshActiveThreads();
expect(host.refreshSharedThreads).toHaveBeenCalledOnce();
expect(refreshAppServerMetadata).not.toHaveBeenCalled();
});
it("ignores stale shared query failures while refreshing active threads", async () => {
const { controller, host } = createController({ connected: true });
const { actions, host } = createActionsHarness({ connected: true });
const error = new Error("stale");
vi.mocked(host.refreshSharedThreads).mockRejectedValueOnce(error);
host.isStaleSharedQueryError = vi.fn((candidate) => candidate === error);
await controller.refreshActiveThreads();
await actions.refreshActiveThreads();
expect(host.isStaleSharedQueryError).toHaveBeenCalledWith(error);
expect(host.addSystemMessage).not.toHaveBeenCalled();
});
it("clears disconnected connection state on server exit while keeping last startup metadata", () => {
const { controller, host, stateStore } = createController({ connected: true });
const { actions, host, stateStore } = createActionsHarness({ connected: true });
const initializeResponse = { codexHome: "/codex", platformFamily: "unix", platformOs: "macos", userAgent: "test" } as const;
const runtimeConfig = { ...emptyRuntimeConfigSnapshot(), model: "gpt-5.1" };
stateStore.dispatch({ type: "connection/initialized", initializeResponse });
@ -128,7 +128,7 @@ describe("ChatConnectionController", () => {
runtimeConfig,
});
controller.handleExit();
actions.handleExit();
expect(host.invalidateThreadWork).toHaveBeenCalledOnce();
expect(host.setStatus).toHaveBeenCalledWith("Codex app-server stopped.", {
@ -152,11 +152,11 @@ describe("ChatConnectionController", () => {
});
it("explains missing configured command failures", async () => {
const { controller, connect, host } = createController();
const { actions, connect, host } = createActionsHarness();
const error = Object.assign(new Error("spawn codex ENOENT"), { code: "ENOENT", syscall: "spawn" });
connect.mockRejectedValueOnce(error);
await controller.ensureConnected();
await actions.ensureConnected();
expect(host.setStatus).toHaveBeenCalledWith("Connection failed.", {
kind: "failed",
@ -170,12 +170,12 @@ describe("ChatConnectionController", () => {
});
it("ignores stale connection failures during startup", async () => {
const { controller, connect, host } = createController();
const { actions, connect, host } = createActionsHarness();
const error = new Error("stale connection");
connect.mockRejectedValueOnce(error);
host.isStaleConnectionError = vi.fn((candidate) => candidate === error);
await controller.ensureConnected();
await actions.ensureConnected();
expect(host.isStaleConnectionError).toHaveBeenCalledWith(error);
expect(host.setStatus).toHaveBeenCalledWith("Starting Codex app-server...", { kind: "connecting" });

View file

@ -48,7 +48,7 @@ function resumeThread(stateStore: ChatStateStore, items: readonly MessageStreamI
stateStore.dispatch({ type: "runtime/requested-collaboration-mode-set", collaborationMode: "plan" });
}
function createController() {
function createPlanImplementationHost() {
const stateStore = createChatStateStore(createChatState());
const ensureConnected = vi.fn().mockResolvedValue(true);
const sendTurnText = vi.fn().mockResolvedValue(undefined);
@ -94,7 +94,7 @@ describe("implementPlan", () => {
});
it("switches out of plan mode and submits the implementation prompt", async () => {
const { host, ensureConnected, requestDefaultCollaborationModeForNextTurn, sendTurnText, stateStore } = createController();
const { host, ensureConnected, requestDefaultCollaborationModeForNextTurn, sendTurnText, stateStore } = createPlanImplementationHost();
const plan = planItem("plan");
resumeThread(stateStore, [plan]);
stateStore.dispatch({ type: "ui/panel-set", panel: "status-panel" });
@ -109,7 +109,7 @@ describe("implementPlan", () => {
});
it("ignores stale plan items", async () => {
const { host, ensureConnected, sendTurnText, stateStore } = createController();
const { host, ensureConnected, sendTurnText, stateStore } = createPlanImplementationHost();
const first = planItem("first");
const latest = planItem("latest");
resumeThread(stateStore, [first, latest]);

View file

@ -20,7 +20,7 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture();
const messages: string[] = [];
const controller = createChatRuntimeSettingsActions({
const actions = createChatRuntimeSettingsActions({
stateStore: store,
runtimeTransport: transport,
runtimeSnapshotForState: runtimeSnapshotFixture,
@ -28,7 +28,7 @@ describe("createChatRuntimeSettingsActions", () => {
addSystemMessage: (text) => messages.push(text),
});
await expect(controller.requestModel("gpt-5.5")).resolves.toBe(true);
await expect(actions.requestModel("gpt-5.5")).resolves.toBe(true);
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { model: "gpt-5.5" });
expect(store.getState().runtime.pending.model).toEqual({ kind: "unchanged" });
@ -53,9 +53,9 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture();
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await expect(controller.applyPendingThreadSettings()).resolves.toBe(true);
await expect(actions.applyPendingThreadSettings()).resolves.toBe(true);
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { permissions: ":workspace" });
expect(store.getState().runtime.pending.permissionProfile).toEqual({ kind: "unchanged" });
@ -70,14 +70,14 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(chatStateFixture());
const transport = settingsTransportFixture();
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await expect(controller.requestModel("gpt-5.5")).resolves.toBe(true);
await expect(controller.requestReasoningEffort("high")).resolves.toBe(true);
await controller.enableFastMode();
await controller.enableAutoReview();
await expect(controller.setCollaborationMode("plan")).resolves.toBe(true);
controller.requestDefaultCollaborationModeForNextTurn();
await expect(actions.requestModel("gpt-5.5")).resolves.toBe(true);
await expect(actions.requestReasoningEffort("high")).resolves.toBe(true);
await actions.enableFastMode();
await actions.enableAutoReview();
await expect(actions.setCollaborationMode("plan")).resolves.toBe(true);
actions.requestDefaultCollaborationModeForNextTurn();
expect(transport.updateThreadSettings).not.toHaveBeenCalled();
expect(store.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
@ -99,9 +99,9 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture();
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await controller.toggleFastMode();
await actions.toggleFastMode();
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { serviceTier: "fast" });
expect(store.getState().runtime.pending.fastMode).toEqual({ kind: "unchanged" });
@ -116,11 +116,11 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture();
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await controller.enableFastMode();
await actions.enableFastMode();
store.dispatch({ type: "active-thread/settings-applied", ...threadSettings("fast") });
await controller.disableFastMode();
await actions.disableFastMode();
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(1, "thread", { serviceTier: "fast" });
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(2, "thread", { serviceTier: null });
@ -135,15 +135,15 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture();
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await controller.disableFastMode();
await actions.disableFastMode();
expect(transport.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: null });
expect(store.getState().runtime.active.serviceTier).toBeNull();
expect(store.getState().runtime.active.serviceTierKnown).toBe(true);
await controller.toggleFastMode();
await actions.toggleFastMode();
expect(transport.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: "fast" });
expect(messages).toEqual(["Fast mode off for subsequent turns.", "Fast mode on for subsequent turns."]);
@ -159,15 +159,15 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture();
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await controller.toggleFastMode();
await actions.toggleFastMode();
expect(transport.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: "priority" });
expect(store.getState().runtime.active.serviceTier).toBe("priority");
store.dispatch({ type: "active-thread/settings-applied", ...threadSettings("priority") });
await controller.toggleFastMode();
await actions.toggleFastMode();
expect(transport.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: null });
expect(store.getState().runtime.active.serviceTier).toBeNull();
@ -180,9 +180,9 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture();
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await expect(controller.setCollaborationMode("plan")).resolves.toBe(true);
await expect(actions.setCollaborationMode("plan")).resolves.toBe(true);
expect(transport.updateThreadSettings).not.toHaveBeenCalled();
expect(store.getState().runtime.pending.collaborationMode).toEqual(setCollaborationModeIntent("plan"));
@ -198,9 +198,9 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture();
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
controller.requestDefaultCollaborationModeForNextTurn();
actions.requestDefaultCollaborationModeForNextTurn();
expect(transport.updateThreadSettings).not.toHaveBeenCalled();
expect(store.getState().runtime.pending.collaborationMode).toEqual(setCollaborationModeIntent("default"));
@ -223,7 +223,7 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture();
const messages: string[] = [];
const controller = createChatRuntimeSettingsActions({
const actions = createChatRuntimeSettingsActions({
stateStore: store,
runtimeTransport: transport,
runtimeSnapshotForState: (state) => ({ ...runtimeSnapshotFixture(state), runtimeConfig: null }),
@ -231,7 +231,7 @@ describe("createChatRuntimeSettingsActions", () => {
addSystemMessage: (text) => messages.push(text),
});
await expect(controller.setCollaborationMode("plan")).resolves.toBe(true);
await expect(actions.setCollaborationMode("plan")).resolves.toBe(true);
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", {
collaborationMode: {
@ -252,9 +252,9 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture({ updateThreadSettings: vi.fn().mockRejectedValue(new Error("nope")) });
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await expect(controller.requestModel("gpt-5.5")).resolves.toBe(false);
await expect(actions.requestModel("gpt-5.5")).resolves.toBe(false);
expect(store.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
expect(store.getState().runtime.active.model).toBeNull();
@ -267,9 +267,9 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture({ updateThreadSettings: vi.fn().mockResolvedValue(false) });
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await expect(controller.requestModel("gpt-5.5")).resolves.toBe(false);
await expect(actions.requestModel("gpt-5.5")).resolves.toBe(false);
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { model: "gpt-5.5" });
expect(store.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
@ -284,9 +284,9 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture({ updateThreadSettings: vi.fn().mockRejectedValue(new Error("nope")) });
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await controller.enableFastMode();
await actions.enableFastMode();
expect(store.getState().runtime.pending.fastMode).toEqual({ kind: "set", value: "enabled" });
expect(store.getState().ui.toolbarPanel).toBe("status-panel");
@ -304,9 +304,9 @@ describe("createChatRuntimeSettingsActions", () => {
}),
});
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await expect(controller.requestModel("gpt-5.5")).resolves.toBe(false);
await expect(actions.requestModel("gpt-5.5")).resolves.toBe(false);
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { model: "gpt-5.5" });
expect(store.getState().activeThread.id).toBeNull();
@ -326,9 +326,9 @@ describe("createChatRuntimeSettingsActions", () => {
}),
});
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await expect(controller.requestModel("gpt-5.5")).resolves.toBe(false);
await expect(actions.requestModel("gpt-5.5")).resolves.toBe(false);
expect(store.getState().activeThread.id).toBeNull();
expect(store.getState().runtime.active.model).toBeNull();
@ -349,12 +349,12 @@ describe("createChatRuntimeSettingsActions", () => {
.mockImplementationOnce(() => secondUpdate.promise),
});
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
const firstRequest = controller.requestModel("gpt-old");
const firstRequest = actions.requestModel("gpt-old");
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(1, "thread", { model: "gpt-old" });
const secondRequest = controller.requestModel("gpt-new");
const secondRequest = actions.requestModel("gpt-new");
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(2, "thread", { model: "gpt-new" });
secondUpdate.resolve();
@ -376,9 +376,9 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture();
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await expect(controller.resetModelToConfig()).resolves.toBe(true);
await expect(actions.resetModelToConfig()).resolves.toBe(true);
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { model: null });
expect(store.getState().runtime.pending.model).toEqual({ kind: "unchanged" });
@ -391,11 +391,11 @@ describe("createChatRuntimeSettingsActions", () => {
const store = createChatStateStore(state);
const transport = settingsTransportFixture();
const messages: string[] = [];
const controller = runtimeControllerFixture(store, transport, messages);
const actions = runtimeActionsFixture(store, transport, messages);
await controller.enableAutoReview();
await actions.enableAutoReview();
store.dispatch({ type: "active-thread/settings-applied", ...threadSettings(null, "auto_review") });
await controller.disableAutoReview();
await actions.disableAutoReview();
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(1, "thread", { approvalsReviewer: "auto_review" });
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(2, "thread", { approvalsReviewer: "user" });
@ -403,7 +403,7 @@ describe("createChatRuntimeSettingsActions", () => {
});
});
function runtimeControllerFixture(
function runtimeActionsFixture(
store: ReturnType<typeof createChatStateStore>,
transport: RuntimeSettingsTransport,
messages: string[],

View file

@ -17,7 +17,7 @@ function thread(id: string, name: string | null = null): Thread {
};
}
function createController(options: { restoredThreadPending?: boolean } = {}) {
function createIdentitySyncHarness(options: { restoredThreadPending?: boolean } = {}) {
const stateStore = createChatStateStore(createChatState());
const restoredPlaceholder = vi.fn<() => RestoredThreadPlaceholderState | null>(() => null);
const restoredClear = vi.fn();
@ -36,12 +36,12 @@ function createController(options: { restoredThreadPending?: boolean } = {}) {
notifyActiveThreadIdentityChanged: vi.fn(),
refreshTabHeader: vi.fn(),
};
return { controller: createActiveThreadIdentitySync(host), host, restoredClear, restoredPlaceholder, restoredRename, stateStore };
return { sync: createActiveThreadIdentitySync(host), host, restoredClear, restoredPlaceholder, restoredRename, stateStore };
}
describe("createActiveThreadIdentitySync", () => {
it("clears active thread identity as a complete archive transaction", () => {
const { controller, host, restoredClear, stateStore } = createController();
const { sync, host, restoredClear, stateStore } = createIdentitySyncHarness();
stateStore.dispatch({
type: "active-thread/resumed",
approvalPolicyKnown: true,
@ -58,7 +58,7 @@ describe("createActiveThreadIdentitySync", () => {
approvalsReviewer: null,
});
controller.applyThreadArchiveToActiveIdentity("thread");
sync.applyThreadArchiveToActiveIdentity("thread");
expect(stateStore.getState().activeThread.id).toBeNull();
expect(host.invalidateThreadWork).toHaveBeenCalledOnce();
@ -69,7 +69,7 @@ describe("createActiveThreadIdentitySync", () => {
});
it("ignores archive notifications for non-active threads without identity side effects", () => {
const { controller, host, restoredClear, stateStore } = createController();
const { sync, host, restoredClear, stateStore } = createIdentitySyncHarness();
stateStore.dispatch({
type: "active-thread/resumed",
approvalPolicyKnown: true,
@ -86,7 +86,7 @@ describe("createActiveThreadIdentitySync", () => {
approvalsReviewer: null,
});
controller.applyThreadArchiveToActiveIdentity("other");
sync.applyThreadArchiveToActiveIdentity("other");
expect(stateStore.getState().activeThread.id).toBe("active");
expect(host.invalidateThreadWork).not.toHaveBeenCalled();
@ -97,9 +97,9 @@ describe("createActiveThreadIdentitySync", () => {
});
it("clears pending restored thread identity when that thread is archived", () => {
const { controller, host, restoredClear, stateStore } = createController({ restoredThreadPending: true });
const { sync, host, restoredClear, stateStore } = createIdentitySyncHarness({ restoredThreadPending: true });
controller.applyThreadArchiveToActiveIdentity("thread");
sync.applyThreadArchiveToActiveIdentity("thread");
expect(stateStore.getState().activeThread.id).toBeNull();
expect(host.invalidateThreadWork).toHaveBeenCalledOnce();
@ -110,7 +110,7 @@ describe("createActiveThreadIdentitySync", () => {
});
it("routes active thread rename notifications through active identity refresh", () => {
const { controller, host, restoredRename, stateStore } = createController();
const { sync, host, restoredRename, stateStore } = createIdentitySyncHarness();
stateStore.dispatch({
type: "active-thread/resumed",
approvalPolicyKnown: true,
@ -127,7 +127,7 @@ describe("createActiveThreadIdentitySync", () => {
approvalsReviewer: null,
});
controller.applyThreadRenameToActiveIdentity("thread", "New");
sync.applyThreadRenameToActiveIdentity("thread", "New");
expect(host.notifyActiveThreadIdentityChanged).toHaveBeenCalledOnce();
expect(host.refreshTabHeader).not.toHaveBeenCalled();
@ -135,7 +135,7 @@ describe("createActiveThreadIdentitySync", () => {
});
it("routes pending restored thread rename notifications through active identity refresh", () => {
const { controller, host, restoredPlaceholder, restoredRename } = createController({ restoredThreadPending: true });
const { sync, host, restoredPlaceholder, restoredRename } = createIdentitySyncHarness({ restoredThreadPending: true });
restoredPlaceholder.mockReturnValue({
kind: "placeholder",
threadId: "thread",
@ -144,7 +144,7 @@ describe("createActiveThreadIdentitySync", () => {
loading: null,
});
controller.applyThreadRenameToActiveIdentity("thread", "New");
sync.applyThreadRenameToActiveIdentity("thread", "New");
expect(restoredRename).toHaveBeenCalledWith("thread", "New");
expect(host.notifyActiveThreadIdentityChanged).toHaveBeenCalledOnce();
@ -152,10 +152,10 @@ describe("createActiveThreadIdentitySync", () => {
});
it("ignores rename notifications for inactive and unrestored threads without identity side effects", () => {
const { controller, host, restoredPlaceholder, restoredRename, stateStore } = createController();
const { sync, host, restoredPlaceholder, restoredRename, stateStore } = createIdentitySyncHarness();
stateStore.dispatch({ type: "thread-list/applied", threads: [thread("thread", "Old")] });
controller.applyThreadRenameToActiveIdentity("other", "New");
sync.applyThreadRenameToActiveIdentity("other", "New");
expect(stateStore.getState().threadList.listedThreads[0]?.name).toBe("Old");
expect(restoredPlaceholder).toHaveBeenCalledOnce();

View file

@ -16,7 +16,7 @@ describe("createGoalActions", () => {
const currentGoal = goal();
const goalTransport = goalTransportFixture({ readThreadGoal: vi.fn().mockResolvedValue(currentGoal) });
const refreshLiveState = vi.fn();
const controller = createGoalActions({
const actions = createGoalActions({
stateStore,
goalTransport,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
@ -26,7 +26,7 @@ describe("createGoalActions", () => {
refreshLiveState,
});
await controller.syncThreadGoal("thread");
await actions.syncThreadGoal("thread");
expect(stateStore.getState().activeThread.goal).toEqual(currentGoal);
expect(refreshLiveState).toHaveBeenCalledOnce();
@ -38,7 +38,7 @@ describe("createGoalActions", () => {
const stateStore = createChatStateStore(state);
const addSystemMessage = vi.fn();
const goalTransport = goalTransportFixture({ readThreadGoal: vi.fn().mockRejectedValue(new Error("offline")) });
const controller = createGoalActions({
const actions = createGoalActions({
stateStore,
goalTransport,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
@ -48,7 +48,7 @@ describe("createGoalActions", () => {
refreshLiveState: vi.fn(),
});
await controller.syncThreadGoal("thread");
await actions.syncThreadGoal("thread");
expect(stateStore.getState().activeThread.id).toBe("thread");
expect(stateStore.getState().activeThread.goal).toBeNull();
@ -69,7 +69,7 @@ describe("createGoalActions", () => {
const { setThreadGoal, clearThreadGoal } = goalTransport;
const addSystemMessage = vi.fn();
const addGoalEvent = vi.fn();
const controller = createGoalActions({
const actions = createGoalActions({
stateStore,
goalTransport,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
@ -79,9 +79,9 @@ describe("createGoalActions", () => {
refreshLiveState: vi.fn(),
});
await controller.setObjective("thread", " Updated ", 250);
await controller.setStatus("thread", "paused");
await controller.clear("thread");
await actions.setObjective("thread", " Updated ", 250);
await actions.setStatus("thread", "paused");
await actions.clear("thread");
expect(setThreadGoal).toHaveBeenCalledWith("thread", { objective: "Updated", status: "active", tokenBudget: 250 });
expect(setThreadGoal).toHaveBeenCalledWith("thread", { status: "paused" });
@ -101,7 +101,7 @@ describe("createGoalActions", () => {
const update = deferred<never>();
const goalTransport = goalTransportFixture({ setThreadGoal: vi.fn().mockReturnValue(update.promise) });
const addSystemMessage = vi.fn();
const controller = createGoalActions({
const actions = createGoalActions({
stateStore,
goalTransport,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
@ -111,7 +111,7 @@ describe("createGoalActions", () => {
refreshLiveState: vi.fn(),
});
const pending = controller.setStatus("thread", "paused");
const pending = actions.setStatus("thread", "paused");
await Promise.resolve();
stateStore.dispatch({ type: "active-thread/cleared" });
update.reject(new Error("offline"));
@ -128,7 +128,7 @@ describe("createGoalActions", () => {
const clear = deferred<never>();
const goalTransport = goalTransportFixture({ clearThreadGoal: vi.fn().mockReturnValue(clear.promise) });
const addSystemMessage = vi.fn();
const controller = createGoalActions({
const actions = createGoalActions({
stateStore,
goalTransport,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
@ -138,7 +138,7 @@ describe("createGoalActions", () => {
refreshLiveState: vi.fn(),
});
const pending = controller.clear("thread");
const pending = actions.clear("thread");
await Promise.resolve();
stateStore.dispatch({ type: "active-thread/cleared" });
clear.reject(new Error("offline"));
@ -154,7 +154,7 @@ describe("createGoalActions", () => {
const goalTransport = goalTransportFixture({ setThreadGoal: vi.fn().mockResolvedValueOnce(goal()) });
const addSystemMessage = vi.fn();
const addGoalEvent = vi.fn();
const controller = createGoalActions({
const actions = createGoalActions({
stateStore,
goalTransport,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
@ -164,7 +164,7 @@ describe("createGoalActions", () => {
refreshLiveState: vi.fn(),
});
await controller.setObjective("thread", "Finish", null);
await actions.setObjective("thread", "Finish", null);
expect(addSystemMessage).not.toHaveBeenCalledWith("Goal set.");
expect(addGoalEvent).toHaveBeenCalledWith(
@ -202,7 +202,7 @@ describe("createGoalActions", () => {
});
return { threadId: "thread-new" };
});
const controller = createGoalActions({
const actions = createGoalActions({
stateStore,
goalTransport,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
@ -212,7 +212,7 @@ describe("createGoalActions", () => {
refreshLiveState: vi.fn(),
});
await expect(controller.saveObjective(" Plan release ", null)).resolves.toBe(true);
await expect(actions.saveObjective(" Plan release ", null)).resolves.toBe(true);
expect(startThread).toHaveBeenCalledWith("Plan release", { syncGoal: false });
expect(setThreadGoal).toHaveBeenCalledWith("thread-new", { objective: "Plan release", status: "active", tokenBudget: null });
@ -224,7 +224,7 @@ describe("createGoalActions", () => {
const goalTransport = goalTransportFixture();
const startThread = vi.fn().mockResolvedValue({ threadId: "thread" });
const addSystemMessage = vi.fn();
const controller = createGoalActions({
const actions = createGoalActions({
stateStore,
goalTransport,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
@ -234,7 +234,7 @@ describe("createGoalActions", () => {
refreshLiveState: vi.fn(),
});
await expect(controller.saveObjective(" ", null)).resolves.toBe(false);
await expect(actions.saveObjective(" ", null)).resolves.toBe(false);
expect(addSystemMessage).toHaveBeenCalledWith("Goal objective cannot be empty.");
expect(goalTransport.ensureConnected).not.toHaveBeenCalled();
@ -250,7 +250,7 @@ describe("createGoalActions", () => {
recordThreadGoalUserMessage: vi.fn().mockRejectedValue(new Error("offline")),
});
const addSystemMessage = vi.fn();
const controller = createGoalActions({
const actions = createGoalActions({
stateStore,
goalTransport,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
@ -260,7 +260,7 @@ describe("createGoalActions", () => {
refreshLiveState: vi.fn(),
});
await controller.setObjective("thread", "Finish", null);
await actions.setObjective("thread", "Finish", null);
expect(addSystemMessage).toHaveBeenCalledWith("Could not record goal message: offline");
});
@ -277,7 +277,7 @@ describe("createGoalActions", () => {
}),
});
const addSystemMessage = vi.fn();
const controller = createGoalActions({
const actions = createGoalActions({
stateStore,
goalTransport,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
@ -287,7 +287,7 @@ describe("createGoalActions", () => {
refreshLiveState: vi.fn(),
});
await controller.setObjective("thread", "Finish", null);
await actions.setObjective("thread", "Finish", null);
expect(addSystemMessage).not.toHaveBeenCalled();
});
@ -299,7 +299,7 @@ describe("createGoalActions", () => {
const stateStore = createChatStateStore(state);
const goalTransport = goalTransportFixture({ setThreadGoal: vi.fn().mockResolvedValueOnce(goal({ objective: "Updated" })) });
const addGoalEvent = vi.fn();
const controller = createGoalActions({
const actions = createGoalActions({
stateStore,
goalTransport,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
@ -309,7 +309,7 @@ describe("createGoalActions", () => {
refreshLiveState: vi.fn(),
});
await controller.setObjective("thread", "Updated", null);
await actions.setObjective("thread", "Updated", null);
expect(addGoalEvent).toHaveBeenCalledWith(expect.objectContaining({ kind: "goal", text: "updated: Updated", objective: "Updated" }));
expect(goalTransport.recordThreadGoalUserMessage).not.toHaveBeenCalled();
@ -323,7 +323,7 @@ describe("createGoalActions", () => {
const goalTransport = goalTransportFixture({ setThreadGoal: vi.fn().mockResolvedValueOnce(goal()) });
const addSystemMessage = vi.fn();
const addGoalEvent = vi.fn();
const controller = createGoalActions({
const actions = createGoalActions({
stateStore,
goalTransport,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
@ -333,7 +333,7 @@ describe("createGoalActions", () => {
refreshLiveState: vi.fn(),
});
await controller.setStatus("thread", "active");
await actions.setStatus("thread", "active");
expect(addSystemMessage).not.toHaveBeenCalledWith("Goal resumed.");
expect(addGoalEvent).toHaveBeenCalledWith(expect.objectContaining({ kind: "goal", text: "resumed: Finish", objective: "Finish" }));
@ -346,7 +346,7 @@ describe("createGoalActions", () => {
const currentGoal = goal();
const goalTransport = goalTransportFixture({ readThreadGoal: vi.fn().mockResolvedValue(currentGoal) });
const addSystemMessage = vi.fn();
const controller = createGoalActions({
const actions = createGoalActions({
stateStore,
goalTransport,
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
@ -356,7 +356,7 @@ describe("createGoalActions", () => {
refreshLiveState: vi.fn(),
});
await controller.syncThreadGoal("thread");
await actions.syncThreadGoal("thread");
expect(stateStore.getState().activeThread.goal).toEqual(currentGoal);
expect(addSystemMessage).not.toHaveBeenCalled();

View file

@ -26,7 +26,7 @@ function resumeThreadState(stateStore: ChatStateStore, threadId: string): void {
});
}
function createController(overrides: Partial<ThreadNavigationActionsHost> = {}) {
function createActionsHarness(overrides: Partial<ThreadNavigationActionsHost> = {}) {
const stateStore = createChatStateStore(createChatState());
const host: ThreadNavigationActionsHost = {
stateStore,
@ -40,15 +40,15 @@ function createController(overrides: Partial<ThreadNavigationActionsHost> = {})
focusComposer: vi.fn(),
...overrides,
};
return { controller: createThreadNavigationActions(host), host, stateStore };
return { actions: createThreadNavigationActions(host), host, stateStore };
}
describe("ThreadNavigationActions", () => {
it("starts a blank chat by clearing active thread identity", async () => {
const { controller, host, stateStore } = createController();
const { actions, host, stateStore } = createActionsHarness();
stateStore.dispatch({ type: "ui/panel-set", panel: "history" });
await controller.startNewThread();
await actions.startNewThread();
expect(host.identity.clearActiveThreadIdentity).toHaveBeenCalledOnce();
expect(stateStore.getState().ui.toolbarPanel).toBeNull();
@ -57,12 +57,12 @@ describe("ThreadNavigationActions", () => {
});
it("ignores blank chat navigation while a turn is running", async () => {
const { controller, host, stateStore } = createController();
const { actions, host, stateStore } = createActionsHarness();
resumeThreadState(stateStore, "active");
stateStore.dispatch({ type: "ui/panel-set", panel: "history" });
stateStore.dispatch({ type: "turn/started", threadId: "active", turnId: "turn" });
await controller.startNewThread();
await actions.startNewThread();
expect(host.identity.clearActiveThreadIdentity).not.toHaveBeenCalled();
expect(stateStore.getState().ui.toolbarPanel).toBe("history");
@ -70,11 +70,11 @@ describe("ThreadNavigationActions", () => {
});
it("focuses an already open thread without resuming it", async () => {
const { controller, host } = createController({
const { actions, host } = createActionsHarness({
focusThreadInOpenView: vi.fn().mockResolvedValue(true),
});
await controller.selectThread("thread");
await actions.selectThread("thread");
expect(host.closeForThreadSelection).toHaveBeenCalledOnce();
expect(host.focusThreadInOpenView).toHaveBeenCalledWith("thread");
@ -82,20 +82,20 @@ describe("ThreadNavigationActions", () => {
});
it("resumes the thread when it is not already open", async () => {
const { controller, host } = createController();
const { actions, host } = createActionsHarness();
await controller.selectThread("thread");
await actions.selectThread("thread");
expect(host.closeForThreadSelection).toHaveBeenCalledOnce();
expect(host.resumeThread).toHaveBeenCalledWith("thread");
});
it("blocks switching away while a turn is running", async () => {
const { controller, host, stateStore } = createController();
const { actions, host, stateStore } = createActionsHarness();
resumeThreadState(stateStore, "active");
stateStore.dispatch({ type: "turn/started", threadId: "active", turnId: "turn" });
await controller.selectThread("other");
await actions.selectThread("other");
expect(host.addSystemMessage).toHaveBeenCalledWith("Finish or interrupt the current turn before switching threads.");
expect(host.closeForThreadSelection).not.toHaveBeenCalled();
@ -104,10 +104,10 @@ describe("ThreadNavigationActions", () => {
});
it("closes the toolbar panel before selecting from the toolbar", async () => {
const { controller, host, stateStore } = createController();
const { actions, host, stateStore } = createActionsHarness();
stateStore.dispatch({ type: "ui/panel-set", panel: "history" });
await controller.selectThreadFromToolbar("thread");
await actions.selectThreadFromToolbar("thread");
expect(stateStore.getState().ui.toolbarPanel).toBeNull();
expect(host.closeForThreadSelection).toHaveBeenCalledOnce();
@ -115,12 +115,12 @@ describe("ThreadNavigationActions", () => {
});
it("ignores toolbar selection while another thread is busy", async () => {
const { controller, host, stateStore } = createController();
const { actions, host, stateStore } = createActionsHarness();
resumeThreadState(stateStore, "active");
stateStore.dispatch({ type: "ui/panel-set", panel: "history" });
stateStore.dispatch({ type: "turn/started", threadId: "active", turnId: "turn" });
await controller.selectThreadFromToolbar("other");
await actions.selectThreadFromToolbar("other");
expect(stateStore.getState().ui.toolbarPanel).toBe("history");
expect(host.addSystemMessage).not.toHaveBeenCalled();

View file

@ -13,7 +13,7 @@ import type { ChatPanelEnvironment } from "../../../../src/features/chat/host/co
import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/session/deferred-work";
import { createChatPanelSessionGraph } from "../../../../src/features/chat/host/session-graph";
import { ChatComposerController } from "../../../../src/features/chat/panel/composer-controller";
import { createChatMessageScrollController } from "../../../../src/features/chat/panel/message-stream-scroll-controller";
import { createChatMessageStreamScrollBinding } from "../../../../src/features/chat/panel/message-stream-scroll-binding";
import { MessageStreamPresenter } from "../../../../src/features/chat/panel/surface/message-stream-presenter";
import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../../../../src/settings/model";
import { installObsidianDomShims } from "../../../support/dom";
@ -224,7 +224,7 @@ describe("createChatPanelSessionGraph actions", () => {
deferredTasks: createChatViewDeferredTasks(() => window),
resumeWork,
connectionWork: new ConnectionWorkTracker(),
messageScrollController: createChatMessageScrollController(),
messageScrollBinding: createChatMessageStreamScrollBinding(),
getClosing: () => false,
viewWindow: () => window,
});

View file

@ -52,7 +52,7 @@ function turnBundleFixture(options: { stateStore?: ReturnType<typeof createChatS
const bundle = createTurnBundle(
{
stateStore,
messageScrollController: {
messageScrollBinding: {
showLatest: vi.fn(),
},
} as never,

View file

@ -12,7 +12,7 @@ import type { ChatPanelGoalSurface } from "../../../../src/features/chat/panel/s
import type { ChatPanelToolbarSurface } from "../../../../src/features/chat/panel/surface/toolbar-projection";
import { messageStreamViewBlocks } from "../../../../src/features/chat/presentation/message-stream/view-model";
import type { MessageStreamContext } from "../../../../src/features/chat/ui/message-stream/context";
import type { MessageStreamScrollControllerBinding } from "../../../../src/features/chat/ui/message-stream/flow-scroll.measure";
import type { MessageStreamScrollPortBinding } from "../../../../src/features/chat/ui/message-stream/flow-scroll.measure";
import { installObsidianDomShims } from "../../../support/dom";
installObsidianDomShims();
@ -429,7 +429,7 @@ function shellParts(
items: model.items.value,
}),
context: testMessageStreamContext,
scrollController: noOpMessageStreamScrollController,
scrollPortBinding: noOpMessageStreamScrollPortBinding,
};
},
},
@ -588,7 +588,7 @@ function surfaceFixture(options: { toolbarConnected?: () => boolean; goalSendSho
};
}
const noOpMessageStreamScrollController: MessageStreamScrollControllerBinding = {
const noOpMessageStreamScrollPortBinding: MessageStreamScrollPortBinding = {
mountScrollPort: () => () => undefined,
};

View file

@ -7,9 +7,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { type ChatAction, type ChatState, chatReducer } from "../../../../../src/features/chat/application/state/root-reducer";
import { type ChatStateStore, createChatStateStore } from "../../../../../src/features/chat/application/state/store";
import {
type ChatMessageScrollController,
createChatMessageScrollController,
} from "../../../../../src/features/chat/panel/message-stream-scroll-controller";
type ChatMessageStreamScrollBinding,
createChatMessageStreamScrollBinding,
} from "../../../../../src/features/chat/panel/message-stream-scroll-binding";
import { MessageStreamPresenter } from "../../../../../src/features/chat/panel/surface/message-stream-presenter";
import {
type ChatMessageStreamSurfaceContext,
@ -213,7 +213,7 @@ describe("MessageStreamPresenter scroll pinning", () => {
},
]);
const parent = document.createElement("div");
const { presenter, scrollController } = messageStreamPresenter(state);
const { presenter, scrollPortBinding } = messageStreamPresenter(state);
renderMessageStreamPresenter(parent, presenter, state);
const messages = messageViewport(parent);
@ -223,7 +223,7 @@ describe("MessageStreamPresenter scroll pinning", () => {
const scrollIntoView = vi.spyOn(HTMLElement.prototype, "scrollIntoView");
scrollIntoView.mockClear();
scrollController.showLatest();
scrollPortBinding.showLatest();
await settleMessageRender(messages);
expect(messages.scrollTop).toBe(0);
@ -245,7 +245,7 @@ describe("MessageStreamPresenter scroll pinning", () => {
},
]);
const parent = document.createElement("div");
const { presenter, scrollController } = messageStreamPresenter(state);
const { presenter, scrollPortBinding } = messageStreamPresenter(state);
const messages = parent.createDiv({ cls: "codex-panel__messages" });
let scrollTop = 0;
@ -275,7 +275,7 @@ describe("MessageStreamPresenter scroll pinning", () => {
await settleMessageRender(messages);
expect(messages.scrollTop).toBe(0);
scrollController.showLatest();
scrollPortBinding.showLatest();
expect(messages.scrollTop).toBe(0);
layoutSettled = true;
@ -285,15 +285,15 @@ describe("MessageStreamPresenter scroll pinning", () => {
});
it("accepts scroll commands when no message stream viewport is mounted", () => {
const { presenter, scrollController } = messageStreamPresenter();
const { presenter, scrollPortBinding } = messageStreamPresenter();
expect(() => {
scrollController.showLatest();
scrollController.scrollFromComposer({ kind: "scroll-by", direction: 1, amount: "text-lines" });
scrollController.scrollFromComposer({ kind: "scroll-by", direction: -1, amount: "page" });
scrollController.scrollFromComposer({ kind: "scroll-to", edge: "start" });
scrollPortBinding.showLatest();
scrollPortBinding.scrollFromComposer({ kind: "scroll-by", direction: 1, amount: "text-lines" });
scrollPortBinding.scrollFromComposer({ kind: "scroll-by", direction: -1, amount: "page" });
scrollPortBinding.scrollFromComposer({ kind: "scroll-to", edge: "start" });
presenter.dispose();
scrollController.showLatest();
scrollPortBinding.showLatest();
}).not.toThrow();
});
@ -312,7 +312,7 @@ describe("MessageStreamPresenter scroll pinning", () => {
},
]);
const parent = document.createElement("div");
const { presenter, scrollController } = messageStreamPresenter(state);
const { presenter, scrollPortBinding } = messageStreamPresenter(state);
renderMessageStreamPresenter(parent, presenter, state);
const messages = messageViewport(parent);
installMessageViewportMetrics(messages);
@ -321,8 +321,8 @@ describe("MessageStreamPresenter scroll pinning", () => {
unmountUiRoot(parent);
expect(() => {
scrollController.showLatest();
scrollController.scrollFromComposer({ kind: "scroll-by", direction: 1, amount: "page" });
scrollPortBinding.showLatest();
scrollPortBinding.scrollFromComposer({ kind: "scroll-by", direction: 1, amount: "page" });
}).not.toThrow();
});
@ -341,7 +341,7 @@ describe("MessageStreamPresenter scroll pinning", () => {
},
]);
const parent = document.createElement("div");
const { presenter, scrollController } = messageStreamPresenter(state);
const { presenter, scrollPortBinding } = messageStreamPresenter(state);
renderMessageStreamPresenter(parent, presenter, state);
const oldMessages = messageViewport(parent);
installMessageViewportMetrics(oldMessages, { clientHeight: 100, scrollHeight: 1000 });
@ -354,7 +354,7 @@ describe("MessageStreamPresenter scroll pinning", () => {
const newMessages = messageViewport(parent);
installMessageViewportMetrics(newMessages, { clientHeight: 100, scrollHeight: 1000 });
await settleMessageRender(newMessages);
scrollController.showLatest();
scrollPortBinding.showLatest();
expect(newMessages.scrollTop).toBe(900);
expect(oldMessages.scrollTop).toBe(125);
@ -375,12 +375,12 @@ describe("MessageStreamPresenter scroll pinning", () => {
},
]);
const parent = document.createElement("div");
const { presenter, scrollController } = messageStreamPresenter(state, vi.fn(), "/vault");
const { presenter, scrollPortBinding } = messageStreamPresenter(state, vi.fn(), "/vault");
renderMessageStreamPresenter(parent, presenter, state);
const messages = messageViewport(parent);
installMessageViewportMetrics(messages, { clientHeight: 100, scrollHeight: 1000 });
scrollController.showLatest();
scrollPortBinding.showLatest();
await settleMessageRender(messages);
expect(messages.scrollTop).toBe(900);
@ -608,7 +608,7 @@ async function renderedInternalLink(
interface TestMessageStreamPresenter {
presenter: MessageStreamPresenter;
scrollController: ChatMessageScrollController;
scrollPortBinding: ChatMessageStreamScrollBinding;
}
function messageStreamPresenter(
@ -618,7 +618,7 @@ function messageStreamPresenter(
vaultFiles: string[] = [],
): TestMessageStreamPresenter {
const files = new Map(vaultFiles.map((path) => [path, tFile(path)]));
const scrollController = createChatMessageScrollController();
const scrollPortBinding = createChatMessageStreamScrollBinding();
const presenter = new MessageStreamPresenter({
obsidian: {
app: {
@ -640,9 +640,9 @@ function messageStreamPresenter(
vaultPath,
},
scroll: {
controller: scrollController,
portBinding: scrollPortBinding,
dispose: () => {
scrollController.dispose();
scrollPortBinding.dispose();
},
},
history: {
@ -666,7 +666,7 @@ function messageStreamPresenter(
consumePendingAutoFocus: () => false,
},
});
return { presenter, scrollController };
return { presenter, scrollPortBinding };
}
function testStoreForState(state: ChatState): ChatStateStore {

View file

@ -11,7 +11,7 @@ import type { ChatPanelGoalSurface } from "../../../../src/features/chat/panel/s
import type { ChatPanelToolbarSurface } from "../../../../src/features/chat/panel/surface/toolbar-projection";
import { createToolbarPanelActions, type ToolbarPanelActions } from "../../../../src/features/chat/panel/toolbar-actions";
import type { MessageStreamContext } from "../../../../src/features/chat/ui/message-stream/context";
import type { MessageStreamScrollControllerBinding } from "../../../../src/features/chat/ui/message-stream/flow-scroll.measure";
import type { MessageStreamScrollPortBinding } from "../../../../src/features/chat/ui/message-stream/flow-scroll.measure";
import { installObsidianDomShims } from "../../../support/dom";
installObsidianDomShims();
@ -113,7 +113,7 @@ function shellParts(store: ReturnType<typeof createChatStateStore>, toolbarPanel
renderState: () => ({
blocks: [],
context: testMessageStreamContext,
scrollController: noOpMessageStreamScrollController,
scrollPortBinding: noOpMessageStreamScrollPortBinding,
}),
},
composer: {
@ -189,7 +189,7 @@ function surfaceFixture(
};
}
const noOpMessageStreamScrollController: MessageStreamScrollControllerBinding = {
const noOpMessageStreamScrollPortBinding: MessageStreamScrollPortBinding = {
mountScrollPort: () => () => undefined,
};

View file

@ -7,8 +7,8 @@ import { MESSAGE_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat
import {
MessageStreamFlowFrame,
type MessageStreamScrollCommand,
type MessageStreamScrollControllerBinding,
type MessageStreamScrollPort,
type MessageStreamScrollPortBinding,
} from "../../../../../src/features/chat/ui/message-stream/flow-scroll.measure";
import { renderUiRoot } from "../../../../../src/shared/dom/preact-root.dom";
import { installObsidianDomShims } from "../../../../support/dom";
@ -265,11 +265,11 @@ interface CapturedScrollToOptions {
behavior: ScrollBehavior | undefined;
}
interface TestMessageStreamScrollController extends MessageStreamScrollControllerBinding {
interface TestMessageStreamScrollPortBinding extends MessageStreamScrollPortBinding {
dispatch(command: MessageStreamScrollCommand): void;
}
function createTestMessageStreamScrollController(): TestMessageStreamScrollController {
function createTestMessageStreamScrollPortBinding(): TestMessageStreamScrollPortBinding {
let port: MessageStreamScrollPort | null = null;
return {
mountScrollPort(nextPort) {
@ -292,7 +292,7 @@ function renderFlowMessageStream(
viewport?: { width: number; height: number };
} = {},
): {
controller: TestMessageStreamScrollController;
controller: TestMessageStreamScrollPortBinding;
messages: HTMLElement;
render: (nextKeys: readonly string[], nextHeights: Record<string, number>) => void;
setHeights: (nextHeights: Record<string, number>) => void;
@ -301,7 +301,7 @@ function renderFlowMessageStream(
} {
const parent = document.createElement("div");
document.body.append(parent);
const controller = createTestMessageStreamScrollController();
const controller = createTestMessageStreamScrollPortBinding();
let currentHeights = heights;
let currentKeys = keys;
let viewport = options.viewport ?? { width: 240, height: 100 };
@ -314,7 +314,7 @@ function renderFlowMessageStream(
parent,
h(MessageStreamFlowFrame, {
blocks: nextKeys.map((key) => ({ key })),
scrollController: controller,
scrollPortBinding: controller,
renderBlockContent: (block) => options.blockNode?.(block.key) ?? h("div", null, block.key),
}),
);

View file

@ -16,7 +16,7 @@ import type {
PendingRequestBlockActions,
PendingRequestBlockContext,
} from "../../../../../src/features/chat/ui/message-stream/context";
import type { MessageStreamScrollControllerBinding } from "../../../../../src/features/chat/ui/message-stream/flow-scroll.measure";
import type { MessageStreamScrollPortBinding } from "../../../../../src/features/chat/ui/message-stream/flow-scroll.measure";
import { pendingRequestBlockNode } from "../../../../../src/features/chat/ui/message-stream/pending-request-block";
import { MessageStreamViewport } from "../../../../../src/features/chat/ui/message-stream/stream-blocks";
import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/dom/preact-root.dom";
@ -175,14 +175,14 @@ export function renderMessageStreamBlocksInAct(parent: HTMLElement, blocks: Mess
state={{
blocks,
context,
scrollController: noOpMessageStreamScrollController,
scrollPortBinding: noOpMessageStreamScrollPortBinding,
}}
/>,
);
});
}
const noOpMessageStreamScrollController: MessageStreamScrollControllerBinding = {
const noOpMessageStreamScrollPortBinding: MessageStreamScrollPortBinding = {
mountScrollPort: () => () => undefined,
};