Tidy chat controller boundaries

This commit is contained in:
murashit 2026-05-29 07:12:13 +09:00
parent 34e1289c22
commit e0182d7a1c
51 changed files with 316 additions and 271 deletions

View file

@ -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;

View file

@ -1,4 +1,4 @@
import type { ChatViewDeferredTasks } from "./view-lifecycle";
import type { ChatViewDeferredTasks } from "../../view-lifecycle";
export interface AppServerWarmupControllerHost {
deferredTasks: ChatViewDeferredTasks;

View file

@ -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<InitializeResponse>;

View file

@ -1,4 +1,4 @@
import type { ChatAction, ChatStateStore } from "./chat-state";
import type { ChatAction, ChatStateStore } from "../../chat-state";
export interface ChatReconnectControllerHost {
stateStore: ChatStateStore;

View file

@ -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;

View file

@ -1,4 +1,4 @@
import type { AppServerClient } from "../../app-server/client";
import type { AppServerClient } from "../../../../app-server/client";
type RespondRequestId = Parameters<AppServerClient["respondToServerRequest"]>[0];
type RejectRequestId = Parameters<AppServerClient["rejectServerRequest"]>[0];

View file

@ -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";

View file

@ -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<ChatState, "activeThreadId" | "turnLifecycle" | "composerDraft" | "requestedCollaborationMode" | "displayItems">,
): 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
);
}

View file

@ -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;

View file

@ -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,

View file

@ -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;

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -0,0 +1,19 @@
import { chatTurnBusy, type ChatState } from "./chat-state";
import type { DisplayItem } from "./display/types";
export function implementPlanCandidateFromState(
state: Pick<ChatState, "activeThreadId" | "turnLifecycle" | "composerDraft" | "requestedCollaborationMode" | "displayItems">,
): 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
);
}

View file

@ -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[],

View file

@ -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;
}

View file

@ -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));
}

View file

@ -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";

View file

@ -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;

View file

@ -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<string>;
onDetailsToggle?: (key: string, open: boolean) => void;
}
export function workItemsActiveTurnId(context: Pick<WorkItemContext, "turnLifecycle">): 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 <AgentRunSummaryItem summary={summary} />;
}
export function workItemNode(item: WorkItemDisplayItem, context: MessageStreamContext): ReactNode {
export function workItemNode(item: WorkItemDisplayItem, context: WorkItemContext): ReactNode {
if (item.kind === "taskProgress") return <TaskProgressItem item={item} />;
if (item.kind === "agent") return <AgentItem item={item} context={context} />;
return <ReasoningItem item={item} context={context} />;
@ -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 (
<WorkMessage label="agent" className="codex-panel__agent-activity" state={executionState(item)}>
<div className="codex-panel__tool-summary">{agentSummaryText(item)}</div>
@ -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 (
<div className={`codex-panel__reasoning${active ? " is-active" : ""}`}>
@ -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);

View file

@ -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`;
}

View file

@ -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<ServerRequest, { method: "item/tool/requestUserInput" }>;

View file

@ -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;

View file

@ -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);
}

View file

@ -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,

View file

@ -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());

View file

@ -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<ChatReconnectControllerHost> = {}) {
const stateStore = createChatStateStore(createChatState());

View file

@ -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<T>(value: T | null | undefined): T {
if (value === null || value === undefined) throw new Error("Expected value to be present");

View file

@ -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", () => {

View file

@ -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 {

View file

@ -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,

View file

@ -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: [] }];

View file

@ -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: [] }];

View file

@ -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", () => {

View file

@ -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", () => {

View file

@ -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 {

View file

@ -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 {

View file

@ -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({

View file

@ -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", () => {

View file

@ -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(),
}));

View file

@ -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(),
}));

View file

@ -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<ChatViewStateControllerHost> = {}) {
const host: ChatViewStateControllerHost = {

View file

@ -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";

View file

@ -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 () => {