diff --git a/src/features/chat/chat-message-renderer.ts b/src/features/chat/chat-message-renderer.ts index f1f2b47d..b113cf0c 100644 --- a/src/features/chat/chat-message-renderer.ts +++ b/src/features/chat/chat-message-renderer.ts @@ -9,7 +9,7 @@ import type { ChatTurnDiffViewState } from "./ui/turn-diff"; import { MarkdownMessageRenderer } from "./markdown-message-renderer"; import { isRollbackCandidateItem, rollbackCandidateFromItems } from "./rollback"; import { chatTurnBusy, type ChatAction, type ChatState, type ChatStateStore } from "./chat-state"; -import { implementPlanCandidateFromState } from "./plan-implementation-controller"; +import { implementPlanCandidateFromState } from "./plan-implementation"; import { unmountReactRoot } from "../../shared/ui/react-root"; export interface ChatMessageRendererOptions { @@ -17,7 +17,7 @@ export interface ChatMessageRendererOptions { owner: Component; stateStore: ChatStateStore; vaultPath: string; - consumeScrollIntent: () => ChatMessageScrollIntent; + consumeScrollIntent: () => MessageScrollIntent; loadOlderTurns: () => void; rollbackThread: (threadId: string) => void; implementPlan: (item: DisplayItem) => void; @@ -26,8 +26,6 @@ export interface ChatMessageRendererOptions { renderPendingRequests: () => ReactNode; } -export type ChatMessageScrollIntent = MessageScrollIntent; - export class ChatMessageRenderer { private messagesEl: HTMLElement | null = null; private readonly scrollController: MessageScrollController; diff --git a/src/features/chat/app-server-warmup-controller.ts b/src/features/chat/controllers/connection/app-server-warmup-controller.ts similarity index 90% rename from src/features/chat/app-server-warmup-controller.ts rename to src/features/chat/controllers/connection/app-server-warmup-controller.ts index c2a7ed27..0b6b1e50 100644 --- a/src/features/chat/app-server-warmup-controller.ts +++ b/src/features/chat/controllers/connection/app-server-warmup-controller.ts @@ -1,4 +1,4 @@ -import type { ChatViewDeferredTasks } from "./view-lifecycle"; +import type { ChatViewDeferredTasks } from "../../view-lifecycle"; export interface AppServerWarmupControllerHost { deferredTasks: ChatViewDeferredTasks; diff --git a/src/features/chat/connection-controller.ts b/src/features/chat/controllers/connection/connection-controller.ts similarity index 91% rename from src/features/chat/connection-controller.ts rename to src/features/chat/controllers/connection/connection-controller.ts index 269613ce..84816787 100644 --- a/src/features/chat/connection-controller.ts +++ b/src/features/chat/controllers/connection/connection-controller.ts @@ -1,9 +1,9 @@ -import { StaleConnectionError } from "../../app-server/connection-manager"; -import type { AppServerClient } from "../../app-server/client"; -import type { InitializeResponse } from "../../generated/app-server/InitializeResponse"; -import type { ChatAction, ChatStateStore } from "./chat-state"; -import type { ChatAppServerController } from "./chat-app-server-controller"; -import type { ChatConnectionWorkTracker, ActiveChatConnection } from "./view-lifecycle"; +import { StaleConnectionError } from "../../../../app-server/connection-manager"; +import type { AppServerClient } from "../../../../app-server/client"; +import type { InitializeResponse } from "../../../../generated/app-server/InitializeResponse"; +import type { ChatAction, ChatStateStore } from "../../chat-state"; +import type { ChatAppServerController } from "../../chat-app-server-controller"; +import type { ChatConnectionWorkTracker, ActiveChatConnection } from "../../view-lifecycle"; export interface ChatConnectionAdapter { connect(): Promise; diff --git a/src/features/chat/reconnect-controller.ts b/src/features/chat/controllers/connection/reconnect-controller.ts similarity index 95% rename from src/features/chat/reconnect-controller.ts rename to src/features/chat/controllers/connection/reconnect-controller.ts index 8f4e0438..5cda62d2 100644 --- a/src/features/chat/reconnect-controller.ts +++ b/src/features/chat/controllers/connection/reconnect-controller.ts @@ -1,4 +1,4 @@ -import type { ChatAction, ChatStateStore } from "./chat-state"; +import type { ChatAction, ChatStateStore } from "../../chat-state"; export interface ChatReconnectControllerHost { stateStore: ChatStateStore; diff --git a/src/features/chat/pending-request-controller.ts b/src/features/chat/controllers/requests/pending-request-controller.ts similarity index 84% rename from src/features/chat/pending-request-controller.ts rename to src/features/chat/controllers/requests/pending-request-controller.ts index 1cad1c48..79d608bf 100644 --- a/src/features/chat/pending-request-controller.ts +++ b/src/features/chat/controllers/requests/pending-request-controller.ts @@ -1,11 +1,12 @@ import type { ReactNode } from "react"; -import type { ApprovalAction, PendingApproval } from "./approvals/model"; -import type { ChatAction, ChatState, ChatStateStore } from "./chat-state"; -import type { ChatController } from "./chat-controller"; -import { pendingRequestFocusSignature, userInputDraftKey, userInputOtherDraftKey } from "./request-state"; -import { pendingRequestMessageNode } from "./ui/pending-request-message"; -import { answersForPendingUserInput, type PendingUserInput } from "./user-input/model"; +import type { ApprovalAction, PendingApproval } from "../../approvals/model"; +import type { ChatAction, ChatState, ChatStateStore } from "../../chat-state"; +import type { ChatController } from "../../chat-controller"; +import { pendingRequestFocusSignature } from "../../request-state"; +import { pendingRequestMessageNode } from "../../ui/pending-request-message"; +import { userInputDraftKey, userInputOtherDraftKey } from "../../user-input/drafts"; +import { answersForPendingUserInput, type PendingUserInput } from "../../user-input/model"; export interface PendingRequestControllerHost { stateStore: ChatStateStore; diff --git a/src/features/chat/server-request-responder.ts b/src/features/chat/controllers/requests/server-request-responder.ts similarity index 92% rename from src/features/chat/server-request-responder.ts rename to src/features/chat/controllers/requests/server-request-responder.ts index f8f95dba..2471975d 100644 --- a/src/features/chat/server-request-responder.ts +++ b/src/features/chat/controllers/requests/server-request-responder.ts @@ -1,4 +1,4 @@ -import type { AppServerClient } from "../../app-server/client"; +import type { AppServerClient } from "../../../../app-server/client"; type RespondRequestId = Parameters[0]; type RejectRequestId = Parameters[0]; diff --git a/src/features/chat/composer-submission-controller.ts b/src/features/chat/controllers/submission/composer-submission-controller.ts similarity index 90% rename from src/features/chat/composer-submission-controller.ts rename to src/features/chat/controllers/submission/composer-submission-controller.ts index f13e9f6c..ef82aa38 100644 --- a/src/features/chat/composer-submission-controller.ts +++ b/src/features/chat/controllers/submission/composer-submission-controller.ts @@ -1,7 +1,7 @@ -import type { AppServerClient } from "../../app-server/client"; -import { parseSlashCommand } from "./composer/suggestions"; -import { activeTurnId, chatTurnBusy, type ChatStateStore } from "./chat-state"; -import type { ChatComposerController } from "./chat-composer-controller"; +import type { AppServerClient } from "../../../../app-server/client"; +import { parseSlashCommand } from "../../composer/suggestions"; +import { activeTurnId, chatTurnBusy, type ChatStateStore } from "../../chat-state"; +import type { ChatComposerController } from "../../chat-composer-controller"; import type { SlashCommandController } from "./slash-command-controller"; import type { TurnSubmissionController } from "./turn-submission-controller"; diff --git a/src/features/chat/plan-implementation-controller.ts b/src/features/chat/controllers/submission/plan-implementation-controller.ts similarity index 58% rename from src/features/chat/plan-implementation-controller.ts rename to src/features/chat/controllers/submission/plan-implementation-controller.ts index db7f09cc..4724bd26 100644 --- a/src/features/chat/plan-implementation-controller.ts +++ b/src/features/chat/controllers/submission/plan-implementation-controller.ts @@ -1,6 +1,7 @@ -import type { AppServerClient } from "../../app-server/client"; -import { chatTurnBusy, type ChatState, type ChatStateStore } from "./chat-state"; -import type { DisplayItem } from "./display/types"; +import type { AppServerClient } from "../../../../app-server/client"; +import type { ChatState, ChatStateStore } from "../../chat-state"; +import type { DisplayItem } from "../../display/types"; +import { implementPlanCandidateFromState } from "../../plan-implementation"; const IMPLEMENT_PLAN_PROMPT = "Please implement this plan."; @@ -32,20 +33,3 @@ export class PlanImplementationController { return this.host.stateStore.getState(); } } - -export function implementPlanCandidateFromState( - state: Pick, -): DisplayItem | null { - if ( - !state.activeThreadId || - chatTurnBusy(state) || - state.composerDraft.trim().length > 0 || - state.requestedCollaborationMode !== "plan" - ) { - return null; - } - return ( - [...state.displayItems].reverse().find((item) => item.kind === "message" && item.role === "assistant" && item.proposedPlan === true) ?? - null - ); -} diff --git a/src/features/chat/slash-command-controller.ts b/src/features/chat/controllers/submission/slash-command-controller.ts similarity index 86% rename from src/features/chat/slash-command-controller.ts rename to src/features/chat/controllers/submission/slash-command-controller.ts index 1fdb10c7..db4ccbc8 100644 --- a/src/features/chat/slash-command-controller.ts +++ b/src/features/chat/controllers/submission/slash-command-controller.ts @@ -1,16 +1,16 @@ -import type { AppServerClient } from "../../app-server/client"; +import type { AppServerClient } from "../../../../app-server/client"; import { referencedThreadInput as buildReferencedThreadInput, referencedThreadTurns, REFERENCED_THREAD_TURN_LIMIT, -} from "../../domain/threads/reference"; -import type { Thread } from "../../generated/app-server/v2/Thread"; -import { chatTurnBusy, type ChatStateStore } from "./chat-state"; -import { executeSlashCommand as runSlashCommand, type SlashCommandExecutionResult, type ThreadReferenceInput } from "./slash-commands"; -import type { SlashCommandName } from "./composer/slash-commands"; -import type { DisplayDetailSection } from "./display/types"; -import type { ReasoningEffort } from "../../generated/app-server/ReasoningEffort"; -import type { UserInput } from "../../generated/app-server/v2/UserInput"; +} from "../../../../domain/threads/reference"; +import type { Thread } from "../../../../generated/app-server/v2/Thread"; +import { chatTurnBusy, type ChatStateStore } from "../../chat-state"; +import { executeSlashCommand as runSlashCommand, type SlashCommandExecutionResult, type ThreadReferenceInput } from "../../slash-commands"; +import type { SlashCommandName } from "../../composer/slash-commands"; +import type { DisplayDetailSection } from "../../display/types"; +import type { ReasoningEffort } from "../../../../generated/app-server/ReasoningEffort"; +import type { UserInput } from "../../../../generated/app-server/v2/UserInput"; export interface SlashCommandControllerHost { stateStore: ChatStateStore; diff --git a/src/features/chat/turn-submission-controller.ts b/src/features/chat/controllers/submission/turn-submission-controller.ts similarity index 95% rename from src/features/chat/turn-submission-controller.ts rename to src/features/chat/controllers/submission/turn-submission-controller.ts index 7d79ef4d..f8be5401 100644 --- a/src/features/chat/turn-submission-controller.ts +++ b/src/features/chat/controllers/submission/turn-submission-controller.ts @@ -1,7 +1,7 @@ -import type { AppServerClient } from "../../app-server/client"; -import type { UserInput } from "../../generated/app-server/v2/UserInput"; -import { activeTurnId, chatTurnBusy, pendingTurnStart, type ChatStateStore } from "./chat-state"; -import type { ReferencedThreadDisplay } from "../../domain/threads/reference"; +import type { AppServerClient } from "../../../../app-server/client"; +import type { UserInput } from "../../../../generated/app-server/v2/UserInput"; +import { activeTurnId, chatTurnBusy, pendingTurnStart, type ChatStateStore } from "../../chat-state"; +import type { ReferencedThreadDisplay } from "../../../../domain/threads/reference"; import { acknowledgeOptimisticTurnStart, cleanupFailedTurnStart, diff --git a/src/features/chat/turn-submission.ts b/src/features/chat/controllers/submission/turn-submission.ts similarity index 92% rename from src/features/chat/turn-submission.ts rename to src/features/chat/controllers/submission/turn-submission.ts index 8eef0b99..2f72f602 100644 --- a/src/features/chat/turn-submission.ts +++ b/src/features/chat/controllers/submission/turn-submission.ts @@ -1,8 +1,8 @@ -import type { PendingTurnStart } from "./chat-state"; -import type { DisplayFileMention, DisplayItem, MessageDisplayItem } from "./display/types"; -import { fileMentionsFromInput } from "./display/thread-items"; -import { attachHookRunsToTurn } from "./hook-display"; -import type { UserInput } from "../../generated/app-server/v2/UserInput"; +import type { PendingTurnStart } from "../../chat-state"; +import type { DisplayFileMention, DisplayItem, MessageDisplayItem } from "../../display/types"; +import { fileMentionsFromInput } from "../../display/thread-items"; +import { attachHookRunsToTurn } from "../../hook-display"; +import type { UserInput } from "../../../../generated/app-server/v2/UserInput"; export interface LocalUserMessageParams { id: string; diff --git a/src/features/chat/restored-thread-controller.ts b/src/features/chat/controllers/thread/restored-thread-controller.ts similarity index 95% rename from src/features/chat/restored-thread-controller.ts rename to src/features/chat/controllers/thread/restored-thread-controller.ts index da7533fc..21a735d6 100644 --- a/src/features/chat/restored-thread-controller.ts +++ b/src/features/chat/controllers/thread/restored-thread-controller.ts @@ -1,12 +1,12 @@ -import type { ChatAction } from "./chat-state"; -import type { DisplayItem } from "./display/types"; +import type { ChatAction } from "../../chat-state"; +import type { DisplayItem } from "../../display/types"; import { transitionRestoredThreadLifecycle, type RestoredThreadLifecycleState, type RestoredThreadPlaceholderState, type RestoredThreadState, type ChatViewDeferredTasks, -} from "./view-lifecycle"; +} from "../../view-lifecycle"; export interface RestoredThreadControllerHost { deferredTasks: ChatViewDeferredTasks; diff --git a/src/features/chat/thread-identity-controller.ts b/src/features/chat/controllers/thread/thread-identity-controller.ts similarity index 97% rename from src/features/chat/thread-identity-controller.ts rename to src/features/chat/controllers/thread/thread-identity-controller.ts index a7efb8ca..33f5713e 100644 --- a/src/features/chat/thread-identity-controller.ts +++ b/src/features/chat/controllers/thread/thread-identity-controller.ts @@ -1,4 +1,4 @@ -import type { ChatStateStore } from "./chat-state"; +import type { ChatStateStore } from "../../chat-state"; import type { RestoredThreadController } from "./restored-thread-controller"; export interface ThreadIdentityControllerHost { diff --git a/src/features/chat/thread-resume-controller.ts b/src/features/chat/controllers/thread/thread-resume-controller.ts similarity index 87% rename from src/features/chat/thread-resume-controller.ts rename to src/features/chat/controllers/thread/thread-resume-controller.ts index 4ff1ed94..45a9195d 100644 --- a/src/features/chat/thread-resume-controller.ts +++ b/src/features/chat/controllers/thread/thread-resume-controller.ts @@ -1,11 +1,11 @@ -import type { AppServerClient } from "../../app-server/client"; -import type { ChatStateStore } from "./chat-state"; -import { chatTurnBusy } from "./chat-state"; -import type { DisplayItem } from "./display/types"; +import type { AppServerClient } from "../../../../app-server/client"; +import type { ChatStateStore } from "../../chat-state"; +import { chatTurnBusy } from "../../chat-state"; +import type { DisplayItem } from "../../display/types"; import type { RestoredThreadController } from "./restored-thread-controller"; -import { resumedThreadAction, type ThreadActivationResponse } from "./thread-resume"; -import type { ThreadHistoryLoader } from "./thread-history"; -import type { ChatResumeWorkTracker, ActiveChatResume } from "./view-lifecycle"; +import { resumedThreadAction, type ThreadActivationResponse } from "../../thread-resume"; +import type { ThreadHistoryLoader } from "../../thread-history"; +import type { ChatResumeWorkTracker, ActiveChatResume } from "../../view-lifecycle"; export interface ThreadResumeControllerHost { stateStore: ChatStateStore; diff --git a/src/features/chat/thread-selection-controller.ts b/src/features/chat/controllers/thread/thread-selection-controller.ts similarity index 97% rename from src/features/chat/thread-selection-controller.ts rename to src/features/chat/controllers/thread/thread-selection-controller.ts index 677de63a..c828795e 100644 --- a/src/features/chat/thread-selection-controller.ts +++ b/src/features/chat/controllers/thread/thread-selection-controller.ts @@ -1,4 +1,4 @@ -import { chatTurnBusy, type ChatAction, type ChatState, type ChatStateStore } from "./chat-state"; +import { chatTurnBusy, type ChatAction, type ChatState, type ChatStateStore } from "../../chat-state"; export interface ThreadSelectionControllerHost { stateStore: ChatStateStore; diff --git a/src/features/chat/message-scroll-controller.ts b/src/features/chat/controllers/view/message-scroll-controller.ts similarity index 73% rename from src/features/chat/message-scroll-controller.ts rename to src/features/chat/controllers/view/message-scroll-controller.ts index 478b2c20..5329d770 100644 --- a/src/features/chat/message-scroll-controller.ts +++ b/src/features/chat/controllers/view/message-scroll-controller.ts @@ -1,5 +1,5 @@ -import type { ChatStateStore } from "./chat-state"; -import type { ChatMessageScrollIntent } from "./chat-message-renderer"; +import type { ChatStateStore } from "../../chat-state"; +import type { MessageScrollIntent } from "../../ui/scroll"; export interface ChatMessageScrollControllerHost { stateStore: ChatStateStore; @@ -7,11 +7,11 @@ export interface ChatMessageScrollControllerHost { } export class ChatMessageScrollController { - private nextIntent: ChatMessageScrollIntent = "auto"; + private nextIntent: MessageScrollIntent = "auto"; constructor(private readonly host: ChatMessageScrollControllerHost) {} - consumeIntent(): ChatMessageScrollIntent { + consumeIntent(): MessageScrollIntent { const value = this.nextIntent; this.nextIntent = "auto"; return value; diff --git a/src/features/chat/view-open-close-controller.ts b/src/features/chat/controllers/view/view-open-close-controller.ts similarity index 95% rename from src/features/chat/view-open-close-controller.ts rename to src/features/chat/controllers/view/view-open-close-controller.ts index 7be371c7..880c092a 100644 --- a/src/features/chat/view-open-close-controller.ts +++ b/src/features/chat/controllers/view/view-open-close-controller.ts @@ -1,7 +1,7 @@ import type { EventRef, WorkspaceLeaf } from "obsidian"; -import { unmountReactRoot } from "../../shared/ui/react-root"; -import { unmountChatPanelShell } from "./ui/shell"; +import { unmountReactRoot } from "../../../../shared/ui/react-root"; +import { unmountChatPanelShell } from "../../ui/shell"; export interface ChatViewOpenCloseControllerHost { setOpened: (opened: boolean) => void; diff --git a/src/features/chat/view-render-controller.ts b/src/features/chat/controllers/view/view-render-controller.ts similarity index 89% rename from src/features/chat/view-render-controller.ts rename to src/features/chat/controllers/view/view-render-controller.ts index bce0195f..a72b9829 100644 --- a/src/features/chat/view-render-controller.ts +++ b/src/features/chat/controllers/view/view-render-controller.ts @@ -1,7 +1,7 @@ -import type { ChatState, ChatStateStore } from "./chat-state"; -import type { ChatViewRenderScheduleOptions } from "./view-lifecycle"; -import { composerSlotSnapshot, messagesSlotSnapshot, toolbarSlotSnapshot } from "./view-snapshot"; -import { renderChatPanelShell } from "./ui/shell"; +import type { ChatState, ChatStateStore } from "../../chat-state"; +import type { ChatViewRenderScheduleOptions } from "../../view-lifecycle"; +import { composerSlotSnapshot, messagesSlotSnapshot, toolbarSlotSnapshot } from "../../view-snapshot"; +import { renderChatPanelShell } from "../../ui/shell"; export interface ChatViewRenderControllerHost { stateStore: ChatStateStore; diff --git a/src/features/chat/view-state-controller.ts b/src/features/chat/controllers/view/view-state-controller.ts similarity index 86% rename from src/features/chat/view-state-controller.ts rename to src/features/chat/controllers/view/view-state-controller.ts index 1d073a93..21188c1a 100644 --- a/src/features/chat/view-state-controller.ts +++ b/src/features/chat/controllers/view/view-state-controller.ts @@ -1,5 +1,5 @@ -import { parseRestoredThreadState } from "./view-snapshot"; -import type { RestoredThreadState } from "./view-lifecycle"; +import { parseRestoredThreadState } from "../../view-snapshot"; +import type { RestoredThreadState } from "../../view-lifecycle"; export interface ChatViewStateControllerHost { invalidateResumeWork: () => void; diff --git a/src/features/chat/markdown-message-renderer.ts b/src/features/chat/markdown-message-renderer.ts index 9d7ddf2b..f4243af3 100644 --- a/src/features/chat/markdown-message-renderer.ts +++ b/src/features/chat/markdown-message-renderer.ts @@ -2,7 +2,7 @@ import { MarkdownRenderer, Notice, type App, type Component } from "obsidian"; import { isAbsoluteFileHref, vaultFileLinkTarget, vaultRelativeFileLinkTarget } from "./markdown-file-links"; import { renderTextWithWikiLinks as renderInlineWikiLinks } from "../../shared/ui/dom"; -import { notifyMessageContentRendered } from "./ui/message-stream"; +import { notifyMessageContentRendered } from "./ui/message-content-events"; export interface MarkdownMessageRendererOptions { app: App; diff --git a/src/features/chat/plan-implementation.ts b/src/features/chat/plan-implementation.ts new file mode 100644 index 00000000..bd82ca30 --- /dev/null +++ b/src/features/chat/plan-implementation.ts @@ -0,0 +1,19 @@ +import { chatTurnBusy, type ChatState } from "./chat-state"; +import type { DisplayItem } from "./display/types"; + +export function implementPlanCandidateFromState( + state: Pick, +): DisplayItem | null { + if ( + !state.activeThreadId || + chatTurnBusy(state) || + state.composerDraft.trim().length > 0 || + state.requestedCollaborationMode !== "plan" + ) { + return null; + } + return ( + [...state.displayItems].reverse().find((item) => item.kind === "message" && item.role === "assistant" && item.proposedPlan === true) ?? + null + ); +} diff --git a/src/features/chat/request-state.ts b/src/features/chat/request-state.ts index 2624dfc4..42e47646 100644 --- a/src/features/chat/request-state.ts +++ b/src/features/chat/request-state.ts @@ -1,4 +1,3 @@ -import type { RequestId } from "../../generated/app-server/RequestId"; import { approvalActionKind, approvalDetails, @@ -9,16 +8,9 @@ import { } from "./approvals/model"; import type { DisplayDetailSection, DisplayItem } from "./display/types"; import type { PendingUserInput } from "./user-input/model"; +import { userInputDraftKey, userInputOtherDraftKey } from "./user-input/drafts"; import { definedProp } from "../../utils"; -export function userInputDraftKey(requestId: RequestId, questionId: string): string { - return `${String(requestId)}:${questionId}`; -} - -export function userInputOtherDraftKey(requestId: RequestId, questionId: string): string { - return `${String(requestId)}:${questionId}:other`; -} - export function pendingRequestsSignature( approvals: readonly PendingApproval[], inputs: readonly PendingUserInput[], diff --git a/src/features/chat/toolbar-model.ts b/src/features/chat/toolbar-model.ts new file mode 100644 index 00000000..3f4db177 --- /dev/null +++ b/src/features/chat/toolbar-model.ts @@ -0,0 +1,62 @@ +import type { EffectiveConfigSection, RateLimitSummary } from "../../runtime/view"; + +export type ToolbarPanelKind = "history" | "status" | "runtime"; +export type ToolbarStatusState = "offline" | "connected" | "running"; +export type ToolbarDiagnosticAlertLevel = "normal" | "warning" | "error"; + +export interface ToolbarChoice { + label: string; + selected?: boolean; + disabled?: boolean; + meta?: string; + onClick: () => void; +} + +export interface ToolbarThreadRow { + title: string; + threadId: string; + selected: boolean; + disabled: boolean; + canArchive: boolean; + archiveConfirm?: { active: boolean; defaultSaveMarkdown: boolean }; + rename: { + draft: string; + generating: boolean; + } | null; +} + +export interface ToolbarDiagnosticRow { + label: string; + value: string; + level?: "normal" | "warning" | "error"; +} + +export interface ToolbarDiagnosticSection { + title: string; + rows: ToolbarDiagnosticRow[]; +} + +export interface ToolbarViewModel { + connected: boolean; + status: string; + statusState: ToolbarStatusState; + historyOpen: boolean; + statusPanelOpen: boolean; + runtimeOpen: boolean; + planActive: boolean; + autoReviewActive: boolean; + fastActive: boolean; + runtimeSummary: string; + runtimeTitle: string; + runtimeEmphasized: boolean; + context: { level: "ok" | "warn" | "danger"; title: string; label: string; percent: number | null } | null; + rateLimit: RateLimitSummary | null; + configSections: EffectiveConfigSection[]; + openPanel: ToolbarPanelKind | null; + threads: ToolbarThreadRow[]; + modelChoices: ToolbarChoice[]; + effortChoices: ToolbarChoice[]; + connectLabel: string; + diagnostics: ToolbarDiagnosticSection[]; + diagnosticAlertLevel: ToolbarDiagnosticAlertLevel; +} diff --git a/src/features/chat/ui/message-content-events.ts b/src/features/chat/ui/message-content-events.ts new file mode 100644 index 00000000..f6dd7a84 --- /dev/null +++ b/src/features/chat/ui/message-content-events.ts @@ -0,0 +1,5 @@ +export const MESSAGE_CONTENT_RENDERED_EVENT = "codex-panel:message-content-rendered"; + +export function notifyMessageContentRendered(element: HTMLElement): void { + element.dispatchEvent(new Event(MESSAGE_CONTENT_RENDERED_EVENT)); +} diff --git a/src/features/chat/ui/message-stream.tsx b/src/features/chat/ui/message-stream.tsx index e9d158e5..fafc714f 100644 --- a/src/features/chat/ui/message-stream.tsx +++ b/src/features/chat/ui/message-stream.tsx @@ -6,13 +6,13 @@ import type { ToolResultDisplayItem } from "../display/tool-view"; import type { DisplayBlock, DisplayDetailSection, DisplayItem } from "../display/types"; import { activeTurnId, type ChatTurnLifecycleState } from "../chat-state"; import { IconButton } from "../../../shared/ui/react-components"; +import { MESSAGE_CONTENT_RENDERED_EVENT } from "./message-content-events"; import { toolResultNode } from "./tool-result"; import { activeAgentRunSummaryBlock, agentRunSummaryNode, workItemNode, type WorkItemDisplayItem } from "./work-items"; import type { ChatTurnDiffViewState } from "./turn-diff"; import { renderReactRoot } from "../../../shared/ui/react-root"; const USER_MESSAGE_COLLAPSE_HEIGHT_PX = 360; -const MESSAGE_CONTENT_RENDERED_EVENT = "codex-panel:message-content-rendered"; export interface MessageStreamBlock { key: string; @@ -579,10 +579,6 @@ function userMessageCollapseHeight(element: HTMLElement): number { return Math.min(USER_MESSAGE_COLLAPSE_HEIGHT_PX, viewportHeight * 0.45); } -export function notifyMessageContentRendered(element: HTMLElement): void { - element.dispatchEvent(new Event(MESSAGE_CONTENT_RENDERED_EVENT)); -} - function displayRoleLabel(item: DisplayItem): string { if (item.kind === "approvalResult") return "Approval"; if (item.kind === "userInputResult") return "Input"; diff --git a/src/features/chat/ui/toolbar.tsx b/src/features/chat/ui/toolbar.tsx index bd1ed018..8650b230 100644 --- a/src/features/chat/ui/toolbar.tsx +++ b/src/features/chat/ui/toolbar.tsx @@ -3,67 +3,7 @@ import { useLayoutEffect, useRef, type ButtonHTMLAttributes, type KeyboardEvent, import type { EffectiveConfigSection, RateLimitSummary } from "../../../runtime/view"; import { IconButton, ObsidianIcon } from "../../../shared/ui/react-components"; import { renderReactRoot } from "../../../shared/ui/react-root"; - -export type ToolbarPanelKind = "history" | "status" | "runtime"; -export type ToolbarStatusState = "offline" | "connected" | "running"; -export type ToolbarDiagnosticAlertLevel = "normal" | "warning" | "error"; - -export interface ToolbarChoice { - label: string; - selected?: boolean; - disabled?: boolean; - meta?: string; - onClick: () => void; -} - -export interface ToolbarThreadRow { - title: string; - threadId: string; - selected: boolean; - disabled: boolean; - canArchive: boolean; - archiveConfirm?: { active: boolean; defaultSaveMarkdown: boolean }; - rename: { - draft: string; - generating: boolean; - } | null; -} - -export interface ToolbarDiagnosticRow { - label: string; - value: string; - level?: "normal" | "warning" | "error"; -} - -export interface ToolbarDiagnosticSection { - title: string; - rows: ToolbarDiagnosticRow[]; -} - -export interface ToolbarViewModel { - connected: boolean; - status: string; - statusState: ToolbarStatusState; - historyOpen: boolean; - statusPanelOpen: boolean; - runtimeOpen: boolean; - planActive: boolean; - autoReviewActive: boolean; - fastActive: boolean; - runtimeSummary: string; - runtimeTitle: string; - runtimeEmphasized: boolean; - context: { level: "ok" | "warn" | "danger"; title: string; label: string; percent: number | null } | null; - rateLimit: RateLimitSummary | null; - configSections: EffectiveConfigSection[]; - openPanel: ToolbarPanelKind | null; - threads: ToolbarThreadRow[]; - modelChoices: ToolbarChoice[]; - effortChoices: ToolbarChoice[]; - connectLabel: string; - diagnostics: ToolbarDiagnosticSection[]; - diagnosticAlertLevel: ToolbarDiagnosticAlertLevel; -} +import type { ToolbarDiagnosticSection, ToolbarThreadRow, ToolbarViewModel } from "../toolbar-model"; export interface ToolbarActions { toggleHistory: () => void; diff --git a/src/features/chat/ui/work-items.tsx b/src/features/chat/ui/work-items.tsx index 82705c48..55671937 100644 --- a/src/features/chat/ui/work-items.tsx +++ b/src/features/chat/ui/work-items.tsx @@ -2,9 +2,16 @@ import { useLayoutEffect, useState, type ReactNode } from "react"; import { activeAgentRunSummary } from "../display/agent"; import { executionState } from "../display/state"; -import type { AgentDisplayItem, AgentRunSummary, AgentRunSummaryAgent, TaskProgressDisplayItem, ToolDisplayItem } from "../display/types"; +import type { + AgentDisplayItem, + AgentRunSummary, + AgentRunSummaryAgent, + DisplayItem, + TaskProgressDisplayItem, + ToolDisplayItem, +} from "../display/types"; import { agentActivityMetaLabel, agentMessagePreview, agentRunSummaryLabel, taskStatusMarker } from "../display/labels"; -import { messageStreamActiveTurnId, type MessageStreamContext } from "./message-stream"; +import { activeTurnId, type ChatTurnLifecycleState } from "../chat-state"; import { createWorkMessageClassName } from "./work-message"; import { shortThreadId } from "../../../utils"; @@ -13,15 +20,26 @@ const AGENT_ROW_MESSAGE_PREVIEW_LIMIT = 120; type ReasoningDisplayItem = ToolDisplayItem & { kind: "reasoning" }; export type WorkItemDisplayItem = TaskProgressDisplayItem | AgentDisplayItem | ReasoningDisplayItem; -export function activeAgentRunSummaryBlock(context: MessageStreamContext): AgentRunSummary | null { - return activeAgentRunSummary(context.displayItems, messageStreamActiveTurnId(context)); +export interface WorkItemContext { + turnLifecycle: ChatTurnLifecycleState; + displayItems: readonly DisplayItem[]; + openDetails: ReadonlySet; + onDetailsToggle?: (key: string, open: boolean) => void; +} + +export function workItemsActiveTurnId(context: Pick): string | null { + return activeTurnId(context); +} + +export function activeAgentRunSummaryBlock(context: WorkItemContext): AgentRunSummary | null { + return activeAgentRunSummary(context.displayItems, workItemsActiveTurnId(context)); } export function agentRunSummaryNode(summary: AgentRunSummary): ReactNode { return ; } -export function workItemNode(item: WorkItemDisplayItem, context: MessageStreamContext): ReactNode { +export function workItemNode(item: WorkItemDisplayItem, context: WorkItemContext): ReactNode { if (item.kind === "taskProgress") return ; if (item.kind === "agent") return ; return ; @@ -56,7 +74,7 @@ function TaskProgressItem({ item }: { item: TaskProgressDisplayItem }): ReactNod ); } -function AgentItem({ item, context }: { item: AgentDisplayItem; context: MessageStreamContext }): ReactNode { +function AgentItem({ item, context }: { item: AgentDisplayItem; context: WorkItemContext }): ReactNode { return (
{agentSummaryText(item)}
@@ -107,7 +125,7 @@ function AgentItem({ item, context }: { item: AgentDisplayItem; context: Message ); } -function ReasoningItem({ item, context }: { item: ReasoningDisplayItem; context: MessageStreamContext }): ReactNode { +function ReasoningItem({ item, context }: { item: ReasoningDisplayItem; context: WorkItemContext }): ReactNode { const active = isReasoningActive(item, context); return (
@@ -158,7 +176,7 @@ function RememberedDetails({ detailsClassName: string; detailsKey: string; summary: string; - context: MessageStreamContext; + context: WorkItemContext; children: ReactNode; }): ReactNode { const [open, setOpen] = useState(context.openDetails.has(detailsKey)); @@ -228,8 +246,8 @@ function isLongAgentMessage(message: string): boolean { return message.length > AGENT_ROW_MESSAGE_PREVIEW_LIMIT || message.includes("\n"); } -function isReasoningActive(item: ReasoningDisplayItem, context: MessageStreamContext): boolean { - const activeTurn = messageStreamActiveTurnId(context); +function isReasoningActive(item: ReasoningDisplayItem, context: WorkItemContext): boolean { + const activeTurn = workItemsActiveTurnId(context); if (!activeTurn || item.turnId !== activeTurn) return false; if (executionState(item) === "completed") return false; const latestActiveTurnItem = [...context.displayItems].reverse().find((candidate) => candidate.turnId === activeTurn); diff --git a/src/features/chat/user-input/drafts.ts b/src/features/chat/user-input/drafts.ts new file mode 100644 index 00000000..1d38e6e3 --- /dev/null +++ b/src/features/chat/user-input/drafts.ts @@ -0,0 +1,9 @@ +import type { RequestId } from "../../../generated/app-server/RequestId"; + +export function userInputDraftKey(requestId: RequestId, questionId: string): string { + return `${String(requestId)}:${questionId}`; +} + +export function userInputOtherDraftKey(requestId: RequestId, questionId: string): string { + return `${String(requestId)}:${questionId}:other`; +} diff --git a/src/features/chat/user-input/model.ts b/src/features/chat/user-input/model.ts index 5e9a0cfa..49a283ad 100644 --- a/src/features/chat/user-input/model.ts +++ b/src/features/chat/user-input/model.ts @@ -3,7 +3,7 @@ import type { ServerRequest } from "../../../generated/app-server/ServerRequest" import type { ToolRequestUserInputParams } from "../../../generated/app-server/v2/ToolRequestUserInputParams"; import type { ToolRequestUserInputQuestion } from "../../../generated/app-server/v2/ToolRequestUserInputQuestion"; import type { ToolRequestUserInputResponse } from "../../../generated/app-server/v2/ToolRequestUserInputResponse"; -import { userInputDraftKey } from "../request-state"; +import { userInputDraftKey } from "./drafts"; export type UserInputRequest = Extract; diff --git a/src/features/chat/view-model.ts b/src/features/chat/view-model.ts index d875e9b0..a74f5e6c 100644 --- a/src/features/chat/view-model.ts +++ b/src/features/chat/view-model.ts @@ -19,7 +19,7 @@ import { codexPanelDisplayTitle, explicitThreadName, getThreadTitle } from "../. import { connectionDiagnosticSections, diagnosticAlertLevel } from "./diagnostics"; import type { ChatState } from "./chat-state"; import { statusValue, usageLimitStatusLines } from "./status-lines"; -import type { ToolbarChoice, ToolbarThreadRow, ToolbarViewModel } from "./ui/toolbar"; +import type { ToolbarChoice, ToolbarThreadRow, ToolbarViewModel } from "./toolbar-model"; export interface RuntimeSnapshotInput { state: ChatState; diff --git a/src/features/chat/view.ts b/src/features/chat/view.ts index 4dae1f9b..4061890a 100644 --- a/src/features/chat/view.ts +++ b/src/features/chat/view.ts @@ -18,14 +18,15 @@ import { pendingRequestsSignature as requestStateSignature } from "./request-sta import type { CodexPanelSettings } from "../../settings/model"; import { ChatComposerController } from "./chat-composer-controller"; import { activeTurnId, chatTurnBusy, createChatStateStore, type ChatState, type ChatAction } from "./chat-state"; -import { renderToolbar, type ToolbarViewModel } from "./ui/toolbar"; +import { renderToolbar } from "./ui/toolbar"; +import type { ToolbarViewModel } from "./toolbar-model"; import type { ChatTurnDiffViewState } from "./ui/turn-diff"; import { ChatMessageRenderer } from "./chat-message-renderer"; import type { OpenCodexPanelSnapshot } from "../../runtime/open-panel-snapshot"; import type { SharedAppServerMetadata } from "../../runtime/shared-app-server-state"; import { ChatThreadActionController } from "./thread-actions"; import { ChatRuntimeSettingsController } from "./runtime-settings-controller"; -import { RestoredThreadController } from "./restored-thread-controller"; +import { RestoredThreadController } from "./controllers/thread/restored-thread-controller"; import { activeComposerThreadName as buildActiveComposerThreadName, activeThreadTitle as buildActiveThreadTitle, @@ -39,30 +40,30 @@ import { statusSummaryLines as buildStatusSummaryLines, toolbarViewModel as buildToolbarViewModel, } from "./view-model"; -import { composerSlotSnapshot, openPanelTurnLifecycle } from "./view-snapshot"; +import { openPanelTurnLifecycle } from "./view-snapshot"; import { ChatConnectionWorkTracker, ChatResumeWorkTracker, ChatViewDeferredTasks, type ChatViewRenderScheduleOptions, } from "./view-lifecycle"; -import { PendingRequestController } from "./pending-request-controller"; +import { ChatConnectionController } from "./controllers/connection/connection-controller"; +import { ChatReconnectController } from "./controllers/connection/reconnect-controller"; +import { AppServerWarmupController } from "./controllers/connection/app-server-warmup-controller"; +import { PendingRequestController } from "./controllers/requests/pending-request-controller"; +import { ServerRequestResponder } from "./controllers/requests/server-request-responder"; +import { ComposerSubmissionController } from "./controllers/submission/composer-submission-controller"; +import { PlanImplementationController } from "./controllers/submission/plan-implementation-controller"; +import { SlashCommandController } from "./controllers/submission/slash-command-controller"; +import { TurnSubmissionController } from "./controllers/submission/turn-submission-controller"; +import { ThreadIdentityController } from "./controllers/thread/thread-identity-controller"; +import { ThreadResumeController } from "./controllers/thread/thread-resume-controller"; +import { ThreadSelectionController } from "./controllers/thread/thread-selection-controller"; +import { ChatMessageScrollController } from "./controllers/view/message-scroll-controller"; +import { ChatViewOpenCloseController } from "./controllers/view/view-open-close-controller"; +import { ChatViewRenderController } from "./controllers/view/view-render-controller"; +import { ChatViewStateController } from "./controllers/view/view-state-controller"; import { ToolbarPanelController } from "./toolbar-panel-controller"; -import { ChatReconnectController } from "./reconnect-controller"; -import { ChatMessageScrollController } from "./message-scroll-controller"; -import { TurnSubmissionController } from "./turn-submission-controller"; -import { SlashCommandController } from "./slash-command-controller"; -import { ComposerSubmissionController } from "./composer-submission-controller"; -import { ChatConnectionController } from "./connection-controller"; -import { ThreadIdentityController } from "./thread-identity-controller"; -import { ThreadResumeController } from "./thread-resume-controller"; -import { ChatViewRenderController } from "./view-render-controller"; -import { ChatViewOpenCloseController } from "./view-open-close-controller"; -import { PlanImplementationController } from "./plan-implementation-controller"; -import { ThreadSelectionController } from "./thread-selection-controller"; -import { ChatViewStateController } from "./view-state-controller"; -import { AppServerWarmupController } from "./app-server-warmup-controller"; -import { ServerRequestResponder } from "./server-request-responder"; export interface CodexChatHost { readonly settings: CodexPanelSettings; @@ -959,8 +960,6 @@ export class CodexChatView extends ItemView { return buildActiveComposerThreadName(this.state, this.restoredThreadPlaceholder()); } - private readonly composerSnapshot = (state: ChatState) => composerSlotSnapshot(state, this.activeComposerThreadName()); - private render(options: ChatViewRenderScheduleOptions = {}): void { this.renderController.render(options); } diff --git a/tests/features/chat/app-server-warmup-controller.test.ts b/tests/features/chat/controllers/connection/app-server-warmup-controller.test.ts similarity index 88% rename from tests/features/chat/app-server-warmup-controller.test.ts rename to tests/features/chat/controllers/connection/app-server-warmup-controller.test.ts index b7edc039..a9617f4a 100644 --- a/tests/features/chat/app-server-warmup-controller.test.ts +++ b/tests/features/chat/controllers/connection/app-server-warmup-controller.test.ts @@ -2,8 +2,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { AppServerWarmupController } from "../../../src/features/chat/app-server-warmup-controller"; -import { ChatViewDeferredTasks } from "../../../src/features/chat/view-lifecycle"; +import { AppServerWarmupController } from "../../../../../src/features/chat/controllers/connection/app-server-warmup-controller"; +import { ChatViewDeferredTasks } from "../../../../../src/features/chat/view-lifecycle"; function createController({ opened = true, diff --git a/tests/features/chat/connection-controller.test.ts b/tests/features/chat/controllers/connection/connection-controller.test.ts similarity index 89% rename from tests/features/chat/connection-controller.test.ts rename to tests/features/chat/controllers/connection/connection-controller.test.ts index 54289b13..ccb667ad 100644 --- a/tests/features/chat/connection-controller.test.ts +++ b/tests/features/chat/controllers/connection/connection-controller.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it, vi } from "vitest"; -import type { AppServerClient } from "../../../src/app-server/client"; -import { createChatState, createChatStateStore } from "../../../src/features/chat/chat-state"; -import { ChatConnectionController, type ChatConnectionAdapter } from "../../../src/features/chat/connection-controller"; -import { ChatConnectionWorkTracker } from "../../../src/features/chat/view-lifecycle"; -import type { ChatAppServerController } from "../../../src/features/chat/chat-app-server-controller"; +import type { AppServerClient } from "../../../../../src/app-server/client"; +import { createChatState, createChatStateStore } from "../../../../../src/features/chat/chat-state"; +import { + ChatConnectionController, + type ChatConnectionAdapter, +} from "../../../../../src/features/chat/controllers/connection/connection-controller"; +import { ChatConnectionWorkTracker } from "../../../../../src/features/chat/view-lifecycle"; +import type { ChatAppServerController } from "../../../../../src/features/chat/chat-app-server-controller"; function createController({ connected = false, client = {} as AppServerClient } = {}) { const stateStore = createChatStateStore(createChatState()); diff --git a/tests/features/chat/reconnect-controller.test.ts b/tests/features/chat/controllers/connection/reconnect-controller.test.ts similarity index 91% rename from tests/features/chat/reconnect-controller.test.ts rename to tests/features/chat/controllers/connection/reconnect-controller.test.ts index 677d863e..fb7612fc 100644 --- a/tests/features/chat/reconnect-controller.test.ts +++ b/tests/features/chat/controllers/connection/reconnect-controller.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it, vi } from "vitest"; -import { createChatState, createChatStateStore } from "../../../src/features/chat/chat-state"; -import { ChatReconnectController, type ChatReconnectControllerHost } from "../../../src/features/chat/reconnect-controller"; +import { createChatState, createChatStateStore } from "../../../../../src/features/chat/chat-state"; +import { + ChatReconnectController, + type ChatReconnectControllerHost, +} from "../../../../../src/features/chat/controllers/connection/reconnect-controller"; function createHost(overrides: Partial = {}) { const stateStore = createChatStateStore(createChatState()); diff --git a/tests/features/chat/pending-request-controller.test.ts b/tests/features/chat/controllers/requests/pending-request-controller.test.ts similarity index 83% rename from tests/features/chat/pending-request-controller.test.ts rename to tests/features/chat/controllers/requests/pending-request-controller.test.ts index bffd6cb2..00d27586 100644 --- a/tests/features/chat/pending-request-controller.test.ts +++ b/tests/features/chat/controllers/requests/pending-request-controller.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from "vitest"; -import { ChatController } from "../../../src/features/chat/chat-controller"; -import { createChatState, createChatStateStore } from "../../../src/features/chat/chat-state"; -import { PendingRequestController } from "../../../src/features/chat/pending-request-controller"; -import { toPendingUserInput } from "../../../src/features/chat/user-input/model"; -import type { ServerRequest } from "../../../src/generated/app-server/ServerRequest"; +import { ChatController } from "../../../../../src/features/chat/chat-controller"; +import { createChatState, createChatStateStore } from "../../../../../src/features/chat/chat-state"; +import { PendingRequestController } from "../../../../../src/features/chat/controllers/requests/pending-request-controller"; +import { toPendingUserInput } from "../../../../../src/features/chat/user-input/model"; +import type { ServerRequest } from "../../../../../src/generated/app-server/ServerRequest"; function expectPresent(value: T | null | undefined): T { if (value === null || value === undefined) throw new Error("Expected value to be present"); diff --git a/tests/features/chat/server-request-responder.test.ts b/tests/features/chat/controllers/requests/server-request-responder.test.ts similarity index 87% rename from tests/features/chat/server-request-responder.test.ts rename to tests/features/chat/controllers/requests/server-request-responder.test.ts index e5d5bdd9..7a4558b7 100644 --- a/tests/features/chat/server-request-responder.test.ts +++ b/tests/features/chat/controllers/requests/server-request-responder.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; -import type { AppServerClient } from "../../../src/app-server/client"; -import { ServerRequestResponder } from "../../../src/features/chat/server-request-responder"; +import type { AppServerClient } from "../../../../../src/app-server/client"; +import { ServerRequestResponder } from "../../../../../src/features/chat/controllers/requests/server-request-responder"; describe("ServerRequestResponder", () => { it("responds through the current app-server client", () => { diff --git a/tests/features/chat/composer-submission-controller.test.ts b/tests/features/chat/controllers/submission/composer-submission-controller.test.ts similarity index 80% rename from tests/features/chat/composer-submission-controller.test.ts rename to tests/features/chat/controllers/submission/composer-submission-controller.test.ts index 5b09dfa1..6a0ba24e 100644 --- a/tests/features/chat/composer-submission-controller.test.ts +++ b/tests/features/chat/controllers/submission/composer-submission-controller.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it, vi } from "vitest"; -import type { AppServerClient } from "../../../src/app-server/client"; -import { createChatState, createChatStateStore } from "../../../src/features/chat/chat-state"; -import { ComposerSubmissionController } from "../../../src/features/chat/composer-submission-controller"; -import type { ChatComposerController } from "../../../src/features/chat/chat-composer-controller"; -import type { SlashCommandController } from "../../../src/features/chat/slash-command-controller"; -import type { TurnSubmissionController } from "../../../src/features/chat/turn-submission-controller"; -import type { Thread } from "../../../src/generated/app-server/v2/Thread"; +import type { AppServerClient } from "../../../../../src/app-server/client"; +import { createChatState, createChatStateStore } from "../../../../../src/features/chat/chat-state"; +import { ComposerSubmissionController } from "../../../../../src/features/chat/controllers/submission/composer-submission-controller"; +import type { ChatComposerController } from "../../../../../src/features/chat/chat-composer-controller"; +import type { SlashCommandController } from "../../../../../src/features/chat/controllers/submission/slash-command-controller"; +import type { TurnSubmissionController } from "../../../../../src/features/chat/controllers/submission/turn-submission-controller"; +import type { Thread } from "../../../../../src/generated/app-server/v2/Thread"; function thread(id: string): Thread { return { diff --git a/tests/features/chat/plan-implementation-controller.test.ts b/tests/features/chat/controllers/submission/plan-implementation-controller.test.ts similarity index 87% rename from tests/features/chat/plan-implementation-controller.test.ts rename to tests/features/chat/controllers/submission/plan-implementation-controller.test.ts index 12fc1143..67999b35 100644 --- a/tests/features/chat/plan-implementation-controller.test.ts +++ b/tests/features/chat/controllers/submission/plan-implementation-controller.test.ts @@ -1,13 +1,13 @@ import { describe, expect, it, vi } from "vitest"; -import type { AppServerClient } from "../../../src/app-server/client"; -import { createChatState, createChatStateStore, type ChatStateStore } from "../../../src/features/chat/chat-state"; +import type { AppServerClient } from "../../../../../src/app-server/client"; +import { createChatState, createChatStateStore, type ChatStateStore } from "../../../../../src/features/chat/chat-state"; +import { implementPlanCandidateFromState } from "../../../../../src/features/chat/plan-implementation"; import { - implementPlanCandidateFromState, PlanImplementationController, type PlanImplementationControllerHost, -} from "../../../src/features/chat/plan-implementation-controller"; -import type { DisplayItem } from "../../../src/features/chat/display/types"; +} from "../../../../../src/features/chat/controllers/submission/plan-implementation-controller"; +import type { DisplayItem } from "../../../../../src/features/chat/display/types"; const planItem = (id: string): DisplayItem => ({ id, diff --git a/tests/features/chat/slash-command-controller.test.ts b/tests/features/chat/controllers/submission/slash-command-controller.test.ts similarity index 89% rename from tests/features/chat/slash-command-controller.test.ts rename to tests/features/chat/controllers/submission/slash-command-controller.test.ts index e7f71f27..62d3a027 100644 --- a/tests/features/chat/slash-command-controller.test.ts +++ b/tests/features/chat/controllers/submission/slash-command-controller.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it, vi } from "vitest"; -import type { AppServerClient } from "../../../src/app-server/client"; -import { createChatState, createChatStateStore } from "../../../src/features/chat/chat-state"; -import { SlashCommandController, type SlashCommandControllerHost } from "../../../src/features/chat/slash-command-controller"; -import type { Thread } from "../../../src/generated/app-server/v2/Thread"; -import type { UserInput } from "../../../src/generated/app-server/v2/UserInput"; +import type { AppServerClient } from "../../../../../src/app-server/client"; +import { createChatState, createChatStateStore } from "../../../../../src/features/chat/chat-state"; +import { + SlashCommandController, + type SlashCommandControllerHost, +} from "../../../../../src/features/chat/controllers/submission/slash-command-controller"; +import type { Thread } from "../../../../../src/generated/app-server/v2/Thread"; +import type { UserInput } from "../../../../../src/generated/app-server/v2/UserInput"; const textInput = (text: string): UserInput[] => [{ type: "text", text, text_elements: [] }]; diff --git a/tests/features/chat/turn-submission-controller.test.ts b/tests/features/chat/controllers/submission/turn-submission-controller.test.ts similarity index 89% rename from tests/features/chat/turn-submission-controller.test.ts rename to tests/features/chat/controllers/submission/turn-submission-controller.test.ts index 0809ddcc..c4d9d280 100644 --- a/tests/features/chat/turn-submission-controller.test.ts +++ b/tests/features/chat/controllers/submission/turn-submission-controller.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it, vi } from "vitest"; -import type { AppServerClient } from "../../../src/app-server/client"; -import { createChatState, createChatStateStore } from "../../../src/features/chat/chat-state"; -import { TurnSubmissionController, type TurnSubmissionControllerHost } from "../../../src/features/chat/turn-submission-controller"; -import type { Thread } from "../../../src/generated/app-server/v2/Thread"; -import type { UserInput } from "../../../src/generated/app-server/v2/UserInput"; +import type { AppServerClient } from "../../../../../src/app-server/client"; +import { createChatState, createChatStateStore } from "../../../../../src/features/chat/chat-state"; +import { + TurnSubmissionController, + type TurnSubmissionControllerHost, +} from "../../../../../src/features/chat/controllers/submission/turn-submission-controller"; +import type { Thread } from "../../../../../src/generated/app-server/v2/Thread"; +import type { UserInput } from "../../../../../src/generated/app-server/v2/UserInput"; const textInput = (text: string): UserInput[] => [{ type: "text", text, text_elements: [] }]; diff --git a/tests/features/chat/turn-submission.test.ts b/tests/features/chat/controllers/submission/turn-submission.test.ts similarity index 96% rename from tests/features/chat/turn-submission.test.ts rename to tests/features/chat/controllers/submission/turn-submission.test.ts index ec9fa12e..3412f4d4 100644 --- a/tests/features/chat/turn-submission.test.ts +++ b/tests/features/chat/controllers/submission/turn-submission.test.ts @@ -7,8 +7,8 @@ import { localUserMessageItemFromInput, optimisticTurnStart, shouldAcknowledgeTurnStart, -} from "../../../src/features/chat/turn-submission"; -import type { DisplayItem } from "../../../src/features/chat/display/types"; +} from "../../../../../src/features/chat/controllers/submission/turn-submission"; +import type { DisplayItem } from "../../../../../src/features/chat/display/types"; describe("chat turn submission helpers", () => { it("builds local user messages without sharing mentioned file arrays", () => { diff --git a/tests/features/chat/restored-thread-controller.test.ts b/tests/features/chat/controllers/thread/restored-thread-controller.test.ts similarity index 90% rename from tests/features/chat/restored-thread-controller.test.ts rename to tests/features/chat/controllers/thread/restored-thread-controller.test.ts index 6a1cd84f..2398466d 100644 --- a/tests/features/chat/restored-thread-controller.test.ts +++ b/tests/features/chat/controllers/thread/restored-thread-controller.test.ts @@ -2,9 +2,9 @@ import { describe, expect, it, vi } from "vitest"; -import { RestoredThreadController } from "../../../src/features/chat/restored-thread-controller"; -import { ChatViewDeferredTasks } from "../../../src/features/chat/view-lifecycle"; -import type { ChatAction } from "../../../src/features/chat/chat-state"; +import { RestoredThreadController } from "../../../../../src/features/chat/controllers/thread/restored-thread-controller"; +import { ChatViewDeferredTasks } from "../../../../../src/features/chat/view-lifecycle"; +import type { ChatAction } from "../../../../../src/features/chat/chat-state"; describe("RestoredThreadController", () => { it("restores a placeholder and schedules deferred hydration", () => { diff --git a/tests/features/chat/thread-identity-controller.test.ts b/tests/features/chat/controllers/thread/thread-identity-controller.test.ts similarity index 86% rename from tests/features/chat/thread-identity-controller.test.ts rename to tests/features/chat/controllers/thread/thread-identity-controller.test.ts index 2bdf76e6..456942c6 100644 --- a/tests/features/chat/thread-identity-controller.test.ts +++ b/tests/features/chat/controllers/thread/thread-identity-controller.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from "vitest"; -import { createChatState, createChatStateStore } from "../../../src/features/chat/chat-state"; -import { ThreadIdentityController } from "../../../src/features/chat/thread-identity-controller"; -import type { RestoredThreadController } from "../../../src/features/chat/restored-thread-controller"; -import type { RestoredThreadPlaceholderState } from "../../../src/features/chat/view-lifecycle"; -import type { Thread } from "../../../src/generated/app-server/v2/Thread"; +import { createChatState, createChatStateStore } from "../../../../../src/features/chat/chat-state"; +import { ThreadIdentityController } from "../../../../../src/features/chat/controllers/thread/thread-identity-controller"; +import type { RestoredThreadController } from "../../../../../src/features/chat/controllers/thread/restored-thread-controller"; +import type { RestoredThreadPlaceholderState } from "../../../../../src/features/chat/view-lifecycle"; +import type { Thread } from "../../../../../src/generated/app-server/v2/Thread"; function thread(id: string, name: string | null = null): Thread { return { diff --git a/tests/features/chat/thread-resume-controller.test.ts b/tests/features/chat/controllers/thread/thread-resume-controller.test.ts similarity index 82% rename from tests/features/chat/thread-resume-controller.test.ts rename to tests/features/chat/controllers/thread/thread-resume-controller.test.ts index 11f829cf..0f5e2c97 100644 --- a/tests/features/chat/thread-resume-controller.test.ts +++ b/tests/features/chat/controllers/thread/thread-resume-controller.test.ts @@ -1,13 +1,13 @@ import { describe, expect, it, vi } from "vitest"; -import type { AppServerClient } from "../../../src/app-server/client"; -import { createChatState, createChatStateStore } from "../../../src/features/chat/chat-state"; -import type { RestoredThreadController } from "../../../src/features/chat/restored-thread-controller"; -import type { ThreadActivationResponse } from "../../../src/features/chat/thread-resume"; -import { ThreadResumeController } from "../../../src/features/chat/thread-resume-controller"; -import type { ThreadHistoryLoader } from "../../../src/features/chat/thread-history"; -import { ChatResumeWorkTracker } from "../../../src/features/chat/view-lifecycle"; -import type { Thread } from "../../../src/generated/app-server/v2/Thread"; +import type { AppServerClient } from "../../../../../src/app-server/client"; +import { createChatState, createChatStateStore } from "../../../../../src/features/chat/chat-state"; +import type { RestoredThreadController } from "../../../../../src/features/chat/controllers/thread/restored-thread-controller"; +import type { ThreadActivationResponse } from "../../../../../src/features/chat/thread-resume"; +import { ThreadResumeController } from "../../../../../src/features/chat/controllers/thread/thread-resume-controller"; +import type { ThreadHistoryLoader } from "../../../../../src/features/chat/thread-history"; +import { ChatResumeWorkTracker } from "../../../../../src/features/chat/view-lifecycle"; +import type { Thread } from "../../../../../src/generated/app-server/v2/Thread"; function thread(id: string): Thread { return { diff --git a/tests/features/chat/thread-selection-controller.test.ts b/tests/features/chat/controllers/thread/thread-selection-controller.test.ts similarity index 94% rename from tests/features/chat/thread-selection-controller.test.ts rename to tests/features/chat/controllers/thread/thread-selection-controller.test.ts index f02ce4c2..1597a94b 100644 --- a/tests/features/chat/thread-selection-controller.test.ts +++ b/tests/features/chat/controllers/thread/thread-selection-controller.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it, vi } from "vitest"; -import { createChatState, createChatStateStore, type ChatStateStore } from "../../../src/features/chat/chat-state"; -import { ThreadSelectionController, type ThreadSelectionControllerHost } from "../../../src/features/chat/thread-selection-controller"; +import { createChatState, createChatStateStore, type ChatStateStore } from "../../../../../src/features/chat/chat-state"; +import { + ThreadSelectionController, + type ThreadSelectionControllerHost, +} from "../../../../../src/features/chat/controllers/thread/thread-selection-controller"; function resumeThreadState(stateStore: ChatStateStore, threadId: string): void { stateStore.dispatch({ diff --git a/tests/features/chat/message-scroll-controller.test.ts b/tests/features/chat/controllers/view/message-scroll-controller.test.ts similarity index 86% rename from tests/features/chat/message-scroll-controller.test.ts rename to tests/features/chat/controllers/view/message-scroll-controller.test.ts index e9718bb2..7f986697 100644 --- a/tests/features/chat/message-scroll-controller.test.ts +++ b/tests/features/chat/controllers/view/message-scroll-controller.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; -import { createChatState, createChatStateStore } from "../../../src/features/chat/chat-state"; -import { ChatMessageScrollController } from "../../../src/features/chat/message-scroll-controller"; +import { createChatState, createChatStateStore } from "../../../../../src/features/chat/chat-state"; +import { ChatMessageScrollController } from "../../../../../src/features/chat/controllers/view/message-scroll-controller"; describe("ChatMessageScrollController", () => { it("consumes one-shot scroll intents", () => { diff --git a/tests/features/chat/view-open-close-controller.test.ts b/tests/features/chat/controllers/view/view-open-close-controller.test.ts similarity index 88% rename from tests/features/chat/view-open-close-controller.test.ts rename to tests/features/chat/controllers/view/view-open-close-controller.test.ts index 9cd0cfa4..5ae134f9 100644 --- a/tests/features/chat/view-open-close-controller.test.ts +++ b/tests/features/chat/controllers/view/view-open-close-controller.test.ts @@ -3,15 +3,18 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { EventRef } from "obsidian"; -import { ChatViewOpenCloseController, type ChatViewOpenCloseControllerHost } from "../../../src/features/chat/view-open-close-controller"; -import { unmountChatPanelShell } from "../../../src/features/chat/ui/shell"; -import { unmountReactRoot } from "../../../src/shared/ui/react-root"; +import { + ChatViewOpenCloseController, + type ChatViewOpenCloseControllerHost, +} from "../../../../../src/features/chat/controllers/view/view-open-close-controller"; +import { unmountChatPanelShell } from "../../../../../src/features/chat/ui/shell"; +import { unmountReactRoot } from "../../../../../src/shared/ui/react-root"; -vi.mock("../../../src/features/chat/ui/shell", () => ({ +vi.mock("../../../../../src/features/chat/ui/shell", () => ({ unmountChatPanelShell: vi.fn(), })); -vi.mock("../../../src/shared/ui/react-root", () => ({ +vi.mock("../../../../../src/shared/ui/react-root", () => ({ unmountReactRoot: vi.fn(), })); diff --git a/tests/features/chat/view-render-controller.test.ts b/tests/features/chat/controllers/view/view-render-controller.test.ts similarity index 87% rename from tests/features/chat/view-render-controller.test.ts rename to tests/features/chat/controllers/view/view-render-controller.test.ts index b465ccba..0c74e225 100644 --- a/tests/features/chat/view-render-controller.test.ts +++ b/tests/features/chat/controllers/view/view-render-controller.test.ts @@ -2,11 +2,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { createChatState, createChatStateStore } from "../../../src/features/chat/chat-state"; -import { ChatViewRenderController } from "../../../src/features/chat/view-render-controller"; -import { renderChatPanelShell } from "../../../src/features/chat/ui/shell"; +import { createChatState, createChatStateStore } from "../../../../../src/features/chat/chat-state"; +import { ChatViewRenderController } from "../../../../../src/features/chat/controllers/view/view-render-controller"; +import { renderChatPanelShell } from "../../../../../src/features/chat/ui/shell"; -vi.mock("../../../src/features/chat/ui/shell", () => ({ +vi.mock("../../../../../src/features/chat/ui/shell", () => ({ renderChatPanelShell: vi.fn(), })); diff --git a/tests/features/chat/view-state-controller.test.ts b/tests/features/chat/controllers/view/view-state-controller.test.ts similarity index 91% rename from tests/features/chat/view-state-controller.test.ts rename to tests/features/chat/controllers/view/view-state-controller.test.ts index a07cc267..1b779e9c 100644 --- a/tests/features/chat/view-state-controller.test.ts +++ b/tests/features/chat/controllers/view/view-state-controller.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it, vi } from "vitest"; -import { ChatViewStateController, type ChatViewStateControllerHost } from "../../../src/features/chat/view-state-controller"; +import { + ChatViewStateController, + type ChatViewStateControllerHost, +} from "../../../../../src/features/chat/controllers/view/view-state-controller"; function createController(overrides: Partial = {}) { const host: ChatViewStateControllerHost = { diff --git a/tests/features/chat/ui/view-renderers.test.ts b/tests/features/chat/ui/view-renderers.test.ts index 50577b35..caf8a39b 100644 --- a/tests/features/chat/ui/view-renderers.test.ts +++ b/tests/features/chat/ui/view-renderers.test.ts @@ -9,7 +9,8 @@ import { scrollComposerSuggestionIntoView, syncComposerHeight, } from "../../../../src/features/chat/ui/composer"; -import { renderToolbar, type ToolbarViewModel } from "../../../../src/features/chat/ui/toolbar"; +import type { ToolbarViewModel } from "../../../../src/features/chat/toolbar-model"; +import { renderToolbar } from "../../../../src/features/chat/ui/toolbar"; import { CodexChatTurnDiffView } from "../../../../src/features/chat/chat-turn-diff-view"; import { persistedChatTurnDiffViewState, renderChatTurnDiffView } from "../../../../src/features/chat/ui/turn-diff"; import { displayDiffLines } from "../../../../src/shared/diff/unified"; diff --git a/tests/features/chat/view-connection.test.ts b/tests/features/chat/view-connection.test.ts index b18e39f1..33907433 100644 --- a/tests/features/chat/view-connection.test.ts +++ b/tests/features/chat/view-connection.test.ts @@ -6,6 +6,7 @@ import { DEFAULT_SETTINGS } from "../../../src/settings/model"; import type { CodexChatHost } from "../../../src/features/chat/view"; import { createAppServerDiagnostics } from "../../../src/app-server/compatibility"; import { createChatState, type ChatState } from "../../../src/features/chat/chat-state"; +import { composerSlotSnapshot } from "../../../src/features/chat/view-snapshot"; import type { ServerNotification } from "../../../src/generated/app-server/ServerNotification"; import { notices } from "../../mocks/obsidian"; import { installObsidianDomShims } from "./ui/dom-test-helpers"; @@ -325,17 +326,16 @@ describe("CodexChatView connection lifecycle", () => { }); it("tracks composer slot dependencies for model and skill suggestions", async () => { - const view = await chatView(); - const composerSnapshot = (view as unknown as { composerSnapshot: (state: ChatState) => string }).composerSnapshot; + await chatView(); const state = createChatState(); state.effectiveConfig = effectiveConfig("gpt-configured"); state.availableSkills = [skillFixture("writer")]; - const base = composerSnapshot(state); + const base = composerSlotSnapshot(state, null); - expect(composerSnapshot({ ...state, effectiveConfig: effectiveConfig("gpt-updated") })).not.toBe(base); - expect(composerSnapshot({ ...state, requestedModel: { kind: "set", value: "gpt-requested" } })).not.toBe(base); - expect(composerSnapshot({ ...state, availableSkills: [skillFixture("reader")] })).not.toBe(base); + expect(composerSlotSnapshot({ ...state, effectiveConfig: effectiveConfig("gpt-updated") }, null)).not.toBe(base); + expect(composerSlotSnapshot({ ...state, requestedModel: { kind: "set", value: "gpt-requested" } }, null)).not.toBe(base); + expect(composerSlotSnapshot({ ...state, availableSkills: [skillFixture("reader")] }, null)).not.toBe(base); }); it("hydrates a focused restored thread immediately", async () => {