diff --git a/src/features/chat/application/connection/connection-controller.ts b/src/features/chat/application/connection/connection-actions.ts similarity index 83% rename from src/features/chat/application/connection/connection-controller.ts rename to src/features/chat/application/connection/connection-actions.ts index dec3ed84..43520d91 100644 --- a/src/features/chat/application/connection/connection-controller.ts +++ b/src/features/chat/application/connection/connection-actions.ts @@ -21,7 +21,7 @@ export interface ChatConnectionDiagnosticsActions { refreshServerDiagnostics: (options?: { appServerMetadataSnapshot?: boolean; forceResourceProbes?: boolean }) => Promise; } -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; invalidate(): void; handleExit(): void; @@ -65,8 +65,8 @@ export interface ChatConnectionController { refreshStatusPanel(): Promise; } -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 { +async function ensureConnected(host: ChatConnectionActionsHost): Promise { const connecting = host.connectionWork.active(); if (connecting?.promise) return connecting.promise; @@ -99,7 +99,7 @@ async function ensureConnected(host: ChatConnectionControllerHost): Promise { +async function refreshActiveThreads(host: ChatConnectionActionsHost): Promise { 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, -): Promise { +async function refreshDiagnostics(host: ChatConnectionActionsHost, actions: Pick): Promise { 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, + host: ChatConnectionActionsHost, + actions: Pick, ): Promise { 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 { +async function initializeConnection(host: ChatConnectionActionsHost, connection: ActiveConnectionWork): Promise { host.setStatus(STATUS_CONNECTION_STARTING, { kind: "connecting" }); try { const initialization = await host.connection.connect(); diff --git a/src/features/chat/host/bundles/composer-bundle.ts b/src/features/chat/host/bundles/composer-bundle.ts index 1ebf2a72..0524883e 100644 --- a/src/features/chat/host/bundles/composer-bundle.ts +++ b/src/features/chat/host/bundles/composer-bundle.ts @@ -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(), diff --git a/src/features/chat/host/bundles/connection-bundle.ts b/src/features/chat/host/bundles/connection-bundle.ts index ca55b16f..c30b9ed4 100644 --- a/src/features/chat/host/bundles/connection-bundle.ts +++ b/src/features/chat/host/bundles/connection-bundle.ts @@ -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: { diff --git a/src/features/chat/host/bundles/shell-bundle.ts b/src/features/chat/host/bundles/shell-bundle.ts index 2f7b1848..37d42950 100644 --- a/src/features/chat/host/bundles/shell-bundle.ts +++ b/src/features/chat/host/bundles/shell-bundle.ts @@ -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: { diff --git a/src/features/chat/host/bundles/thread-bundle.ts b/src/features/chat/host/bundles/thread-bundle.ts index 7af175ae..18d82bf2 100644 --- a/src/features/chat/host/bundles/thread-bundle.ts +++ b/src/features/chat/host/bundles/thread-bundle.ts @@ -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); diff --git a/src/features/chat/host/bundles/turn-bundle.ts b/src/features/chat/host/bundles/turn-bundle.ts index e82d8d90..93cd0b25 100644 --- a/src/features/chat/host/bundles/turn-bundle.ts +++ b/src/features/chat/host/bundles/turn-bundle.ts @@ -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(); }, }, }, diff --git a/src/features/chat/host/session-graph.ts b/src/features/chat/host/session-graph.ts index d3c45e94..df777923 100644 --- a/src/features/chat/host/session-graph.ts +++ b/src/features/chat/host/session-graph.ts @@ -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 = 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, diff --git a/src/features/chat/host/session.ts b/src/features/chat/host/session.ts index 2ac368c6..8be275ca 100644 --- a/src/features/chat/host/session.ts +++ b/src/features/chat/host/session.ts @@ -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 { - await this.graph.connection.controller.ensureConnected(); + await this.graph.connection.actions.ensureConnected(); } async startNewThread(): Promise { @@ -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(), }); diff --git a/src/features/chat/panel/composer-controller.ts b/src/features/chat/panel/composer-controller.ts index 761463dd..fcf62398 100644 --- a/src/features/chat/panel/composer-controller.ts +++ b/src/features/chat/panel/composer-controller.ts @@ -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"; diff --git a/src/features/chat/panel/composer-controller.dom.ts b/src/features/chat/panel/composer-element.dom.ts similarity index 100% rename from src/features/chat/panel/composer-controller.dom.ts rename to src/features/chat/panel/composer-element.dom.ts diff --git a/src/features/chat/panel/message-stream-scroll-controller.ts b/src/features/chat/panel/message-stream-scroll-binding.ts similarity index 80% rename from src/features/chat/panel/message-stream-scroll-controller.ts rename to src/features/chat/panel/message-stream-scroll-binding.ts index 8880f99f..07130c7b 100644 --- a/src/features/chat/panel/message-stream-scroll-controller.ts +++ b/src/features/chat/panel/message-stream-scroll-binding.ts @@ -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 => { diff --git a/src/features/chat/panel/surface/message-stream-presenter.ts b/src/features/chat/panel/surface/message-stream-presenter.ts index cbebc47f..d5a6b171 100644 --- a/src/features/chat/panel/surface/message-stream-presenter.ts +++ b/src/features/chat/panel/surface/message-stream-presenter.ts @@ -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, }; } diff --git a/src/features/chat/panel/toolbar-actions.ts b/src/features/chat/panel/toolbar-actions.ts index 7d0bc3ff..429b8dfe 100644 --- a/src/features/chat/panel/toolbar-actions.ts +++ b/src/features/chat/panel/toolbar-actions.ts @@ -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; 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."); diff --git a/src/features/chat/ui/message-stream/flow-scroll.measure.ts b/src/features/chat/ui/message-stream/flow-scroll.measure.ts index 7c2ad6f3..ea8f8715 100644 --- a/src/features/chat/ui/message-stream/flow-scroll.measure.ts +++ b/src/features/chat/ui/message-stream/flow-scroll.measure.ts @@ -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 { blocks: readonly Block[]; rootAttributes?: Partial>; - scrollController: MessageStreamScrollControllerBinding; + scrollPortBinding: MessageStreamScrollPortBinding; renderBlockContent: (block: Block) => UiNode; } @@ -71,7 +71,7 @@ export class MessageStreamFlowFrame>, 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 } {...(rootAttributes ? { rootAttributes } : {})} /> diff --git a/tests/features/chat/app-server/actions/actions.test.ts b/tests/features/chat/app-server/actions/actions.test.ts index 5f0f047e..358258c7 100644 --- a/tests/features/chat/app-server/actions/actions.test.ts +++ b/tests/features/chat/app-server/actions/actions.test.ts @@ -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 } }), diff --git a/tests/features/chat/application/connection/connection-controller.test.ts b/tests/features/chat/application/connection/connection-actions.test.ts similarity index 84% rename from tests/features/chat/application/connection/connection-controller.test.ts rename to tests/features/chat/application/connection/connection-actions.test.ts index b216711a..0eae3d15 100644 --- a/tests/features/chat/application/connection/connection-controller.test.ts +++ b/tests/features/chat/application/connection/connection-actions.test.ts @@ -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" }); diff --git a/tests/features/chat/application/conversation/plan-implementation.test.ts b/tests/features/chat/application/conversation/plan-implementation.test.ts index 06f4617d..f9d2418a 100644 --- a/tests/features/chat/application/conversation/plan-implementation.test.ts +++ b/tests/features/chat/application/conversation/plan-implementation.test.ts @@ -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]); diff --git a/tests/features/chat/application/runtime/settings-actions.test.ts b/tests/features/chat/application/runtime/settings-actions.test.ts index 379406f5..2464d7f5 100644 --- a/tests/features/chat/application/runtime/settings-actions.test.ts +++ b/tests/features/chat/application/runtime/settings-actions.test.ts @@ -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, transport: RuntimeSettingsTransport, messages: string[], diff --git a/tests/features/chat/application/threads/active-thread-identity-sync.test.ts b/tests/features/chat/application/threads/active-thread-identity-sync.test.ts index ac70ebed..02f46311 100644 --- a/tests/features/chat/application/threads/active-thread-identity-sync.test.ts +++ b/tests/features/chat/application/threads/active-thread-identity-sync.test.ts @@ -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(); diff --git a/tests/features/chat/application/threads/goal-actions.test.ts b/tests/features/chat/application/threads/goal-actions.test.ts index bc0bbaab..5769203f 100644 --- a/tests/features/chat/application/threads/goal-actions.test.ts +++ b/tests/features/chat/application/threads/goal-actions.test.ts @@ -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(); 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(); 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(); diff --git a/tests/features/chat/application/threads/thread-navigation-actions.test.ts b/tests/features/chat/application/threads/thread-navigation-actions.test.ts index 5045552c..533867ed 100644 --- a/tests/features/chat/application/threads/thread-navigation-actions.test.ts +++ b/tests/features/chat/application/threads/thread-navigation-actions.test.ts @@ -26,7 +26,7 @@ function resumeThreadState(stateStore: ChatStateStore, threadId: string): void { }); } -function createController(overrides: Partial = {}) { +function createActionsHarness(overrides: Partial = {}) { const stateStore = createChatStateStore(createChatState()); const host: ThreadNavigationActionsHost = { stateStore, @@ -40,15 +40,15 @@ function createController(overrides: Partial = {}) 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(); diff --git a/tests/features/chat/host/session-graph.test.ts b/tests/features/chat/host/session-graph.test.ts index 037adf84..62c7cd6e 100644 --- a/tests/features/chat/host/session-graph.test.ts +++ b/tests/features/chat/host/session-graph.test.ts @@ -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, }); diff --git a/tests/features/chat/host/turn-bundle.test.ts b/tests/features/chat/host/turn-bundle.test.ts index 6b2aee37..9ee1077a 100644 --- a/tests/features/chat/host/turn-bundle.test.ts +++ b/tests/features/chat/host/turn-bundle.test.ts @@ -52,7 +52,7 @@ function turnBundleFixture(options: { stateStore?: ReturnType boolean; goalSendSho }; } -const noOpMessageStreamScrollController: MessageStreamScrollControllerBinding = { +const noOpMessageStreamScrollPortBinding: MessageStreamScrollPortBinding = { mountScrollPort: () => () => undefined, }; diff --git a/tests/features/chat/panel/surface/message-stream-presenter.test.ts b/tests/features/chat/panel/surface/message-stream-presenter.test.ts index 2e3a7673..a4826d8a 100644 --- a/tests/features/chat/panel/surface/message-stream-presenter.test.ts +++ b/tests/features/chat/panel/surface/message-stream-presenter.test.ts @@ -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 { diff --git a/tests/features/chat/panel/toolbar-archive-state.test.tsx b/tests/features/chat/panel/toolbar-archive-state.test.tsx index b8ff3de8..273a6493 100644 --- a/tests/features/chat/panel/toolbar-archive-state.test.tsx +++ b/tests/features/chat/panel/toolbar-archive-state.test.tsx @@ -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, toolbarPanel renderState: () => ({ blocks: [], context: testMessageStreamContext, - scrollController: noOpMessageStreamScrollController, + scrollPortBinding: noOpMessageStreamScrollPortBinding, }), }, composer: { @@ -189,7 +189,7 @@ function surfaceFixture( }; } -const noOpMessageStreamScrollController: MessageStreamScrollControllerBinding = { +const noOpMessageStreamScrollPortBinding: MessageStreamScrollPortBinding = { mountScrollPort: () => () => undefined, }; diff --git a/tests/features/chat/ui/message-stream/flow-scroll.test.ts b/tests/features/chat/ui/message-stream/flow-scroll.test.ts index 54071ee1..5b7898ff 100644 --- a/tests/features/chat/ui/message-stream/flow-scroll.test.ts +++ b/tests/features/chat/ui/message-stream/flow-scroll.test.ts @@ -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) => void; setHeights: (nextHeights: Record) => 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), }), ); diff --git a/tests/features/chat/ui/message-stream/test-helpers.tsx b/tests/features/chat/ui/message-stream/test-helpers.tsx index c400e088..3e057a44 100644 --- a/tests/features/chat/ui/message-stream/test-helpers.tsx +++ b/tests/features/chat/ui/message-stream/test-helpers.tsx @@ -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, };