diff --git a/src/features/chat/application/connection/reconnect-actions.ts b/src/features/chat/application/connection/reconnect-actions.ts index 26c7828b..19744628 100644 --- a/src/features/chat/application/connection/reconnect-actions.ts +++ b/src/features/chat/application/connection/reconnect-actions.ts @@ -15,17 +15,7 @@ export interface ChatReconnectActionsHost { addSystemMessage: (text: string) => void; } -export interface ChatReconnectActions { - reconnectPanel: () => Promise; -} - -export function createChatReconnectActions(host: ChatReconnectActionsHost): ChatReconnectActions { - return { - reconnectPanel: () => reconnectPanel(host), - }; -} - -async function reconnectPanel(host: ChatReconnectActionsHost): Promise { +export async function reconnectPanel(host: ChatReconnectActionsHost): Promise { const threadId = activeThreadId(host.stateStore.getState()); host.stateStore.dispatch({ type: "ui/panel-set", panel: null }); host.invalidateConnectionWork(); diff --git a/src/features/chat/application/conversation/composer-submit-actions.ts b/src/features/chat/application/conversation/composer-submit-actions.ts index 16ae8e50..a561e834 100644 --- a/src/features/chat/application/conversation/composer-submit-actions.ts +++ b/src/features/chat/application/conversation/composer-submit-actions.ts @@ -37,13 +37,7 @@ export interface ComposerSubmitActions { submit: () => Promise; } -export function createComposerSubmitActions(host: ComposerSubmitActionsHost): ComposerSubmitActions { - return { - submit: () => submitComposer(host), - }; -} - -async function submitComposer(host: ComposerSubmitActionsHost): Promise { +export async function submitComposer(host: ComposerSubmitActionsHost): Promise { const draft = host.composer.trimmedDraft; const state = submissionStateSnapshot(host.stateStore.getState()); if (state.busy && state.activeThreadId && state.activeTurnId && draft.length === 0) { diff --git a/src/features/chat/application/conversation/composition.ts b/src/features/chat/application/conversation/composition.ts index d74fcaf3..ec726fa6 100644 --- a/src/features/chat/application/conversation/composition.ts +++ b/src/features/chat/application/conversation/composition.ts @@ -1,14 +1,14 @@ import type { AppServerClient } from "../../../../app-server/connection/client"; import type { CodexInput } from "../../../../domain/chat/input"; -import type { ChatReconnectActions } from "../connection/reconnect-actions"; import type { MessageStreamNoticeSection } from "../../domain/message-stream/items"; import type { ChatRuntimeSettingsActions } from "../runtime/settings-actions"; +import { canImplementPlan } from "../state/selectors"; import type { ChatStateStore } from "../state/store"; import type { ThreadManagementActions } from "../threads/thread-management-actions"; import type { GoalActions } from "../threads/goal-actions"; -import { createComposerSubmitActions, type ComposerSubmitActions } from "./composer-submit-actions"; -import { createPlanImplementation, type PlanImplementation } from "./plan-implementation"; -import { createSlashCommandHandler } from "./slash-command-handler"; +import { submitComposer, type ComposerSubmitActions, type ComposerSubmitActionsHost } from "./composer-submit-actions"; +import { implementPlan, type PlanImplementation, type PlanImplementationHost } from "./plan-implementation"; +import { executeSlashCommandWithState, type SlashCommandHandlerHost } from "./slash-command-handler"; import { TurnSubmissionController } from "./turn-submission-controller"; export interface ConversationTurnActionsContext { @@ -51,7 +51,7 @@ export interface ConversationTurnActionsRefs { threadStarter: ConversationThreadStarter; runtimeSettings: ChatRuntimeSettingsActions; threadActions: ThreadManagementActions; - reconnectActions: ChatReconnectActions; + reconnectPanel: () => Promise; goals: GoalActions; } @@ -83,7 +83,7 @@ export function createConversationTurnActions( setStatus: status.set, addSystemMessage: status.addSystemMessage, }); - const slashCommands = createSlashCommandHandler({ + const slashCommandHost: SlashCommandHandlerHost = { stateStore, currentClient: client.currentClient, codexInput: composer.codexInput, @@ -95,7 +95,7 @@ export function createConversationTurnActions( compactThread: (threadId) => refs.threadActions.compactThread(threadId), archiveThread: (threadId, saveMarkdown) => refs.threadActions.archiveThread(threadId, saveMarkdown), renameThread: (threadId, name) => refs.threadActions.renameThread(threadId, name).then(() => undefined), - reconnect: () => refs.reconnectActions.reconnectPanel(), + reconnect: refs.reconnectPanel, toggleFastMode: () => refs.runtimeSettings.toggleFastMode(), toggleCollaborationMode: () => refs.runtimeSettings.toggleCollaborationMode(), toggleAutoReview: () => void refs.runtimeSettings.toggleAutoReview(), @@ -115,8 +115,8 @@ export function createConversationTurnActions( mcpStatusLines: runtime.mcpStatusLines, modelStatusLines: runtime.modelStatusLines, effortStatusLines: runtime.effortStatusLines, - }); - const planImplementation = createPlanImplementation({ + }; + const planImplementationHost: PlanImplementationHost = { stateStore, currentClient: client.currentClient, ensureConnected: client.ensureConnected, @@ -124,8 +124,8 @@ export function createConversationTurnActions( requestDefaultCollaborationModeForNextTurn: () => { refs.runtimeSettings.requestDefaultCollaborationModeForNextTurn(); }, - }); - const composerSubmit = createComposerSubmitActions({ + }; + const composerSubmitHost: ComposerSubmitActionsHost = { stateStore, composer: { get trimmedDraft() { @@ -133,7 +133,9 @@ export function createConversationTurnActions( }, setDraft: composer.setDraft, }, - slashCommands, + slashCommands: { + execute: (command, args) => executeSlashCommandWithState(slashCommandHost, command, args), + }, turnSubmission, connection: { currentClient: client.currentClient, @@ -144,11 +146,16 @@ export function createConversationTurnActions( addSystemMessage: status.addSystemMessage, }, scroll, - }); + }; return { - planImplementation, - composerSubmit, + planImplementation: { + canImplement: (item) => canImplementPlan(stateStore.getState(), item), + implement: (item) => implementPlan(planImplementationHost, item), + }, + composerSubmit: { + submit: () => submitComposer(composerSubmitHost), + }, }; } diff --git a/src/features/chat/application/conversation/plan-implementation.ts b/src/features/chat/application/conversation/plan-implementation.ts index 9c81b304..3aca2116 100644 --- a/src/features/chat/application/conversation/plan-implementation.ts +++ b/src/features/chat/application/conversation/plan-implementation.ts @@ -18,14 +18,7 @@ export interface PlanImplementation { implement: (item: MessageStreamItem) => Promise; } -export function createPlanImplementation(host: PlanImplementationHost): PlanImplementation { - return { - canImplement: (item) => canImplementPlan(host.stateStore.getState(), item), - implement: (item) => implementPlan(host, item), - }; -} - -async function implementPlan(host: PlanImplementationHost, item: MessageStreamItem): Promise { +export async function implementPlan(host: PlanImplementationHost, item: MessageStreamItem): Promise { if (!canImplementPlan(host.stateStore.getState(), item)) return; await host.ensureConnected(); if (!host.currentClient() || !activeThreadId(host.stateStore.getState())) return; diff --git a/src/features/chat/application/conversation/slash-command-handler.ts b/src/features/chat/application/conversation/slash-command-handler.ts index 62504700..1b38524c 100644 --- a/src/features/chat/application/conversation/slash-command-handler.ts +++ b/src/features/chat/application/conversation/slash-command-handler.ts @@ -27,17 +27,7 @@ export interface SlashCommandHandlerHost extends Omit void; } -export interface SlashCommandHandler { - execute: (command: SlashCommandName, args: string) => Promise; -} - -export function createSlashCommandHandler(host: SlashCommandHandlerHost): SlashCommandHandler { - return { - execute: (command, args) => executeSlashCommand(host, command, args), - }; -} - -async function executeSlashCommand( +export async function executeSlashCommandWithState( host: SlashCommandHandlerHost, command: SlashCommandName, args: string, diff --git a/src/features/chat/application/runtime/messages.ts b/src/features/chat/application/runtime/messages.ts index 0904eec2..0fe28963 100644 --- a/src/features/chat/application/runtime/messages.ts +++ b/src/features/chat/application/runtime/messages.ts @@ -1,6 +1,6 @@ import type { ReasoningEffort } from "../../../../domain/catalog/metadata"; import type { CollaborationMode } from "../../domain/runtime/pending-settings"; -import type { TurnCollaborationModeWarning } from "../../domain/runtime/warnings"; +import type { TurnCollaborationModeWarning } from "./thread-settings-update"; const COLLABORATION_MODE_WARNING_MESSAGES: Record = { "missing-model": "No effective model is available. Sending without a mode override.", diff --git a/src/features/chat/application/runtime/thread-settings-update.ts b/src/features/chat/application/runtime/thread-settings-update.ts index 64f22ba5..5506fee4 100644 --- a/src/features/chat/application/runtime/thread-settings-update.ts +++ b/src/features/chat/application/runtime/thread-settings-update.ts @@ -8,7 +8,8 @@ import { currentModel, currentReasoningEffort, fastRuntimeServiceTierRequestValu import type { RuntimeConfigSnapshot } from "../../../../domain/runtime/config"; import type { RuntimeSnapshot } from "../../domain/runtime/snapshot"; import { effectiveCollaborationMode, type PendingRuntimeSetting } from "../../domain/runtime/pending-settings"; -import type { TurnCollaborationModeWarning } from "../../domain/runtime/warnings"; + +export type TurnCollaborationModeWarning = "missing-model"; type TurnCollaborationModeSettings = | { diff --git a/src/features/chat/application/threads/composition.ts b/src/features/chat/application/threads/composition.ts index 0fab7a77..3295cf6b 100644 --- a/src/features/chat/application/threads/composition.ts +++ b/src/features/chat/application/threads/composition.ts @@ -5,8 +5,9 @@ import { createSelectionActions } from "./selection-actions"; import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../lifecycle"; import type { ChatStateStore } from "../state/store"; import type { PluginSettingsRef, ThreadSurfaceBroadcaster, WorkspacePanels } from "../ports/chat-host"; -import { createThreadNamingParts } from "./naming-parts"; -import { createThreadManagementActions } from "./thread-management-actions"; +import { AutoTitleController } from "./auto-title-controller"; +import { ThreadRenameEditorController } from "./rename-editor-controller"; +import { createThreadManagementActions, type ThreadManagementActionsHost } from "./thread-management-actions"; import { createThreadLifecycleParts } from "./lifecycle-parts"; interface ThreadPartsContext { @@ -74,39 +75,55 @@ export function createThreadParts(context: ThreadPartsContext) { const stateStore = state.stateStore; const currentClient = client.getClient; - const naming = createThreadNamingParts({ - settingsRef, - threadSurfaces, + const rename = new ThreadRenameEditorController({ stateStore, - client: { - currentClient, - ensureConnected: client.ensureConnected, - }, - status: { - addSystemMessage: status.addSystemMessage, + vaultPath: settingsRef.vaultPath, + settings: () => settingsRef.settings, + ensureConnected: client.ensureConnected, + currentClient, + addSystemMessage: status.addSystemMessage, + notifyThreadRenamed: (threadId, name) => { + threadSurfaces.notifyThreadRenamed(threadId, name); + }, + }); + const autoTitle = new AutoTitleController({ + stateStore, + vaultPath: settingsRef.vaultPath, + settings: () => settingsRef.settings, + currentClient, + notifyThreadRenamed: (threadId, name) => { + threadSurfaces.notifyThreadRenamed(threadId, name); }, }); - const { rename, autoTitle } = naming; - const managementActions = createThreadManagementActions({ - obsidian, - settingsRef, - workspace, - threadSurfaces, + const threadManagementHost: ThreadManagementActionsHost = { stateStore, - client: { - currentClient, - ensureConnected: client.ensureConnected, + vaultPath: settingsRef.vaultPath, + settings: () => settingsRef.settings, + archiveAdapter: obsidian.archiveAdapter, + ensureConnected: client.ensureConnected, + currentClient, + addSystemMessage: status.addSystemMessage, + showNotice: notify.showNotice, + setStatus: status.set, + setComposerText: composer.setText, + openThreadInNewView: (threadId) => workspace.openThreadInNewView(threadId), + openThreadInCurrentPanel: (threadId) => thread.selectThread(threadId), + notifyThreadArchived: (threadId) => { + threadSurfaces.notifyThreadArchived(threadId); }, - status, - notify, - thread: { - selectThread: thread.selectThread, - refreshThreads: thread.refreshThreads, - notifyIdentityChanged: thread.notifyIdentityChanged, + notifyThreadRenamed: (threadId, name) => { + threadSurfaces.notifyThreadRenamed(threadId, name); }, - composer, - }); + notifyActiveThreadIdentityChanged: () => { + thread.notifyIdentityChanged(); + }, + refreshThreads: () => thread.refreshThreads(), + refreshSharedThreadListFromOpenSurface: () => { + threadSurfaces.refreshSharedThreadListFromOpenSurface(); + }, + }; + const managementActions = createThreadManagementActions(threadManagementHost); const goals = createGoalActions({ stateStore, currentClient, diff --git a/src/features/chat/application/threads/naming-parts.ts b/src/features/chat/application/threads/naming-parts.ts deleted file mode 100644 index 6b6ee1e5..00000000 --- a/src/features/chat/application/threads/naming-parts.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { AppServerClient } from "../../../../app-server/connection/client"; -import type { ChatStateStore } from "../state/store"; -import type { PluginSettingsRef, ThreadSurfaceBroadcaster } from "../ports/chat-host"; -import { AutoTitleController } from "./auto-title-controller"; -import { ThreadRenameEditorController } from "./rename-editor-controller"; - -export interface ThreadNamingPartsContext { - settingsRef: PluginSettingsRef; - threadSurfaces: ThreadSurfaceBroadcaster; - stateStore: ChatStateStore; - client: { - currentClient: () => AppServerClient | null; - ensureConnected: () => Promise; - }; - status: { - addSystemMessage: (text: string) => void; - }; -} - -export interface ThreadNamingParts { - rename: ThreadRenameEditorController; - autoTitle: AutoTitleController; -} - -export function createThreadNamingParts(context: ThreadNamingPartsContext): ThreadNamingParts { - const { settingsRef, threadSurfaces, stateStore, client, status } = context; - const rename = new ThreadRenameEditorController({ - stateStore, - vaultPath: settingsRef.vaultPath, - settings: () => settingsRef.settings, - ensureConnected: client.ensureConnected, - currentClient: client.currentClient, - addSystemMessage: status.addSystemMessage, - notifyThreadRenamed: (threadId, name) => { - threadSurfaces.notifyThreadRenamed(threadId, name); - }, - }); - const autoTitle = new AutoTitleController({ - stateStore, - vaultPath: settingsRef.vaultPath, - settings: () => settingsRef.settings, - currentClient: client.currentClient, - notifyThreadRenamed: (threadId, name) => { - threadSurfaces.notifyThreadRenamed(threadId, name); - }, - }); - - return { - rename, - autoTitle, - }; -} diff --git a/src/features/chat/application/threads/thread-management-actions.ts b/src/features/chat/application/threads/thread-management-actions.ts index e65efc62..d96a9274 100644 --- a/src/features/chat/application/threads/thread-management-actions.ts +++ b/src/features/chat/application/threads/thread-management-actions.ts @@ -5,7 +5,6 @@ import type { CodexPanelSettings } from "../../../../settings/model"; import type { ArchiveExportAdapter } from "../../../thread-operations/archive-markdown"; import { archiveThreadOnAppServer } from "../../../thread-operations/archive"; import { renameThreadOnAppServer, threadRenameFromValue, type ThreadRename } from "../../../thread-operations/rename"; -import type { PluginSettingsRef, ThreadSurfaceBroadcaster, WorkspacePanels } from "../ports/chat-host"; import { archivedSourceOpenForkFailedMessage, finishBeforeArchivingThreadsMessage, @@ -56,35 +55,6 @@ export interface ThreadManagementActions { rollbackThread: (threadId: string) => Promise; } -export interface ThreadManagementActionsContext { - obsidian: { - archiveAdapter: () => ArchiveExportAdapter; - }; - settingsRef: PluginSettingsRef; - workspace: Pick; - threadSurfaces: Pick; - stateStore: ChatStateStore; - client: { - currentClient: () => AppServerClient | null; - ensureConnected: () => Promise; - }; - status: { - set: (status: string) => void; - addSystemMessage: (text: string) => void; - }; - notify: { - showNotice: (text: string) => void; - }; - thread: { - selectThread: (threadId: string) => Promise; - refreshThreads: () => Promise; - notifyIdentityChanged: () => void; - }; - composer: { - setText: (text: string) => void; - }; -} - type RenameThreadHost = Pick< ThreadManagementActionsHost, "ensureConnected" | "currentClient" | "stateStore" | "addSystemMessage" | "notifyThreadRenamed" @@ -95,32 +65,7 @@ type ConnectedRenameThreadHost = Pick< "currentClient" | "stateStore" | "addSystemMessage" | "notifyThreadRenamed" >; -export function createThreadManagementActions(context: ThreadManagementActionsContext): ThreadManagementActions { - const { obsidian, settingsRef, workspace, threadSurfaces, stateStore, client, status, notify, thread, composer } = context; - const host: ThreadManagementActionsHost = { - stateStore, - vaultPath: settingsRef.vaultPath, - settings: () => settingsRef.settings, - archiveAdapter: obsidian.archiveAdapter, - ensureConnected: client.ensureConnected, - currentClient: client.currentClient, - addSystemMessage: status.addSystemMessage, - showNotice: notify.showNotice, - setStatus: status.set, - setComposerText: composer.setText, - openThreadInNewView: workspace.openThreadInNewView, - openThreadInCurrentPanel: thread.selectThread, - notifyThreadArchived: threadSurfaces.notifyThreadArchived, - notifyThreadRenamed: (threadId, name) => { - threadSurfaces.notifyThreadRenamed(threadId, name); - }, - notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged, - refreshThreads: thread.refreshThreads, - refreshSharedThreadListFromOpenSurface: () => { - threadSurfaces.refreshSharedThreadListFromOpenSurface(); - }, - }; - +export function createThreadManagementActions(host: ThreadManagementActionsHost): ThreadManagementActions { return { compactThread: (threadId) => compactThread(host, threadId), archiveThread: (threadId, saveMarkdown) => archiveThread(host, threadId, saveMarkdown), diff --git a/src/features/chat/domain/runtime/warnings.ts b/src/features/chat/domain/runtime/warnings.ts deleted file mode 100644 index 3b6c9981..00000000 --- a/src/features/chat/domain/runtime/warnings.ts +++ /dev/null @@ -1 +0,0 @@ -export type TurnCollaborationModeWarning = "missing-model"; diff --git a/src/features/chat/host/conversation.ts b/src/features/chat/host/conversation.ts index ee83d02b..fb94894d 100644 --- a/src/features/chat/host/conversation.ts +++ b/src/features/chat/host/conversation.ts @@ -1,7 +1,6 @@ import type { App, Component } from "obsidian"; import type { AppServerClient } from "../../../app-server/connection/client"; import type { ChatStateStore } from "../application/state/store"; -import type { ChatReconnectActions } from "../application/connection/reconnect-actions"; import { PendingRequestController } from "../application/pending-requests/controller"; import type { ChatRuntimeSettingsActions } from "../application/runtime/settings-actions"; import type { ThreadManagementActions } from "../application/threads/thread-management-actions"; @@ -74,7 +73,7 @@ export function createConversationParts( threadStarter: ConversationThreadStarter; runtimeSettings: ChatRuntimeSettingsActions; threadActions: ThreadManagementActions; - reconnectActions: ChatReconnectActions; + reconnectPanel: () => Promise; goals: GoalActions; history: HistoryController; }, @@ -137,7 +136,7 @@ export function createConversationParts( threadStarter: refs.threadStarter, runtimeSettings: refs.runtimeSettings, threadActions: refs.threadActions, - reconnectActions: refs.reconnectActions, + reconnectPanel: refs.reconnectPanel, goals: refs.goals, }, ); diff --git a/src/features/chat/host/session.ts b/src/features/chat/host/session.ts index f7189087..86100366 100644 --- a/src/features/chat/host/session.ts +++ b/src/features/chat/host/session.ts @@ -11,7 +11,7 @@ import type { OpenCodexPanelSnapshot } from "../../../workspace/open-panel-snaps import type { ArchiveExportAdapter } from "../../thread-operations/archive-markdown"; import type { CodexChatHost } from "../application/ports/chat-host"; import { ChatConnectionController } from "../application/connection/connection-controller"; -import { createChatReconnectActions } from "../application/connection/reconnect-actions"; +import { reconnectPanel, type ChatReconnectActionsHost } from "../application/connection/reconnect-actions"; import { createChatServerDiagnosticsActions, type ChatServerDiagnosticsActions } from "../app-server/actions/diagnostics"; import { createChatServerMetadataActions, type ChatServerMetadataActions } from "../app-server/actions/metadata"; import { createChatServerThreadActions, type ChatServerThreadActions } from "../app-server/actions/threads"; @@ -450,7 +450,7 @@ export class ChatPanelSession { ); selectionRef.set(selection); - const reconnectActions = createChatReconnectActions({ + const reconnectHost: ChatReconnectActionsHost = { stateStore: this.stateStore, invalidateConnectionWork: () => { this.connectionWork.invalidate(); @@ -468,7 +468,8 @@ export class ChatPanelSession { ensureConnected: sessionPorts.ensureConnected, resumeThread: (threadId) => resume.resumeThread(threadId), addSystemMessage: sideEffects.status.addSystemMessage, - }); + }; + const reconnect = () => reconnectPanel(reconnectHost); const serverParts = this.createServerParts({ connection, sideEffects, @@ -488,7 +489,7 @@ export class ChatPanelSession { }, { connectionController, - reconnectActions, + reconnectPanel: reconnect, inboundController, threadActions, toolbarPanels, @@ -578,7 +579,7 @@ export class ChatPanelSession { threadStarter: serverThreads, runtimeSettings, threadActions, - reconnectActions, + reconnectPanel: reconnect, goals, history, }, diff --git a/src/features/chat/panel/toolbar-actions.ts b/src/features/chat/panel/toolbar-actions.ts index 708bcafb..95a2a6f0 100644 --- a/src/features/chat/panel/toolbar-actions.ts +++ b/src/features/chat/panel/toolbar-actions.ts @@ -2,7 +2,6 @@ import type { ThreadManagementActions } from "../application/threads/thread-mana import type { ChatAction, ChatState } from "../application/state/root-reducer"; import type { ChatStateStore } from "../application/state/store"; import type { ChatConnectionController } from "../application/connection/connection-controller"; -import type { ChatReconnectActions } from "../application/connection/reconnect-actions"; import type { ThreadRenameEditorController } from "../application/threads/rename-editor-controller"; import type { SelectionActions } from "../application/threads/selection-actions"; import type { ChatInboundController } from "../app-server/inbound/controller"; @@ -33,7 +32,7 @@ export interface ChatPanelToolbarActionsHost { export interface ChatPanelToolbarActionDependencies { connectionController: ChatConnectionController; - reconnectActions: ChatReconnectActions; + reconnectPanel: () => Promise; inboundController: ChatInboundController; threadActions: ThreadManagementActions; toolbarPanels: ToolbarPanelActions; @@ -154,7 +153,7 @@ export function createChatPanelToolbarActions(host: ChatPanelToolbarActionsHost, deps.toolbarPanels.toggleStatus(); }, connect: () => { - void deps.reconnectActions.reconnectPanel(); + void deps.reconnectPanel(); }, refreshStatus: () => { void deps.connectionController.refreshStatusPanel(); diff --git a/tests/features/chat/connection/reconnect-actions.test.ts b/tests/features/chat/connection/reconnect-actions.test.ts index 08929b92..51bb1321 100644 --- a/tests/features/chat/connection/reconnect-actions.test.ts +++ b/tests/features/chat/connection/reconnect-actions.test.ts @@ -2,10 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import { createChatState } from "../../../../src/features/chat/application/state/root-reducer"; import { createChatStateStore } from "../../../../src/features/chat/application/state/store"; -import { - createChatReconnectActions, - type ChatReconnectActionsHost, -} from "../../../../src/features/chat/application/connection/reconnect-actions"; +import { reconnectPanel, type ChatReconnectActionsHost } from "../../../../src/features/chat/application/connection/reconnect-actions"; function createHost(overrides: Partial = {}) { const stateStore = createChatStateStore(createChatState()); @@ -36,12 +33,11 @@ function createHost(overrides: Partial = {}) { return { host, stateStore }; } -describe("createChatReconnectActions", () => { +describe("reconnectPanel", () => { it("resets local connection state before reconnecting and resumes the active thread", async () => { const { host, stateStore } = createHost(); - const controller = createChatReconnectActions(host); - await controller.reconnectPanel(); + await reconnectPanel(host); expect(stateStore.getState().ui.toolbarPanel).toBeNull(); expect(host.invalidateConnectionWork).toHaveBeenCalledOnce(); @@ -57,9 +53,8 @@ describe("createChatReconnectActions", () => { const { host } = createHost({ resumeThread: vi.fn().mockRejectedValue(new Error("resume failed")), }); - const controller = createChatReconnectActions(host); - await controller.reconnectPanel(); + await reconnectPanel(host); expect(host.addSystemMessage).toHaveBeenCalledWith("resume failed"); }); diff --git a/tests/features/chat/conversation/turns/composer-submit-actions.test.ts b/tests/features/chat/conversation/turns/composer-submit-actions.test.ts index 2b8bc950..fddfe95f 100644 --- a/tests/features/chat/conversation/turns/composer-submit-actions.test.ts +++ b/tests/features/chat/conversation/turns/composer-submit-actions.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest"; import type { AppServerClient } from "../../../../../src/app-server/connection/client"; import { createChatState } from "../../../../../src/features/chat/application/state/root-reducer"; import { createChatStateStore } from "../../../../../src/features/chat/application/state/store"; -import { createComposerSubmitActions } from "../../../../../src/features/chat/application/conversation/composer-submit-actions"; +import { submitComposer } from "../../../../../src/features/chat/application/conversation/composer-submit-actions"; import type { Thread } from "../../../../../src/domain/threads/model"; function thread(id: string): Thread { @@ -17,7 +17,7 @@ function thread(id: string): Thread { }; } -function createController(draft: string) { +function createHost(draft: string) { const stateStore = createChatStateStore(createChatState()); const interruptTurn = vi.fn().mockResolvedValue({}); const client = { interruptTurn } as unknown as AppServerClient; @@ -25,7 +25,7 @@ function createController(draft: string) { const sendTurnText = vi.fn().mockResolvedValue(undefined); const execute = vi.fn().mockResolvedValue(undefined); const followBottom = vi.fn(); - const controller = createComposerSubmitActions({ + const host = { stateStore, composer: { get trimmedDraft() { @@ -44,15 +44,15 @@ function createController(draft: string) { addSystemMessage: vi.fn(), }, scroll: { followBottom }, - }); - return { controller, execute, followBottom, interruptTurn, sendTurnText, setDraft, stateStore }; + }; + return { host, execute, followBottom, interruptTurn, sendTurnText, setDraft, stateStore }; } -describe("createComposerSubmitActions", () => { +describe("submitComposer", () => { it("sends plain drafts as turn text", async () => { - const { controller, followBottom, sendTurnText } = createController("hello"); + const { host, followBottom, sendTurnText } = createHost("hello"); - await controller.submit(); + await submitComposer(host); expect(followBottom).toHaveBeenCalledOnce(); expect(sendTurnText).toHaveBeenCalledWith("hello"); @@ -65,10 +65,10 @@ describe("createComposerSubmitActions", () => { }); it("executes slash commands and forwards command send results", async () => { - const { controller, execute, followBottom, sendTurnText, setDraft } = createController("/clear hello"); + const { host, execute, followBottom, sendTurnText, setDraft } = createHost("/clear hello"); execute.mockResolvedValue({ sendText: "hello" }); - await controller.submit(); + await submitComposer(host); expect(setDraft).toHaveBeenCalledWith("", { clearSuggestions: true }); expect(execute).toHaveBeenCalledWith("clear", "hello"); @@ -77,10 +77,10 @@ describe("createComposerSubmitActions", () => { }); it("restores slash command composer drafts from command results", async () => { - const { controller, execute, followBottom, sendTurnText, setDraft } = createController("/goal edit"); + const { host, execute, followBottom, sendTurnText, setDraft } = createHost("/goal edit"); execute.mockResolvedValue({ composerDraft: "/goal set Current objective" }); - await controller.submit(); + await submitComposer(host); expect(setDraft).toHaveBeenCalledWith("", { clearSuggestions: true }); expect(setDraft).toHaveBeenCalledWith("/goal set Current objective", { focus: true, clearSuggestions: true }); @@ -89,7 +89,7 @@ describe("createComposerSubmitActions", () => { }); it("interrupts a running turn when submitting an empty draft", async () => { - const { controller, followBottom, interruptTurn, stateStore } = createController(""); + const { host, followBottom, interruptTurn, stateStore } = createHost(""); stateStore.dispatch({ type: "active-thread/resumed", thread: thread("thread"), @@ -103,7 +103,7 @@ describe("createComposerSubmitActions", () => { }); stateStore.dispatch({ type: "turn/started", threadId: "thread", turnId: "turn" }); - await controller.submit(); + await submitComposer(host); expect(followBottom).not.toHaveBeenCalled(); expect(interruptTurn).toHaveBeenCalledWith("thread", "turn"); diff --git a/tests/features/chat/conversation/turns/plan-implementation.test.ts b/tests/features/chat/conversation/turns/plan-implementation.test.ts index f6e128d9..07816426 100644 --- a/tests/features/chat/conversation/turns/plan-implementation.test.ts +++ b/tests/features/chat/conversation/turns/plan-implementation.test.ts @@ -4,10 +4,7 @@ import type { AppServerClient } from "../../../../../src/app-server/connection/c import { createChatState } from "../../../../../src/features/chat/application/state/root-reducer"; import { createChatStateStore, type ChatStateStore } from "../../../../../src/features/chat/application/state/store"; import { implementPlanCandidateFromState } from "../../../../../src/features/chat/application/state/selectors"; -import { - createPlanImplementation, - type PlanImplementationHost, -} from "../../../../../src/features/chat/application/conversation/plan-implementation"; +import { implementPlan, type PlanImplementationHost } from "../../../../../src/features/chat/application/conversation/plan-implementation"; import type { MessageStreamItem } from "../../../../../src/features/chat/domain/message-stream/items"; const planItem = (id: string): MessageStreamItem => ({ @@ -59,15 +56,15 @@ function createController({ client = {} as AppServerClient } = {}) { requestDefaultCollaborationModeForNextTurn, }; return { - controller: createPlanImplementation(host), ensureConnected, + host, requestDefaultCollaborationModeForNextTurn, sendTurnText, stateStore, }; } -describe("createPlanImplementation", () => { +describe("implementPlan", () => { it("finds the latest proposed plan only when the thread is idle and in plan mode", () => { const stateStore = createChatStateStore(createChatState()); const first = planItem("first"); @@ -91,12 +88,12 @@ describe("createPlanImplementation", () => { }); it("switches out of plan mode and submits the implementation prompt", async () => { - const { controller, ensureConnected, requestDefaultCollaborationModeForNextTurn, sendTurnText, stateStore } = createController(); + const { host, ensureConnected, requestDefaultCollaborationModeForNextTurn, sendTurnText, stateStore } = createController(); const plan = planItem("plan"); resumeThread(stateStore, [plan]); stateStore.dispatch({ type: "ui/panel-set", panel: "status-panel" }); - await controller.implement(plan); + await implementPlan(host, plan); expect(ensureConnected).toHaveBeenCalledOnce(); expect(requestDefaultCollaborationModeForNextTurn).toHaveBeenCalledOnce(); @@ -106,12 +103,12 @@ describe("createPlanImplementation", () => { }); it("ignores stale plan items", async () => { - const { controller, ensureConnected, sendTurnText, stateStore } = createController(); + const { host, ensureConnected, sendTurnText, stateStore } = createController(); const first = planItem("first"); const latest = planItem("latest"); resumeThread(stateStore, [first, latest]); - await controller.implement(first); + await implementPlan(host, first); expect(ensureConnected).not.toHaveBeenCalled(); expect(sendTurnText).not.toHaveBeenCalled(); diff --git a/tests/features/chat/conversation/turns/slash-command-handler.test.ts b/tests/features/chat/conversation/turns/slash-command-handler.test.ts index 9587ef91..9188a79c 100644 --- a/tests/features/chat/conversation/turns/slash-command-handler.test.ts +++ b/tests/features/chat/conversation/turns/slash-command-handler.test.ts @@ -6,7 +6,7 @@ import type { TurnItem, TurnRecord } from "../../../../../src/app-server/protoco import { createChatState } from "../../../../../src/features/chat/application/state/root-reducer"; import { createChatStateStore } from "../../../../../src/features/chat/application/state/store"; import { - createSlashCommandHandler, + executeSlashCommandWithState, type SlashCommandHandlerHost, } from "../../../../../src/features/chat/application/conversation/slash-command-handler"; import type { Thread } from "../../../../../src/domain/threads/model"; @@ -68,12 +68,11 @@ function createHost(overrides: SlashCommandHostOverrides = {}) { return { compactThread, host, stateStore, threadTurnsList }; } -describe("createSlashCommandHandler", () => { +describe("executeSlashCommandWithState", () => { it("executes slash commands against the current chat state", async () => { const { host } = createHost(); - const controller = createSlashCommandHandler(host); - const result = await controller.execute("clear", ""); + const result = await executeSlashCommandWithState(host, "clear", ""); expect(host.startNewThread).toHaveBeenCalledOnce(); expect(result).toBeUndefined(); @@ -96,9 +95,8 @@ describe("createSlashCommandHandler", () => { approvalsReviewer: null, activePermissionProfile: null, }); - const controller = createSlashCommandHandler(host); - await controller.execute("compact", ""); + await executeSlashCommandWithState(host, "compact", ""); expect(compactThread).toHaveBeenCalledWith("thread"); }); @@ -116,18 +114,16 @@ describe("createSlashCommandHandler", () => { approvalsReviewer: null, activePermissionProfile: null, }); - const controller = createSlashCommandHandler(host); - await controller.execute("compact", ""); + await executeSlashCommandWithState(host, "compact", ""); expect(compactThread).toHaveBeenCalledWith("thread"); }); it("starts an empty panel before setting a slash command goal", async () => { const { host } = createHost(); - const controller = createSlashCommandHandler(host); - await controller.execute("goal", "set Ship this"); + await executeSlashCommandWithState(host, "goal", "set Ship this"); expect(host.startThreadForGoal).toHaveBeenCalledWith("Ship this"); expect(host.setGoalObjective).toHaveBeenCalledWith("thread-new", "Ship this", null); @@ -135,9 +131,8 @@ describe("createSlashCommandHandler", () => { it("runs reconnect even when there is no current app-server client", async () => { const { host } = createHost({ currentClient: () => null }); - const controller = createSlashCommandHandler(host); - await controller.execute("reconnect", ""); + await executeSlashCommandWithState(host, "reconnect", ""); expect(host.reconnect).toHaveBeenCalledOnce(); }); @@ -148,9 +143,8 @@ describe("createSlashCommandHandler", () => { type: "thread-list/applied", threads: [thread("other", "Other")], }); - const controller = createSlashCommandHandler(host); - const result = await controller.execute("refer", "Other summarize"); + const result = await executeSlashCommandWithState(host, "refer", "Other summarize"); expect(threadTurnsList).toHaveBeenCalledWith("other", null, 20); expect(result).toBeUndefined(); @@ -167,9 +161,8 @@ describe("createSlashCommandHandler", () => { data: [turn([userMessage("u1", "元の依頼"), agentMessage("a1", "回答")])], nextCursor: null, }); - const controller = createSlashCommandHandler(host); - const result = await controller.execute("refer", "Other summarize"); + const result = await executeSlashCommandWithState(host, "refer", "Other summarize"); expect(result?.sendText).toBe("summarize"); expect(host.setStatus).toHaveBeenCalledWith("Referencing 019abcde (1/20 turns)."); diff --git a/tests/features/chat/panel/toolbar-actions.test.ts b/tests/features/chat/panel/toolbar-actions.test.ts index 404b4a30..cafd7f4b 100644 --- a/tests/features/chat/panel/toolbar-actions.test.ts +++ b/tests/features/chat/panel/toolbar-actions.test.ts @@ -113,7 +113,7 @@ function toolbarActionDeps(): Parameters[1 connectionController: { refreshStatusPanel: vi.fn() } as unknown as Parameters< typeof createChatPanelToolbarActions >[1]["connectionController"], - reconnectActions: { reconnectPanel: vi.fn() } as Parameters[1]["reconnectActions"], + reconnectPanel: vi.fn(), inboundController: { addSystemMessage: vi.fn() } as unknown as Parameters[1]["inboundController"], threadActions: { archiveThread: vi.fn().mockResolvedValue(undefined), diff --git a/tests/features/chat/threads/thread-management-actions.test.ts b/tests/features/chat/threads/thread-management-actions.test.ts index ed388866..0b79c6b2 100644 --- a/tests/features/chat/threads/thread-management-actions.test.ts +++ b/tests/features/chat/threads/thread-management-actions.test.ts @@ -406,39 +406,7 @@ function clientMock() { } function threadManagementActions(host: ThreadManagementActionsHost): ThreadManagementActions { - return createThreadManagementActions({ - obsidian: { archiveAdapter: host.archiveAdapter }, - settingsRef: { - get settings() { - return host.settings(); - }, - get vaultPath() { - return host.vaultPath; - }, - }, - workspace: { openThreadInNewView: host.openThreadInNewView }, - threadSurfaces: { - notifyThreadArchived: host.notifyThreadArchived, - notifyThreadRenamed: host.notifyThreadRenamed, - refreshSharedThreadListFromOpenSurface: host.refreshSharedThreadListFromOpenSurface, - }, - stateStore: host.stateStore, - client: { - currentClient: host.currentClient, - ensureConnected: host.ensureConnected, - }, - status: { - set: host.setStatus, - addSystemMessage: host.addSystemMessage, - }, - notify: { showNotice: host.showNotice }, - thread: { - selectThread: host.openThreadInCurrentPanel, - refreshThreads: host.refreshThreads, - notifyIdentityChanged: host.notifyActiveThreadIdentityChanged, - }, - composer: { setText: host.setComposerText }, - }); + return createThreadManagementActions(host); } function hostMock({