mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
Clarify controller-adjacent naming
This commit is contained in:
parent
8568b224dc
commit
d5390d9752
29 changed files with 270 additions and 273 deletions
|
|
@ -21,7 +21,7 @@ export interface ChatConnectionDiagnosticsActions {
|
|||
refreshServerDiagnostics: (options?: { appServerMetadataSnapshot?: boolean; forceResourceProbes?: boolean }) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface ChatConnectionControllerHost {
|
||||
export interface ChatConnectionActionsHost {
|
||||
stateStore: ChatStateStore;
|
||||
connection: ChatConnectionAdapter;
|
||||
connectionWork: ConnectionWorkTracker;
|
||||
|
|
@ -43,7 +43,7 @@ export interface ChatConnectionControllerHost {
|
|||
}
|
||||
|
||||
type ChatConnectionExitHost = Pick<
|
||||
ChatConnectionControllerHost,
|
||||
ChatConnectionActionsHost,
|
||||
"connectionWork" | "invalidateThreadWork" | "setStatus" | "stateStore" | "resetThreadTurnPresence" | "refreshLiveState"
|
||||
>;
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ export function handleChatConnectionExit(host: ChatConnectionExitHost): void {
|
|||
host.refreshLiveState();
|
||||
}
|
||||
|
||||
export interface ChatConnectionController {
|
||||
export interface ChatConnectionActions {
|
||||
ensureConnected(): Promise<void>;
|
||||
invalidate(): void;
|
||||
handleExit(): void;
|
||||
|
|
@ -65,8 +65,8 @@ export interface ChatConnectionController {
|
|||
refreshStatusPanel(): Promise<void>;
|
||||
}
|
||||
|
||||
export function createChatConnectionController(host: ChatConnectionControllerHost): ChatConnectionController {
|
||||
const controller: ChatConnectionController = {
|
||||
export function createChatConnectionActions(host: ChatConnectionActionsHost): ChatConnectionActions {
|
||||
const actions: ChatConnectionActions = {
|
||||
ensureConnected: () => ensureConnected(host),
|
||||
invalidate: () => {
|
||||
host.connectionWork.invalidate();
|
||||
|
|
@ -75,13 +75,13 @@ export function createChatConnectionController(host: ChatConnectionControllerHos
|
|||
handleChatConnectionExit(host);
|
||||
},
|
||||
refreshActiveThreads: () => refreshActiveThreads(host),
|
||||
refreshDiagnostics: () => refreshDiagnostics(host, controller),
|
||||
refreshStatusPanel: () => refreshStatusPanel(host, controller),
|
||||
refreshDiagnostics: () => refreshDiagnostics(host, actions),
|
||||
refreshStatusPanel: () => refreshStatusPanel(host, actions),
|
||||
};
|
||||
return controller;
|
||||
return actions;
|
||||
}
|
||||
|
||||
async function ensureConnected(host: ChatConnectionControllerHost): Promise<void> {
|
||||
async function ensureConnected(host: ChatConnectionActionsHost): Promise<void> {
|
||||
const connecting = host.connectionWork.active();
|
||||
if (connecting?.promise) return connecting.promise;
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ async function ensureConnected(host: ChatConnectionControllerHost): Promise<void
|
|||
}
|
||||
}
|
||||
|
||||
async function refreshActiveThreads(host: ChatConnectionControllerHost): Promise<void> {
|
||||
async function refreshActiveThreads(host: ChatConnectionActionsHost): Promise<void> {
|
||||
if (!host.connection.isConnected()) return;
|
||||
try {
|
||||
await host.refreshSharedThreads();
|
||||
|
|
@ -110,12 +110,9 @@ async function refreshActiveThreads(host: ChatConnectionControllerHost): Promise
|
|||
}
|
||||
}
|
||||
|
||||
async function refreshDiagnostics(
|
||||
host: ChatConnectionControllerHost,
|
||||
controller: Pick<ChatConnectionController, "ensureConnected">,
|
||||
): Promise<void> {
|
||||
async function refreshDiagnostics(host: ChatConnectionActionsHost, actions: Pick<ChatConnectionActions, "ensureConnected">): Promise<void> {
|
||||
host.clearDeferredDiagnostics();
|
||||
await controller.ensureConnected();
|
||||
await actions.ensureConnected();
|
||||
if (!host.connection.isConnected()) return;
|
||||
host.clearDeferredDiagnostics();
|
||||
await host.metadata.refreshAppServerMetadata();
|
||||
|
|
@ -123,18 +120,18 @@ async function refreshDiagnostics(
|
|||
}
|
||||
|
||||
async function refreshStatusPanel(
|
||||
host: ChatConnectionControllerHost,
|
||||
controller: Pick<ChatConnectionController, "refreshActiveThreads" | "refreshDiagnostics">,
|
||||
host: ChatConnectionActionsHost,
|
||||
actions: Pick<ChatConnectionActions, "refreshActiveThreads" | "refreshDiagnostics">,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await controller.refreshDiagnostics();
|
||||
await actions.refreshDiagnostics();
|
||||
} catch (error) {
|
||||
host.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
await controller.refreshActiveThreads();
|
||||
await actions.refreshActiveThreads();
|
||||
}
|
||||
|
||||
async function initializeConnection(host: ChatConnectionControllerHost, connection: ActiveConnectionWork): Promise<void> {
|
||||
async function initializeConnection(host: ChatConnectionActionsHost, connection: ActiveConnectionWork): Promise<void> {
|
||||
host.setStatus(STATUS_CONNECTION_STARTING, { kind: "connecting" });
|
||||
try {
|
||||
const initialization = await host.connection.connect();
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import type { ChatResumeWorkTracker } from "../application/threads/resume-work";
|
|||
import { createStructuredSystemItem, createSystemItem } from "../domain/message-stream/factories/system-items";
|
||||
import type { MessageStreamNoticeSection } from "../domain/message-stream/items";
|
||||
import type { ChatComposerController } from "../panel/composer-controller";
|
||||
import type { ChatMessageScrollController } from "../panel/message-stream-scroll-controller";
|
||||
import type { ChatMessageStreamScrollBinding } from "../panel/message-stream-scroll-binding";
|
||||
import { createChatComposerController } from "./bundles/composer-bundle";
|
||||
import { type ChatPanelConnectionBundle, createConnectionBundle } from "./bundles/connection-bundle";
|
||||
import { createReconnectAction } from "./bundles/reconnect-bundle";
|
||||
|
|
@ -27,7 +27,7 @@ import { type ChatPanelSharedStateBinding, createChatPanelSharedStateBinding } f
|
|||
export interface ChatPanelSessionGraph {
|
||||
connection: {
|
||||
manager: ConnectionManager;
|
||||
controller: ChatPanelConnectionBundle["connection"]["controller"];
|
||||
actions: ChatPanelConnectionBundle["connection"]["actions"];
|
||||
};
|
||||
thread: {
|
||||
resume: ResumeActions;
|
||||
|
|
@ -63,7 +63,7 @@ interface ChatPanelSessionGraphHost {
|
|||
deferredTasks: ChatViewDeferredTasks;
|
||||
resumeWork: ChatResumeWorkTracker;
|
||||
connectionWork: ConnectionWorkTracker;
|
||||
messageScrollController: ChatMessageScrollController;
|
||||
messageScrollBinding: ChatMessageStreamScrollBinding;
|
||||
getClosing: () => boolean;
|
||||
viewWindow: () => Window;
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
|
|||
const connection = createConnectionManager(environment);
|
||||
const currentClient = () => connection.currentClient();
|
||||
let ensureConnected: () => Promise<void> = async () => {
|
||||
throw new Error("Codex app-server connection controller is not initialized.");
|
||||
throw new Error("Codex app-server connection actions are not initialized.");
|
||||
};
|
||||
const connectedClient = async () => {
|
||||
await ensureConnected();
|
||||
|
|
@ -130,12 +130,12 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
|
|||
},
|
||||
);
|
||||
const {
|
||||
connection: { controller: connectionController },
|
||||
connection: { actions: connectionActions },
|
||||
inboundHandler,
|
||||
} = connectionBundle;
|
||||
const { threads: serverThreads } = connectionBundle.serverActions;
|
||||
ensureConnected = () => connectionController.ensureConnected();
|
||||
const refreshActiveThreads = () => connectionController.refreshActiveThreads();
|
||||
ensureConnected = () => connectionActions.ensureConnected();
|
||||
const refreshActiveThreads = () => connectionActions.refreshActiveThreads();
|
||||
const runtime = createRuntimeBundle(host, {
|
||||
connection,
|
||||
appServer,
|
||||
|
|
@ -188,13 +188,13 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
|
|||
autoTitleCoordinator: threadFoundation.autoTitleCoordinator,
|
||||
reconnect,
|
||||
runtimeProjection: runtime.projection,
|
||||
refreshDiagnostics: () => connectionController.refreshDiagnostics(),
|
||||
refreshDiagnostics: () => connectionActions.refreshDiagnostics(),
|
||||
refreshLiveState,
|
||||
notifyActiveThreadIdentityChanged,
|
||||
});
|
||||
const shell = createShellBundle(host, {
|
||||
connection,
|
||||
connectionController,
|
||||
connectionActions,
|
||||
goals: threadLifecycle.goals,
|
||||
rename: threadLifecycle.rename,
|
||||
threadActions: threadActions.actions,
|
||||
|
|
@ -225,7 +225,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
|
|||
return {
|
||||
connection: {
|
||||
manager: connection,
|
||||
controller: connectionController,
|
||||
actions: connectionActions,
|
||||
},
|
||||
thread: {
|
||||
resume: threadLifecycle.resume,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { ChatState } from "../application/state/root-reducer";
|
|||
import { type ChatStateStore, createChatStateStore } from "../application/state/store";
|
||||
import { parseRestoredThreadState, type RestoredThreadPlaceholderState } from "../application/threads/restored-thread-lifecycle";
|
||||
import { ChatResumeWorkTracker } from "../application/threads/resume-work";
|
||||
import { type ChatMessageScrollController, createChatMessageScrollController } from "../panel/message-stream-scroll-controller";
|
||||
import { type ChatMessageStreamScrollBinding, createChatMessageStreamScrollBinding } from "../panel/message-stream-scroll-binding";
|
||||
import { renderChatPanelShell, unmountChatPanelShell } from "../panel/shell.dom";
|
||||
import type { ChatPanelEnvironment, ChatPanelHandle, ChatWorkspacePanelSnapshot, ChatWorkspacePanelTurnLifecycle } from "./contracts";
|
||||
import { type ChatViewDeferredTasks, createChatViewDeferredTasks } from "./session/deferred-work";
|
||||
|
|
@ -20,7 +20,7 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
private readonly deferredTasks: ChatViewDeferredTasks;
|
||||
private readonly connectionWork = new ConnectionWorkTracker();
|
||||
private readonly resumeWork = new ChatResumeWorkTracker();
|
||||
private readonly messageScrollController: ChatMessageScrollController = createChatMessageScrollController();
|
||||
private readonly messageScrollBinding: ChatMessageStreamScrollBinding = createChatMessageStreamScrollBinding();
|
||||
private observedAppServerContext: AppServerQueryContext;
|
||||
private opened = false;
|
||||
private closing = false;
|
||||
|
|
@ -175,7 +175,7 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
await this.graph.connection.controller.ensureConnected();
|
||||
await this.graph.connection.actions.ensureConnected();
|
||||
}
|
||||
|
||||
async startNewThread(): Promise<void> {
|
||||
|
|
@ -202,7 +202,7 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
|
||||
this.deferredTasks.scheduleAppServerWarmup(() => {
|
||||
if (!shouldWarmup() || this.closing) return;
|
||||
void this.graph.connection.controller.ensureConnected();
|
||||
void this.graph.connection.actions.ensureConnected();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -251,7 +251,7 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
deferredTasks: this.deferredTasks,
|
||||
resumeWork: this.resumeWork,
|
||||
connectionWork: this.connectionWork,
|
||||
messageScrollController: this.messageScrollController,
|
||||
messageScrollBinding: this.messageScrollBinding,
|
||||
getClosing: () => this.closing,
|
||||
viewWindow: () => this.viewWindow(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { copyTextWithNotice } from "../../../shared/obsidian/clipboard.obsidian";
|
||||
import type { ChatConnectionController } from "../application/connection/connection-controller";
|
||||
import type { ChatConnectionActions } from "../application/connection/connection-actions";
|
||||
import type { ChatAction, ChatState } from "../application/state/root-reducer";
|
||||
import type { ChatStateStore } from "../application/state/store";
|
||||
import type { GoalActions } from "../application/threads/goal-actions";
|
||||
|
|
@ -26,7 +26,7 @@ export interface ToolbarPanelActions {
|
|||
}
|
||||
|
||||
export interface ToolbarUiActionDependencies {
|
||||
connectionController: ChatConnectionController;
|
||||
connectionActions: ChatConnectionActions;
|
||||
reconnectPanel: () => Promise<void>;
|
||||
threadActions: ThreadManagementActions;
|
||||
goals: GoalActions;
|
||||
|
|
@ -146,7 +146,7 @@ export function createToolbarUiActions(deps: ToolbarUiActionDependencies): Toolb
|
|||
void deps.reconnectPanel();
|
||||
},
|
||||
refreshStatus: () => {
|
||||
void deps.connectionController.refreshStatusPanel();
|
||||
void deps.connectionActions.refreshStatusPanel();
|
||||
},
|
||||
copyDebugDetails: (details) => {
|
||||
void copyTextWithNotice(details, "Copied debug details.", "Could not copy debug details.");
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export interface MessageStreamScrollPort {
|
|||
dispatchScrollCommand(command: MessageStreamScrollCommand): void;
|
||||
}
|
||||
|
||||
export interface MessageStreamScrollControllerBinding {
|
||||
export interface MessageStreamScrollPortBinding {
|
||||
mountScrollPort(port: MessageStreamScrollPort): () => void;
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ export interface MessageStreamFlowBlockIdentity {
|
|||
export interface MessageStreamFlowFrameProps<Block extends MessageStreamFlowBlockIdentity> {
|
||||
blocks: readonly Block[];
|
||||
rootAttributes?: Partial<Record<`data-${string}`, string>>;
|
||||
scrollController: MessageStreamScrollControllerBinding;
|
||||
scrollPortBinding: MessageStreamScrollPortBinding;
|
||||
renderBlockContent: (block: Block) => UiNode;
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ export class MessageStreamFlowFrame<Block extends MessageStreamFlowBlockIdentity
|
|||
_previousState: Readonly<Record<string, never>>,
|
||||
snapshot: MessageFlowSnapshot | null,
|
||||
): void {
|
||||
if (previousProps.scrollController !== this.props.scrollController) this.mountScrollPort();
|
||||
if (previousProps.scrollPortBinding !== this.props.scrollPortBinding) this.mountScrollPort();
|
||||
if (this.runtime.container !== this.scrollElement) attachMessageFlowContainer(this.runtime, this.scrollElement);
|
||||
completeMessageFlowRender(this.runtime, snapshot);
|
||||
}
|
||||
|
|
@ -119,7 +119,7 @@ export class MessageStreamFlowFrame<Block extends MessageStreamFlowBlockIdentity
|
|||
|
||||
private mountScrollPort(): void {
|
||||
this.unmountScrollPort?.();
|
||||
this.unmountScrollPort = this.props.scrollController.mountScrollPort(this.scrollPort);
|
||||
this.unmountScrollPort = this.props.scrollPortBinding.mountScrollPort(this.scrollPort);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type {
|
|||
} from "../../presentation/message-stream/view-model";
|
||||
import type { MessageStreamContext, PendingRequestBlockContext } from "./context";
|
||||
import { detailNode } from "./detail";
|
||||
import { MessageStreamFlowFrame, type MessageStreamScrollControllerBinding } from "./flow-scroll.measure";
|
||||
import { MessageStreamFlowFrame, type MessageStreamScrollPortBinding } from "./flow-scroll.measure";
|
||||
import { pendingRequestBlockNode } from "./pending-request-block";
|
||||
import { agentRunSummaryNode, statusNode } from "./status";
|
||||
import { textNode } from "./text";
|
||||
|
|
@ -15,7 +15,7 @@ import { textNode } from "./text";
|
|||
export interface MessageStreamViewportState {
|
||||
blocks: readonly MessageStreamViewBlock[];
|
||||
context: MessageStreamContext;
|
||||
scrollController: MessageStreamScrollControllerBinding;
|
||||
scrollPortBinding: MessageStreamScrollPortBinding;
|
||||
}
|
||||
|
||||
interface MessageStreamViewportProps {
|
||||
|
|
@ -24,11 +24,11 @@ interface MessageStreamViewportProps {
|
|||
}
|
||||
|
||||
export function MessageStreamViewport({ state, rootAttributes }: MessageStreamViewportProps): UiNode {
|
||||
const { blocks, context, scrollController } = state;
|
||||
const { blocks, context, scrollPortBinding } = state;
|
||||
return (
|
||||
<MessageStreamFlowFrame
|
||||
blocks={blocks}
|
||||
scrollController={scrollController}
|
||||
scrollPortBinding={scrollPortBinding}
|
||||
renderBlockContent={(block) => <MessageStreamBlockContent block={block} context={context} />}
|
||||
{...(rootAttributes ? { rootAttributes } : {})}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 } }),
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture();
|
||||
const messages: string[] = [];
|
||||
const controller = createChatRuntimeSettingsActions({
|
||||
const actions = createChatRuntimeSettingsActions({
|
||||
stateStore: store,
|
||||
runtimeTransport: transport,
|
||||
runtimeSnapshotForState: runtimeSnapshotFixture,
|
||||
|
|
@ -28,7 +28,7 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
addSystemMessage: (text) => messages.push(text),
|
||||
});
|
||||
|
||||
await expect(controller.requestModel("gpt-5.5")).resolves.toBe(true);
|
||||
await expect(actions.requestModel("gpt-5.5")).resolves.toBe(true);
|
||||
|
||||
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { model: "gpt-5.5" });
|
||||
expect(store.getState().runtime.pending.model).toEqual({ kind: "unchanged" });
|
||||
|
|
@ -53,9 +53,9 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture();
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await expect(controller.applyPendingThreadSettings()).resolves.toBe(true);
|
||||
await expect(actions.applyPendingThreadSettings()).resolves.toBe(true);
|
||||
|
||||
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { permissions: ":workspace" });
|
||||
expect(store.getState().runtime.pending.permissionProfile).toEqual({ kind: "unchanged" });
|
||||
|
|
@ -70,14 +70,14 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(chatStateFixture());
|
||||
const transport = settingsTransportFixture();
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await expect(controller.requestModel("gpt-5.5")).resolves.toBe(true);
|
||||
await expect(controller.requestReasoningEffort("high")).resolves.toBe(true);
|
||||
await controller.enableFastMode();
|
||||
await controller.enableAutoReview();
|
||||
await expect(controller.setCollaborationMode("plan")).resolves.toBe(true);
|
||||
controller.requestDefaultCollaborationModeForNextTurn();
|
||||
await expect(actions.requestModel("gpt-5.5")).resolves.toBe(true);
|
||||
await expect(actions.requestReasoningEffort("high")).resolves.toBe(true);
|
||||
await actions.enableFastMode();
|
||||
await actions.enableAutoReview();
|
||||
await expect(actions.setCollaborationMode("plan")).resolves.toBe(true);
|
||||
actions.requestDefaultCollaborationModeForNextTurn();
|
||||
|
||||
expect(transport.updateThreadSettings).not.toHaveBeenCalled();
|
||||
expect(store.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
|
||||
|
|
@ -99,9 +99,9 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture();
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await controller.toggleFastMode();
|
||||
await actions.toggleFastMode();
|
||||
|
||||
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { serviceTier: "fast" });
|
||||
expect(store.getState().runtime.pending.fastMode).toEqual({ kind: "unchanged" });
|
||||
|
|
@ -116,11 +116,11 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture();
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await controller.enableFastMode();
|
||||
await actions.enableFastMode();
|
||||
store.dispatch({ type: "active-thread/settings-applied", ...threadSettings("fast") });
|
||||
await controller.disableFastMode();
|
||||
await actions.disableFastMode();
|
||||
|
||||
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(1, "thread", { serviceTier: "fast" });
|
||||
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(2, "thread", { serviceTier: null });
|
||||
|
|
@ -135,15 +135,15 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture();
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await controller.disableFastMode();
|
||||
await actions.disableFastMode();
|
||||
|
||||
expect(transport.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: null });
|
||||
expect(store.getState().runtime.active.serviceTier).toBeNull();
|
||||
expect(store.getState().runtime.active.serviceTierKnown).toBe(true);
|
||||
|
||||
await controller.toggleFastMode();
|
||||
await actions.toggleFastMode();
|
||||
|
||||
expect(transport.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: "fast" });
|
||||
expect(messages).toEqual(["Fast mode off for subsequent turns.", "Fast mode on for subsequent turns."]);
|
||||
|
|
@ -159,15 +159,15 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture();
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await controller.toggleFastMode();
|
||||
await actions.toggleFastMode();
|
||||
|
||||
expect(transport.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: "priority" });
|
||||
expect(store.getState().runtime.active.serviceTier).toBe("priority");
|
||||
|
||||
store.dispatch({ type: "active-thread/settings-applied", ...threadSettings("priority") });
|
||||
await controller.toggleFastMode();
|
||||
await actions.toggleFastMode();
|
||||
|
||||
expect(transport.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: null });
|
||||
expect(store.getState().runtime.active.serviceTier).toBeNull();
|
||||
|
|
@ -180,9 +180,9 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture();
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await expect(controller.setCollaborationMode("plan")).resolves.toBe(true);
|
||||
await expect(actions.setCollaborationMode("plan")).resolves.toBe(true);
|
||||
|
||||
expect(transport.updateThreadSettings).not.toHaveBeenCalled();
|
||||
expect(store.getState().runtime.pending.collaborationMode).toEqual(setCollaborationModeIntent("plan"));
|
||||
|
|
@ -198,9 +198,9 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture();
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
controller.requestDefaultCollaborationModeForNextTurn();
|
||||
actions.requestDefaultCollaborationModeForNextTurn();
|
||||
|
||||
expect(transport.updateThreadSettings).not.toHaveBeenCalled();
|
||||
expect(store.getState().runtime.pending.collaborationMode).toEqual(setCollaborationModeIntent("default"));
|
||||
|
|
@ -223,7 +223,7 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture();
|
||||
const messages: string[] = [];
|
||||
const controller = createChatRuntimeSettingsActions({
|
||||
const actions = createChatRuntimeSettingsActions({
|
||||
stateStore: store,
|
||||
runtimeTransport: transport,
|
||||
runtimeSnapshotForState: (state) => ({ ...runtimeSnapshotFixture(state), runtimeConfig: null }),
|
||||
|
|
@ -231,7 +231,7 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
addSystemMessage: (text) => messages.push(text),
|
||||
});
|
||||
|
||||
await expect(controller.setCollaborationMode("plan")).resolves.toBe(true);
|
||||
await expect(actions.setCollaborationMode("plan")).resolves.toBe(true);
|
||||
|
||||
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", {
|
||||
collaborationMode: {
|
||||
|
|
@ -252,9 +252,9 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture({ updateThreadSettings: vi.fn().mockRejectedValue(new Error("nope")) });
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await expect(controller.requestModel("gpt-5.5")).resolves.toBe(false);
|
||||
await expect(actions.requestModel("gpt-5.5")).resolves.toBe(false);
|
||||
|
||||
expect(store.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
|
||||
expect(store.getState().runtime.active.model).toBeNull();
|
||||
|
|
@ -267,9 +267,9 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture({ updateThreadSettings: vi.fn().mockResolvedValue(false) });
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await expect(controller.requestModel("gpt-5.5")).resolves.toBe(false);
|
||||
await expect(actions.requestModel("gpt-5.5")).resolves.toBe(false);
|
||||
|
||||
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { model: "gpt-5.5" });
|
||||
expect(store.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
|
||||
|
|
@ -284,9 +284,9 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture({ updateThreadSettings: vi.fn().mockRejectedValue(new Error("nope")) });
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await controller.enableFastMode();
|
||||
await actions.enableFastMode();
|
||||
|
||||
expect(store.getState().runtime.pending.fastMode).toEqual({ kind: "set", value: "enabled" });
|
||||
expect(store.getState().ui.toolbarPanel).toBe("status-panel");
|
||||
|
|
@ -304,9 +304,9 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
}),
|
||||
});
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await expect(controller.requestModel("gpt-5.5")).resolves.toBe(false);
|
||||
await expect(actions.requestModel("gpt-5.5")).resolves.toBe(false);
|
||||
|
||||
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { model: "gpt-5.5" });
|
||||
expect(store.getState().activeThread.id).toBeNull();
|
||||
|
|
@ -326,9 +326,9 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
}),
|
||||
});
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await expect(controller.requestModel("gpt-5.5")).resolves.toBe(false);
|
||||
await expect(actions.requestModel("gpt-5.5")).resolves.toBe(false);
|
||||
|
||||
expect(store.getState().activeThread.id).toBeNull();
|
||||
expect(store.getState().runtime.active.model).toBeNull();
|
||||
|
|
@ -349,12 +349,12 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
.mockImplementationOnce(() => secondUpdate.promise),
|
||||
});
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
const firstRequest = controller.requestModel("gpt-old");
|
||||
const firstRequest = actions.requestModel("gpt-old");
|
||||
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(1, "thread", { model: "gpt-old" });
|
||||
|
||||
const secondRequest = controller.requestModel("gpt-new");
|
||||
const secondRequest = actions.requestModel("gpt-new");
|
||||
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(2, "thread", { model: "gpt-new" });
|
||||
|
||||
secondUpdate.resolve();
|
||||
|
|
@ -376,9 +376,9 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture();
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await expect(controller.resetModelToConfig()).resolves.toBe(true);
|
||||
await expect(actions.resetModelToConfig()).resolves.toBe(true);
|
||||
|
||||
expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { model: null });
|
||||
expect(store.getState().runtime.pending.model).toEqual({ kind: "unchanged" });
|
||||
|
|
@ -391,11 +391,11 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
const store = createChatStateStore(state);
|
||||
const transport = settingsTransportFixture();
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, transport, messages);
|
||||
const actions = runtimeActionsFixture(store, transport, messages);
|
||||
|
||||
await controller.enableAutoReview();
|
||||
await actions.enableAutoReview();
|
||||
store.dispatch({ type: "active-thread/settings-applied", ...threadSettings(null, "auto_review") });
|
||||
await controller.disableAutoReview();
|
||||
await actions.disableAutoReview();
|
||||
|
||||
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(1, "thread", { approvalsReviewer: "auto_review" });
|
||||
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(2, "thread", { approvalsReviewer: "user" });
|
||||
|
|
@ -403,7 +403,7 @@ describe("createChatRuntimeSettingsActions", () => {
|
|||
});
|
||||
});
|
||||
|
||||
function runtimeControllerFixture(
|
||||
function runtimeActionsFixture(
|
||||
store: ReturnType<typeof createChatStateStore>,
|
||||
transport: RuntimeSettingsTransport,
|
||||
messages: string[],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ describe("createGoalActions", () => {
|
|||
const currentGoal = goal();
|
||||
const goalTransport = goalTransportFixture({ readThreadGoal: vi.fn().mockResolvedValue(currentGoal) });
|
||||
const refreshLiveState = vi.fn();
|
||||
const controller = createGoalActions({
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
|
|
@ -26,7 +26,7 @@ describe("createGoalActions", () => {
|
|||
refreshLiveState,
|
||||
});
|
||||
|
||||
await controller.syncThreadGoal("thread");
|
||||
await actions.syncThreadGoal("thread");
|
||||
|
||||
expect(stateStore.getState().activeThread.goal).toEqual(currentGoal);
|
||||
expect(refreshLiveState).toHaveBeenCalledOnce();
|
||||
|
|
@ -38,7 +38,7 @@ describe("createGoalActions", () => {
|
|||
const stateStore = createChatStateStore(state);
|
||||
const addSystemMessage = vi.fn();
|
||||
const goalTransport = goalTransportFixture({ readThreadGoal: vi.fn().mockRejectedValue(new Error("offline")) });
|
||||
const controller = createGoalActions({
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
|
|
@ -48,7 +48,7 @@ describe("createGoalActions", () => {
|
|||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await controller.syncThreadGoal("thread");
|
||||
await actions.syncThreadGoal("thread");
|
||||
|
||||
expect(stateStore.getState().activeThread.id).toBe("thread");
|
||||
expect(stateStore.getState().activeThread.goal).toBeNull();
|
||||
|
|
@ -69,7 +69,7 @@ describe("createGoalActions", () => {
|
|||
const { setThreadGoal, clearThreadGoal } = goalTransport;
|
||||
const addSystemMessage = vi.fn();
|
||||
const addGoalEvent = vi.fn();
|
||||
const controller = createGoalActions({
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
|
|
@ -79,9 +79,9 @@ describe("createGoalActions", () => {
|
|||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await controller.setObjective("thread", " Updated ", 250);
|
||||
await controller.setStatus("thread", "paused");
|
||||
await controller.clear("thread");
|
||||
await actions.setObjective("thread", " Updated ", 250);
|
||||
await actions.setStatus("thread", "paused");
|
||||
await actions.clear("thread");
|
||||
|
||||
expect(setThreadGoal).toHaveBeenCalledWith("thread", { objective: "Updated", status: "active", tokenBudget: 250 });
|
||||
expect(setThreadGoal).toHaveBeenCalledWith("thread", { status: "paused" });
|
||||
|
|
@ -101,7 +101,7 @@ describe("createGoalActions", () => {
|
|||
const update = deferred<never>();
|
||||
const goalTransport = goalTransportFixture({ setThreadGoal: vi.fn().mockReturnValue(update.promise) });
|
||||
const addSystemMessage = vi.fn();
|
||||
const controller = createGoalActions({
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
|
|
@ -111,7 +111,7 @@ describe("createGoalActions", () => {
|
|||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
const pending = controller.setStatus("thread", "paused");
|
||||
const pending = actions.setStatus("thread", "paused");
|
||||
await Promise.resolve();
|
||||
stateStore.dispatch({ type: "active-thread/cleared" });
|
||||
update.reject(new Error("offline"));
|
||||
|
|
@ -128,7 +128,7 @@ describe("createGoalActions", () => {
|
|||
const clear = deferred<never>();
|
||||
const goalTransport = goalTransportFixture({ clearThreadGoal: vi.fn().mockReturnValue(clear.promise) });
|
||||
const addSystemMessage = vi.fn();
|
||||
const controller = createGoalActions({
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
|
|
@ -138,7 +138,7 @@ describe("createGoalActions", () => {
|
|||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
const pending = controller.clear("thread");
|
||||
const pending = actions.clear("thread");
|
||||
await Promise.resolve();
|
||||
stateStore.dispatch({ type: "active-thread/cleared" });
|
||||
clear.reject(new Error("offline"));
|
||||
|
|
@ -154,7 +154,7 @@ describe("createGoalActions", () => {
|
|||
const goalTransport = goalTransportFixture({ setThreadGoal: vi.fn().mockResolvedValueOnce(goal()) });
|
||||
const addSystemMessage = vi.fn();
|
||||
const addGoalEvent = vi.fn();
|
||||
const controller = createGoalActions({
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
|
|
@ -164,7 +164,7 @@ describe("createGoalActions", () => {
|
|||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await controller.setObjective("thread", "Finish", null);
|
||||
await actions.setObjective("thread", "Finish", null);
|
||||
|
||||
expect(addSystemMessage).not.toHaveBeenCalledWith("Goal set.");
|
||||
expect(addGoalEvent).toHaveBeenCalledWith(
|
||||
|
|
@ -202,7 +202,7 @@ describe("createGoalActions", () => {
|
|||
});
|
||||
return { threadId: "thread-new" };
|
||||
});
|
||||
const controller = createGoalActions({
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
|
|
@ -212,7 +212,7 @@ describe("createGoalActions", () => {
|
|||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(controller.saveObjective(" Plan release ", null)).resolves.toBe(true);
|
||||
await expect(actions.saveObjective(" Plan release ", null)).resolves.toBe(true);
|
||||
|
||||
expect(startThread).toHaveBeenCalledWith("Plan release", { syncGoal: false });
|
||||
expect(setThreadGoal).toHaveBeenCalledWith("thread-new", { objective: "Plan release", status: "active", tokenBudget: null });
|
||||
|
|
@ -224,7 +224,7 @@ describe("createGoalActions", () => {
|
|||
const goalTransport = goalTransportFixture();
|
||||
const startThread = vi.fn().mockResolvedValue({ threadId: "thread" });
|
||||
const addSystemMessage = vi.fn();
|
||||
const controller = createGoalActions({
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
|
|
@ -234,7 +234,7 @@ describe("createGoalActions", () => {
|
|||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(controller.saveObjective(" ", null)).resolves.toBe(false);
|
||||
await expect(actions.saveObjective(" ", null)).resolves.toBe(false);
|
||||
|
||||
expect(addSystemMessage).toHaveBeenCalledWith("Goal objective cannot be empty.");
|
||||
expect(goalTransport.ensureConnected).not.toHaveBeenCalled();
|
||||
|
|
@ -250,7 +250,7 @@ describe("createGoalActions", () => {
|
|||
recordThreadGoalUserMessage: vi.fn().mockRejectedValue(new Error("offline")),
|
||||
});
|
||||
const addSystemMessage = vi.fn();
|
||||
const controller = createGoalActions({
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
|
|
@ -260,7 +260,7 @@ describe("createGoalActions", () => {
|
|||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await controller.setObjective("thread", "Finish", null);
|
||||
await actions.setObjective("thread", "Finish", null);
|
||||
|
||||
expect(addSystemMessage).toHaveBeenCalledWith("Could not record goal message: offline");
|
||||
});
|
||||
|
|
@ -277,7 +277,7 @@ describe("createGoalActions", () => {
|
|||
}),
|
||||
});
|
||||
const addSystemMessage = vi.fn();
|
||||
const controller = createGoalActions({
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
|
|
@ -287,7 +287,7 @@ describe("createGoalActions", () => {
|
|||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await controller.setObjective("thread", "Finish", null);
|
||||
await actions.setObjective("thread", "Finish", null);
|
||||
|
||||
expect(addSystemMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -299,7 +299,7 @@ describe("createGoalActions", () => {
|
|||
const stateStore = createChatStateStore(state);
|
||||
const goalTransport = goalTransportFixture({ setThreadGoal: vi.fn().mockResolvedValueOnce(goal({ objective: "Updated" })) });
|
||||
const addGoalEvent = vi.fn();
|
||||
const controller = createGoalActions({
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
|
|
@ -309,7 +309,7 @@ describe("createGoalActions", () => {
|
|||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await controller.setObjective("thread", "Updated", null);
|
||||
await actions.setObjective("thread", "Updated", null);
|
||||
|
||||
expect(addGoalEvent).toHaveBeenCalledWith(expect.objectContaining({ kind: "goal", text: "updated: Updated", objective: "Updated" }));
|
||||
expect(goalTransport.recordThreadGoalUserMessage).not.toHaveBeenCalled();
|
||||
|
|
@ -323,7 +323,7 @@ describe("createGoalActions", () => {
|
|||
const goalTransport = goalTransportFixture({ setThreadGoal: vi.fn().mockResolvedValueOnce(goal()) });
|
||||
const addSystemMessage = vi.fn();
|
||||
const addGoalEvent = vi.fn();
|
||||
const controller = createGoalActions({
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
|
|
@ -333,7 +333,7 @@ describe("createGoalActions", () => {
|
|||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await controller.setStatus("thread", "active");
|
||||
await actions.setStatus("thread", "active");
|
||||
|
||||
expect(addSystemMessage).not.toHaveBeenCalledWith("Goal resumed.");
|
||||
expect(addGoalEvent).toHaveBeenCalledWith(expect.objectContaining({ kind: "goal", text: "resumed: Finish", objective: "Finish" }));
|
||||
|
|
@ -346,7 +346,7 @@ describe("createGoalActions", () => {
|
|||
const currentGoal = goal();
|
||||
const goalTransport = goalTransportFixture({ readThreadGoal: vi.fn().mockResolvedValue(currentGoal) });
|
||||
const addSystemMessage = vi.fn();
|
||||
const controller = createGoalActions({
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
|
|
@ -356,7 +356,7 @@ describe("createGoalActions", () => {
|
|||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await controller.syncThreadGoal("thread");
|
||||
await actions.syncThreadGoal("thread");
|
||||
|
||||
expect(stateStore.getState().activeThread.goal).toEqual(currentGoal);
|
||||
expect(addSystemMessage).not.toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ function resumeThreadState(stateStore: ChatStateStore, threadId: string): void {
|
|||
});
|
||||
}
|
||||
|
||||
function createController(overrides: Partial<ThreadNavigationActionsHost> = {}) {
|
||||
function createActionsHarness(overrides: Partial<ThreadNavigationActionsHost> = {}) {
|
||||
const stateStore = createChatStateStore(createChatState());
|
||||
const host: ThreadNavigationActionsHost = {
|
||||
stateStore,
|
||||
|
|
@ -40,15 +40,15 @@ function createController(overrides: Partial<ThreadNavigationActionsHost> = {})
|
|||
focusComposer: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
return { controller: createThreadNavigationActions(host), host, stateStore };
|
||||
return { actions: createThreadNavigationActions(host), host, stateStore };
|
||||
}
|
||||
|
||||
describe("ThreadNavigationActions", () => {
|
||||
it("starts a blank chat by clearing active thread identity", async () => {
|
||||
const { controller, host, stateStore } = createController();
|
||||
const { actions, host, stateStore } = createActionsHarness();
|
||||
stateStore.dispatch({ type: "ui/panel-set", panel: "history" });
|
||||
|
||||
await controller.startNewThread();
|
||||
await actions.startNewThread();
|
||||
|
||||
expect(host.identity.clearActiveThreadIdentity).toHaveBeenCalledOnce();
|
||||
expect(stateStore.getState().ui.toolbarPanel).toBeNull();
|
||||
|
|
@ -57,12 +57,12 @@ describe("ThreadNavigationActions", () => {
|
|||
});
|
||||
|
||||
it("ignores blank chat navigation while a turn is running", async () => {
|
||||
const { controller, host, stateStore } = createController();
|
||||
const { actions, host, stateStore } = createActionsHarness();
|
||||
resumeThreadState(stateStore, "active");
|
||||
stateStore.dispatch({ type: "ui/panel-set", panel: "history" });
|
||||
stateStore.dispatch({ type: "turn/started", threadId: "active", turnId: "turn" });
|
||||
|
||||
await controller.startNewThread();
|
||||
await actions.startNewThread();
|
||||
|
||||
expect(host.identity.clearActiveThreadIdentity).not.toHaveBeenCalled();
|
||||
expect(stateStore.getState().ui.toolbarPanel).toBe("history");
|
||||
|
|
@ -70,11 +70,11 @@ describe("ThreadNavigationActions", () => {
|
|||
});
|
||||
|
||||
it("focuses an already open thread without resuming it", async () => {
|
||||
const { controller, host } = createController({
|
||||
const { actions, host } = createActionsHarness({
|
||||
focusThreadInOpenView: vi.fn().mockResolvedValue(true),
|
||||
});
|
||||
|
||||
await controller.selectThread("thread");
|
||||
await actions.selectThread("thread");
|
||||
|
||||
expect(host.closeForThreadSelection).toHaveBeenCalledOnce();
|
||||
expect(host.focusThreadInOpenView).toHaveBeenCalledWith("thread");
|
||||
|
|
@ -82,20 +82,20 @@ describe("ThreadNavigationActions", () => {
|
|||
});
|
||||
|
||||
it("resumes the thread when it is not already open", async () => {
|
||||
const { controller, host } = createController();
|
||||
const { actions, host } = createActionsHarness();
|
||||
|
||||
await controller.selectThread("thread");
|
||||
await actions.selectThread("thread");
|
||||
|
||||
expect(host.closeForThreadSelection).toHaveBeenCalledOnce();
|
||||
expect(host.resumeThread).toHaveBeenCalledWith("thread");
|
||||
});
|
||||
|
||||
it("blocks switching away while a turn is running", async () => {
|
||||
const { controller, host, stateStore } = createController();
|
||||
const { actions, host, stateStore } = createActionsHarness();
|
||||
resumeThreadState(stateStore, "active");
|
||||
stateStore.dispatch({ type: "turn/started", threadId: "active", turnId: "turn" });
|
||||
|
||||
await controller.selectThread("other");
|
||||
await actions.selectThread("other");
|
||||
|
||||
expect(host.addSystemMessage).toHaveBeenCalledWith("Finish or interrupt the current turn before switching threads.");
|
||||
expect(host.closeForThreadSelection).not.toHaveBeenCalled();
|
||||
|
|
@ -104,10 +104,10 @@ describe("ThreadNavigationActions", () => {
|
|||
});
|
||||
|
||||
it("closes the toolbar panel before selecting from the toolbar", async () => {
|
||||
const { controller, host, stateStore } = createController();
|
||||
const { actions, host, stateStore } = createActionsHarness();
|
||||
stateStore.dispatch({ type: "ui/panel-set", panel: "history" });
|
||||
|
||||
await controller.selectThreadFromToolbar("thread");
|
||||
await actions.selectThreadFromToolbar("thread");
|
||||
|
||||
expect(stateStore.getState().ui.toolbarPanel).toBeNull();
|
||||
expect(host.closeForThreadSelection).toHaveBeenCalledOnce();
|
||||
|
|
@ -115,12 +115,12 @@ describe("ThreadNavigationActions", () => {
|
|||
});
|
||||
|
||||
it("ignores toolbar selection while another thread is busy", async () => {
|
||||
const { controller, host, stateStore } = createController();
|
||||
const { actions, host, stateStore } = createActionsHarness();
|
||||
resumeThreadState(stateStore, "active");
|
||||
stateStore.dispatch({ type: "ui/panel-set", panel: "history" });
|
||||
stateStore.dispatch({ type: "turn/started", threadId: "active", turnId: "turn" });
|
||||
|
||||
await controller.selectThreadFromToolbar("other");
|
||||
await actions.selectThreadFromToolbar("other");
|
||||
|
||||
expect(stateStore.getState().ui.toolbarPanel).toBe("history");
|
||||
expect(host.addSystemMessage).not.toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ function turnBundleFixture(options: { stateStore?: ReturnType<typeof createChatS
|
|||
const bundle = createTurnBundle(
|
||||
{
|
||||
stateStore,
|
||||
messageScrollController: {
|
||||
messageScrollBinding: {
|
||||
showLatest: vi.fn(),
|
||||
},
|
||||
} as never,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import type { ChatPanelGoalSurface } from "../../../../src/features/chat/panel/s
|
|||
import type { ChatPanelToolbarSurface } from "../../../../src/features/chat/panel/surface/toolbar-projection";
|
||||
import { messageStreamViewBlocks } from "../../../../src/features/chat/presentation/message-stream/view-model";
|
||||
import type { MessageStreamContext } from "../../../../src/features/chat/ui/message-stream/context";
|
||||
import type { MessageStreamScrollControllerBinding } from "../../../../src/features/chat/ui/message-stream/flow-scroll.measure";
|
||||
import type { MessageStreamScrollPortBinding } from "../../../../src/features/chat/ui/message-stream/flow-scroll.measure";
|
||||
import { installObsidianDomShims } from "../../../support/dom";
|
||||
|
||||
installObsidianDomShims();
|
||||
|
|
@ -429,7 +429,7 @@ function shellParts(
|
|||
items: model.items.value,
|
||||
}),
|
||||
context: testMessageStreamContext,
|
||||
scrollController: noOpMessageStreamScrollController,
|
||||
scrollPortBinding: noOpMessageStreamScrollPortBinding,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
@ -588,7 +588,7 @@ function surfaceFixture(options: { toolbarConnected?: () => boolean; goalSendSho
|
|||
};
|
||||
}
|
||||
|
||||
const noOpMessageStreamScrollController: MessageStreamScrollControllerBinding = {
|
||||
const noOpMessageStreamScrollPortBinding: MessageStreamScrollPortBinding = {
|
||||
mountScrollPort: () => () => undefined,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type { ChatPanelGoalSurface } from "../../../../src/features/chat/panel/s
|
|||
import type { ChatPanelToolbarSurface } from "../../../../src/features/chat/panel/surface/toolbar-projection";
|
||||
import { createToolbarPanelActions, type ToolbarPanelActions } from "../../../../src/features/chat/panel/toolbar-actions";
|
||||
import type { MessageStreamContext } from "../../../../src/features/chat/ui/message-stream/context";
|
||||
import type { MessageStreamScrollControllerBinding } from "../../../../src/features/chat/ui/message-stream/flow-scroll.measure";
|
||||
import type { MessageStreamScrollPortBinding } from "../../../../src/features/chat/ui/message-stream/flow-scroll.measure";
|
||||
import { installObsidianDomShims } from "../../../support/dom";
|
||||
|
||||
installObsidianDomShims();
|
||||
|
|
@ -113,7 +113,7 @@ function shellParts(store: ReturnType<typeof createChatStateStore>, toolbarPanel
|
|||
renderState: () => ({
|
||||
blocks: [],
|
||||
context: testMessageStreamContext,
|
||||
scrollController: noOpMessageStreamScrollController,
|
||||
scrollPortBinding: noOpMessageStreamScrollPortBinding,
|
||||
}),
|
||||
},
|
||||
composer: {
|
||||
|
|
@ -189,7 +189,7 @@ function surfaceFixture(
|
|||
};
|
||||
}
|
||||
|
||||
const noOpMessageStreamScrollController: MessageStreamScrollControllerBinding = {
|
||||
const noOpMessageStreamScrollPortBinding: MessageStreamScrollPortBinding = {
|
||||
mountScrollPort: () => () => undefined,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { MESSAGE_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat
|
|||
import {
|
||||
MessageStreamFlowFrame,
|
||||
type MessageStreamScrollCommand,
|
||||
type MessageStreamScrollControllerBinding,
|
||||
type MessageStreamScrollPort,
|
||||
type MessageStreamScrollPortBinding,
|
||||
} from "../../../../../src/features/chat/ui/message-stream/flow-scroll.measure";
|
||||
import { renderUiRoot } from "../../../../../src/shared/dom/preact-root.dom";
|
||||
import { installObsidianDomShims } from "../../../../support/dom";
|
||||
|
|
@ -265,11 +265,11 @@ interface CapturedScrollToOptions {
|
|||
behavior: ScrollBehavior | undefined;
|
||||
}
|
||||
|
||||
interface TestMessageStreamScrollController extends MessageStreamScrollControllerBinding {
|
||||
interface TestMessageStreamScrollPortBinding extends MessageStreamScrollPortBinding {
|
||||
dispatch(command: MessageStreamScrollCommand): void;
|
||||
}
|
||||
|
||||
function createTestMessageStreamScrollController(): TestMessageStreamScrollController {
|
||||
function createTestMessageStreamScrollPortBinding(): TestMessageStreamScrollPortBinding {
|
||||
let port: MessageStreamScrollPort | null = null;
|
||||
return {
|
||||
mountScrollPort(nextPort) {
|
||||
|
|
@ -292,7 +292,7 @@ function renderFlowMessageStream(
|
|||
viewport?: { width: number; height: number };
|
||||
} = {},
|
||||
): {
|
||||
controller: TestMessageStreamScrollController;
|
||||
controller: TestMessageStreamScrollPortBinding;
|
||||
messages: HTMLElement;
|
||||
render: (nextKeys: readonly string[], nextHeights: Record<string, number>) => void;
|
||||
setHeights: (nextHeights: Record<string, number>) => void;
|
||||
|
|
@ -301,7 +301,7 @@ function renderFlowMessageStream(
|
|||
} {
|
||||
const parent = document.createElement("div");
|
||||
document.body.append(parent);
|
||||
const controller = createTestMessageStreamScrollController();
|
||||
const controller = createTestMessageStreamScrollPortBinding();
|
||||
let currentHeights = heights;
|
||||
let currentKeys = keys;
|
||||
let viewport = options.viewport ?? { width: 240, height: 100 };
|
||||
|
|
@ -314,7 +314,7 @@ function renderFlowMessageStream(
|
|||
parent,
|
||||
h(MessageStreamFlowFrame, {
|
||||
blocks: nextKeys.map((key) => ({ key })),
|
||||
scrollController: controller,
|
||||
scrollPortBinding: controller,
|
||||
renderBlockContent: (block) => options.blockNode?.(block.key) ?? h("div", null, block.key),
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue