Normalize chat conversation and runtime modules

This commit is contained in:
murashit 2026-06-12 11:54:45 +09:00
parent 806a3f44e6
commit 230f35627f
69 changed files with 637 additions and 593 deletions

View file

@ -6,7 +6,7 @@ import type { SharedAppServerMetadata } from "../../app-server/shared-cache-stat
import type { ArchiveExportAdapter } from "../../domain/threads/export";
import type { Thread } from "../../domain/threads/model";
import type { CodexPanelSettings } from "../../settings/model";
import type { RuntimeSnapshot } from "./runtime/model";
import type { RuntimeSnapshot } from "./runtime/snapshot";
import type { ChatState, ChatStateStore } from "./state/reducer";
import type { ChatTurnDiffViewState } from "./turn-diff/model";
import type { ChatMessageScrollIntentController } from "./ui/message-scroll-intent-controller";

View file

@ -1,24 +1,24 @@
import { ConnectionManager } from "../../app-server/connection-manager";
import type { ChatServerDiagnosticsActions } from "./protocol/client-actions/diagnostics-actions";
import type { ChatServerMetadataActions } from "./protocol/client-actions/metadata-actions";
import type { ChatServerThreadActions } from "./protocol/client-actions/thread-actions";
import type { ChatServerDiagnosticsActions } from "./connection/server-actions/diagnostics";
import type { ChatServerMetadataActions } from "./connection/server-actions/metadata";
import type { ChatServerThreadActions } from "./connection/server-actions/threads";
import type { ChatComposerController } from "./conversation/composer/controller";
import type { ChatInboundController } from "./protocol/inbound/controller";
import type { ChatThreadGoalActions } from "./threads/thread-goal-actions";
import { createChatRuntimeSettingsActions, type ChatRuntimeSettingsActions } from "./runtime/runtime-settings-actions";
import { createChatRuntimeSettingsActions, type ChatRuntimeSettingsActions } from "./runtime/settings-actions";
import type { ChatThreadActions } from "./threads/thread-actions";
import type { ThreadHistoryController } from "./threads/thread-history-controller";
import type { ThreadRenameController } from "./threads/thread-rename-controller";
import type { ToolbarPanelController } from "./panel/regions/toolbar";
import type { ChatConnectionController } from "./connection/connection-controller";
import type { ChatReconnectActions } from "./connection/reconnect-actions";
import type { PendingRequestController } from "./pending-requests/controller";
import type { PendingRequestController } from "./conversation/pending-requests/controller";
import { rejectServerRequest, respondToServerRequest } from "./protocol/requests/server-request-responder";
import type { ComposerSubmissionActions } from "./conversation/turns/composer-submission-actions";
import type { RestoredThreadController } from "./threads/restored-thread-controller";
import type { ThreadIdentityActions } from "./threads/thread-identity-actions";
import type { ThreadIdentitySync } from "./threads/thread-identity-sync";
import type { ThreadResumeController } from "./threads/thread-resume-controller";
import type { ThreadSelectionActions } from "./threads/thread-selection-controller";
import type { ThreadSelectionActions } from "./threads/thread-selection-actions";
import type { ChatViewRenderController } from "./panel/view-render-controller";
import type { ChatMessageRenderer } from "./ui/message-stream/renderer";
import type { ChatControllerCompositionPorts } from "./composition-ports";
@ -29,8 +29,8 @@ import {
createChatInboundController,
createChatReconnectControllerGroup,
} from "./connection/composition";
import { createThreadControllerGroup, createThreadSelectionControllerGroup } from "./threads/composition";
import { createConversationSurfaceControllerGroup } from "./conversation/turns/composition";
import { createThreadControllerGroup, createThreadSelectionActionGroup } from "./threads/composition";
import { createConversationSurfaceControllerGroup } from "./conversation/composition";
import {
createConnectionLifecycleControllerGroup,
createPanelUiControllerGroup,
@ -57,7 +57,7 @@ export interface ChatViewControllers {
resume: ThreadResumeController;
actions: ChatThreadActions;
restored: RestoredThreadController;
identity: ThreadIdentityActions;
identity: ThreadIdentitySync;
rename: ThreadRenameController;
selection: ThreadSelectionActions;
};
@ -221,7 +221,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
threadActions,
},
);
threadSelection = createThreadSelectionControllerGroup(
threadSelection = createThreadSelectionActionGroup(
{
plugin: {
focusThreadInOpenView: (threadId) => ports.plugin.focusThreadInOpenView(threadId),
@ -235,7 +235,9 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
status: actions.status,
},
{
toolbarPanels,
closeForThreadSelection: () => {
toolbarPanels.closeForThreadSelection();
},
},
).threadSelection;
const { reconnectActions } = createChatReconnectControllerGroup(

View file

@ -1,6 +1,6 @@
import type { ChatViewDeferredTasks } from "../lifecycle";
export interface AppServerWarmupControllerHost {
export interface AppServerWarmupHost {
deferredTasks: ChatViewDeferredTasks;
opened: () => boolean;
closing: () => boolean;
@ -8,7 +8,7 @@ export interface AppServerWarmupControllerHost {
ensureConnected: () => Promise<void>;
}
export function scheduleAppServerWarmup(host: AppServerWarmupControllerHost): void {
export function scheduleAppServerWarmup(host: AppServerWarmupHost): void {
const shouldWarmup = (): boolean => host.opened() && !host.connected();
if (!shouldWarmup()) return;

View file

@ -1,14 +1,14 @@
import { Notice } from "obsidian";
import type { ConnectionManager } from "../../../app-server/connection-manager";
import type { RuntimeSnapshot } from "../runtime/model";
import type { RuntimeSnapshot } from "../runtime/snapshot";
import type { ChatState, ChatStateStore } from "../state/reducer";
import type { SharedAppServerMetadata } from "../../../app-server/shared-cache-state";
import type { Thread } from "../../../domain/threads/model";
import type { CodexPanelSettings } from "../../../settings/model";
import { createChatServerDiagnosticsActions, type ChatServerDiagnosticsActions } from "../protocol/client-actions/diagnostics-actions";
import { createChatServerMetadataActions, type ChatServerMetadataActions } from "../protocol/client-actions/metadata-actions";
import { createChatServerThreadActions } from "../protocol/client-actions/thread-actions";
import { createChatServerDiagnosticsActions, type ChatServerDiagnosticsActions } from "./server-actions/diagnostics";
import { createChatServerMetadataActions, type ChatServerMetadataActions } from "./server-actions/metadata";
import { createChatServerThreadActions } from "./server-actions/threads";
import { ChatConnectionController } from "./connection-controller";
import { createChatReconnectActions } from "./reconnect-actions";
import type { rejectServerRequest, respondToServerRequest } from "../protocol/requests/server-request-responder";

View file

@ -12,7 +12,7 @@ import {
} from "../../../../app-server/diagnostics";
import type { SharedAppServerMetadata } from "../../../../app-server/shared-cache-state";
import { mcpStatusLines as buildMcpStatusLines } from "../../display/diagnostics";
import { cloneAppServerDiagnostics, type ChatServerActionHost } from "./shared";
import { cloneAppServerDiagnostics, type ChatServerActionHost } from "./host";
interface RefreshDiagnosticProbesOptions {
cachedAppServerMetadata?: boolean;

View file

@ -4,7 +4,7 @@ import { runtimeConfigSnapshotFromAppServerConfig } from "../../../../app-server
import { rateLimitSnapshotFromAppServerSnapshot } from "../../../../app-server/runtime-metrics";
import type { SharedAppServerMetadata } from "../../../../app-server/shared-cache-state";
import type { ModelMetadata, SkillMetadata } from "../../../../domain/catalog/metadata";
import { cloneAppServerDiagnostics, type ChatServerActionHost } from "./shared";
import { cloneAppServerDiagnostics, type ChatServerActionHost } from "./host";
interface RateLimitMetadataResult {
data: SharedAppServerMetadata["rateLimit"];

View file

@ -1,10 +1,10 @@
import { listThreads } from "../../../../app-server/resource-operations";
import type { Thread } from "../../../../domain/threads/model";
import type { RuntimeSnapshot } from "../../runtime/model";
import { runtimeConfigOrDefault } from "../../runtime/effective-settings";
import { serviceTierRequestForThreadStart } from "../../runtime/turn-settings";
import type { RuntimeSnapshot } from "../../runtime/snapshot";
import { runtimeConfigOrDefault } from "../../runtime/effective";
import { serviceTierRequestForThreadStart } from "../../runtime/thread-settings";
import { resumedThreadActionFromAppServerResponse } from "../../threads/thread-resume";
import type { ChatServerActionHost } from "./shared";
import type { ChatServerActionHost } from "./host";
import type { ChatState } from "../../state/reducer";
interface StartedThreadSummary {

View file

@ -1,29 +1,29 @@
import type { App, Component } from "obsidian";
import type { AppServerClient } from "../../../../app-server/client";
import type { ChatServerThreadActions } from "../../protocol/client-actions/thread-actions";
import { ChatComposerController } from "../composer/controller";
import { activeTurnId, type ChatState, type ChatStateStore } from "../../state/reducer";
import type { ChatReconnectActions } from "../../connection/reconnect-actions";
import { PendingRequestController } from "../../pending-requests/controller";
import type { ChatRuntimeSettingsActions } from "../../runtime/runtime-settings-actions";
import { createComposerSubmissionActions } from "./composer-submission-actions";
import { createPlanImplementationActions } from "./plan-implementation-actions";
import { createSlashCommandActions } from "./slash-command-actions";
import { TurnSubmissionController } from "./turn-submission-controller";
import type { ChatThreadActions } from "../../threads/thread-actions";
import type { ChatThreadGoalActions } from "../../threads/thread-goal-actions";
import type { ThreadHistoryController } from "../../threads/thread-history-controller";
import type { ThreadRenameController } from "../../threads/thread-rename-controller";
import type { ChatInboundController } from "../../protocol/inbound/controller";
import { currentModel, runtimeConfigOrDefault } from "../../runtime/effective-settings";
import type { RuntimeSnapshot } from "../../runtime/model";
import { ChatMessageRenderer } from "../../ui/message-stream/renderer";
import type { DisplayDetailSection } from "../../display/types";
import type { ChatMessageScrollIntentController } from "../../ui/message-scroll-intent-controller";
import type { ChatTurnDiffViewState } from "../../turn-diff/model";
import type { ComposerMetaViewModel } from "../../ui/composer";
import type { CodexPanelSettings } from "../../../../settings/model";
import type { AppServerClient } from "../../../app-server/client";
import type { ChatServerThreadActions } from "../connection/server-actions/threads";
import { ChatComposerController } from "./composer/controller";
import { activeTurnId, type ChatState, type ChatStateStore } from "../state/reducer";
import type { ChatReconnectActions } from "../connection/reconnect-actions";
import { PendingRequestController } from "./pending-requests/controller";
import type { ChatRuntimeSettingsActions } from "../runtime/settings-actions";
import { createComposerSubmissionActions } from "./turns/composer-submission-actions";
import { createPlanImplementationActions } from "./turns/plan-implementation-actions";
import { createSlashCommandActions } from "./turns/slash-command-actions";
import { TurnSubmissionController } from "./turns/turn-submission-controller";
import type { ChatThreadActions } from "../threads/thread-actions";
import type { ChatThreadGoalActions } from "../threads/thread-goal-actions";
import type { ThreadHistoryController } from "../threads/thread-history-controller";
import type { ThreadRenameController } from "../threads/thread-rename-controller";
import type { ChatInboundController } from "../protocol/inbound/controller";
import { currentModel, runtimeConfigOrDefault } from "../runtime/effective";
import type { RuntimeSnapshot } from "../runtime/snapshot";
import { ChatMessageRenderer } from "../ui/message-stream/renderer";
import type { DisplayDetailSection } from "../display/types";
import type { ChatMessageScrollIntentController } from "../ui/message-scroll-intent-controller";
import type { ChatTurnDiffViewState } from "../turn-diff/model";
import type { ComposerMetaViewModel } from "../ui/composer";
import type { CodexPanelSettings } from "../../../settings/model";
interface ConversationSurfaceControllerGroupPorts {
obsidian: {
@ -185,7 +185,7 @@ export function createConversationSurfaceControllerGroup(
rollbackThread: (threadId) => refs.threadActions.rollbackThread(threadId),
compactThread: (threadId) => refs.threadActions.compactThread(threadId),
archiveThread: (threadId) => refs.threadActions.archiveThread(threadId),
renameThread: (threadId, name) => refs.threadRename.rename(threadId, name),
renameThread: (threadId, name) => refs.threadActions.renameThread(threadId, name).then(() => undefined),
reconnect: () => refs.reconnectActions.reconnectPanel(),
},
runtime: {

View file

@ -1,10 +1,10 @@
import { pendingRequestSnapshot, type PendingRequestSnapshot } from "../state/selectors";
import type { ChatStateStore } from "../state/reducer";
import type { ApprovalAction, PendingApproval } from "../protocol/requests/approval";
import type { ChatInboundController } from "../protocol/inbound/controller";
import { pendingRequestFocusSignature } from "./view-model";
import type { PendingRequestMessageActions } from "../ui/pending-request-message";
import { answersForPendingUserInput, type PendingUserInput } from "../protocol/requests/user-input";
import type { ChatStateStore } from "../../state/reducer";
import type { ApprovalAction, PendingApproval } from "../../protocol/requests/approval";
import type { ChatInboundController } from "../../protocol/inbound/controller";
import { pendingRequestFocusSignature } from "./signatures";
import { pendingRequestBlockSnapshot, type PendingRequestBlockSnapshot } from "./snapshot";
import type { PendingRequestMessageActions } from "../../ui/pending-request-message";
import { answersForPendingUserInput, type PendingUserInput } from "../../protocol/requests/user-input";
export interface PendingRequestControllerHost {
stateStore: ChatStateStore;
@ -36,8 +36,8 @@ export class PendingRequestController {
constructor(private readonly host: PendingRequestControllerHost) {}
snapshot(): PendingRequestSnapshot {
return pendingRequestSnapshot(this.host.stateStore.getState());
snapshot(): PendingRequestBlockSnapshot {
return pendingRequestBlockSnapshot(this.host.stateStore.getState());
}
actions(): PendingRequestMessageActions {
@ -52,7 +52,7 @@ export class PendingRequestController {
resolveUserInput(input: PendingUserInput): void {
this.host.controller.resolveUserInput(
input,
answersForPendingUserInput(input, pendingRequestSnapshot(this.host.stateStore.getState()).userInputDrafts),
answersForPendingUserInput(input, pendingRequestBlockSnapshot(this.host.stateStore.getState()).userInputDrafts),
);
this.commitRequestAction();
}
@ -68,7 +68,7 @@ export class PendingRequestController {
}
consumeAutoFocus(): boolean {
const state = pendingRequestSnapshot(this.host.stateStore.getState());
const state = pendingRequestBlockSnapshot(this.host.stateStore.getState());
const signature = pendingRequestFocusSignature(state.approvals, state.pendingUserInputs);
if (!signature) {
this.lastFocusSignature = "";

View file

@ -5,39 +5,10 @@ import {
approvalTitle,
type ApprovalAction,
type PendingApproval,
} from "../protocol/requests/approval";
import type { DisplayDetailSection, DisplayItem } from "../display/types";
import { definedProp } from "../../../utils";
import type { PendingUserInput } from "../protocol/requests/user-input";
export function pendingRequestsSignature(
approvals: readonly PendingApproval[],
inputs: readonly PendingUserInput[],
drafts: ReadonlyMap<string, string>,
): string {
if (approvals.length === 0 && inputs.length === 0) return "";
return JSON.stringify({
approvals: approvals.map((approval) => ({ id: approval.requestId, method: approval.method })),
inputs: inputs.map((input) => ({
id: input.requestId,
questions: input.params.questions.map((question) => ({
id: question.id,
header: question.header,
question: question.question,
options: question.options?.map((option) => option.label) ?? null,
})),
})),
drafts: Array.from(drafts.entries()).sort(([left], [right]) => left.localeCompare(right)),
});
}
export function pendingRequestFocusSignature(approvals: readonly PendingApproval[], inputs: readonly PendingUserInput[]): string {
if (approvals.length === 0 && inputs.length === 0) return "";
return JSON.stringify({
approvals: approvals.map((approval) => ({ id: approval.requestId, method: approval.method })),
inputs: inputs.map((input) => ({ id: input.requestId, method: input.method })),
});
}
} from "../../protocol/requests/approval";
import type { DisplayDetailSection, DisplayItem } from "../../display/types";
import type { PendingUserInput } from "../../protocol/requests/user-input";
import { definedProp } from "../../../../utils";
export function createApprovalResultItem(approval: PendingApproval, action: ApprovalAction): DisplayItem {
const status = approvalResultStatus(action);

View file

@ -0,0 +1,31 @@
import type { PendingApproval } from "../../protocol/requests/approval";
import type { PendingUserInput } from "../../protocol/requests/user-input";
export function pendingRequestsSignature(
approvals: readonly PendingApproval[],
inputs: readonly PendingUserInput[],
drafts: ReadonlyMap<string, string>,
): string {
if (approvals.length === 0 && inputs.length === 0) return "";
return JSON.stringify({
approvals: approvals.map((approval) => ({ id: approval.requestId, method: approval.method })),
inputs: inputs.map((input) => ({
id: input.requestId,
questions: input.params.questions.map((question) => ({
id: question.id,
header: question.header,
question: question.question,
options: question.options?.map((option) => option.label) ?? null,
})),
})),
drafts: Array.from(drafts.entries()).sort(([left], [right]) => left.localeCompare(right)),
});
}
export function pendingRequestFocusSignature(approvals: readonly PendingApproval[], inputs: readonly PendingUserInput[]): string {
if (approvals.length === 0 && inputs.length === 0) return "";
return JSON.stringify({
approvals: approvals.map((approval) => ({ id: approval.requestId, method: approval.method })),
inputs: inputs.map((input) => ({ id: input.requestId, method: input.method })),
});
}

View file

@ -0,0 +1,19 @@
import type { ChatState } from "../../state/reducer";
import type { PendingApproval } from "../../protocol/requests/approval";
import type { PendingUserInput } from "../../protocol/requests/user-input";
export interface PendingRequestBlockSnapshot {
approvals: readonly PendingApproval[];
pendingUserInputs: readonly PendingUserInput[];
userInputDrafts: ReadonlyMap<string, string>;
openDetails: ReadonlySet<string>;
}
export function pendingRequestBlockSnapshot(state: ChatState): PendingRequestBlockSnapshot {
return {
approvals: state.requests.approvals,
pendingUserInputs: state.requests.pendingUserInputs,
userInputDrafts: state.requests.userInputDrafts,
openDetails: state.ui.openDetails,
};
}

View file

@ -1,6 +1,6 @@
import type { PendingApproval } from "../protocol/requests/approval";
import type { RequestId } from "../protocol/requests/model";
import { userInputDraftKey, userInputOtherDraftKey, type PendingUserInput } from "../protocol/requests/user-input";
import type { PendingApproval } from "../../protocol/requests/approval";
import type { RequestId } from "../../../../app-server/types";
import { userInputDraftKey, userInputOtherDraftKey, type PendingUserInput } from "../../protocol/requests/user-input";
export interface ChatRequestState {
approvals: readonly PendingApproval[];

View file

@ -15,7 +15,7 @@ import { findModelMetadataByIdOrName, supportedEffortsForModelMetadata } from ".
import type { ThreadGoal, ThreadGoalStatus } from "../../../../app-server/thread-goal";
import { submissionStateSnapshot } from "../../state/selectors";
import type { ChatStateStore } from "../../state/reducer";
import { currentModel, runtimeConfigOrDefault } from "../../runtime/effective-settings";
import { currentModel, runtimeConfigOrDefault } from "../../runtime/effective";
import { runtimeSnapshotForChatState } from "../../runtime/snapshot";
export interface SlashCommandThreadPort {

View file

@ -14,8 +14,9 @@ import {
runtimeConfigOrDefault,
serviceTierLabel,
supportedReasoningEfforts,
} from "../runtime/effective-settings";
import { collaborationModeLabel, pendingRuntimeSettingLabel, type RuntimeSnapshot } from "../runtime/model";
} from "../runtime/effective";
import type { RuntimeSnapshot } from "../runtime/snapshot";
import { collaborationModeLabel, pendingRuntimeSettingLabel } from "../runtime/settings";
export interface ContextSummary {
label: string;

View file

@ -1,7 +1,7 @@
import type { SharedAppServerMetadata } from "../../../app-server/shared-cache-state";
import type { Thread } from "../../../domain/threads/model";
import type { ChatServerMetadataActions } from "../protocol/client-actions/metadata-actions";
import type { ChatServerThreadActions } from "../protocol/client-actions/thread-actions";
import type { ChatServerMetadataActions } from "../connection/server-actions/metadata";
import type { ChatServerThreadActions } from "../connection/server-actions/threads";
export interface CachedSharedAppServerStateSource {
cachedThreadList: () => readonly Thread[] | null;

View file

@ -2,15 +2,15 @@ import type { ConnectionManager } from "../../../app-server/connection-manager";
import type { ComponentChild as UiNode } from "preact";
import type { ChatStateStore } from "../state/reducer";
import type { CodexPanelSettings } from "../../../settings/model";
import type { ChatServerMetadataActions } from "../protocol/client-actions/metadata-actions";
import type { ChatServerThreadActions } from "../protocol/client-actions/thread-actions";
import type { ChatServerMetadataActions } from "../connection/server-actions/metadata";
import type { ChatServerThreadActions } from "../connection/server-actions/threads";
import type { ChatComposerController } from "../conversation/composer/controller";
import type { ChatThreadActions } from "../threads/thread-actions";
import { scheduleAppServerWarmup } from "../connection/app-server-warmup-controller";
import { scheduleAppServerWarmup } from "../connection/app-server-warmup";
import { closeChatView, openChatView, type ChatViewLifecycleHost } from "./view-lifecycle";
import { createToolbarArchiveConfirmState, ToolbarPanelController } from "./regions/toolbar";
import { ChatViewRenderController } from "./view-render-controller";
import { applyChatViewState } from "./view-state-controller";
import { applyChatViewState } from "./view-state";
import type { ChatMessageRenderer } from "../ui/message-stream/renderer";
import { applyCachedSharedAppServerState, type CachedSharedAppServerStateSource } from "./cached-app-server-state";
import type { ChatViewDeferredTasks, RestoredThreadState } from "../lifecycle";

View file

@ -5,12 +5,12 @@ import {
fastModeActive,
runtimeConfigOrDefault,
supportedReasoningEfforts,
} from "../../runtime/effective-settings";
} from "../../runtime/effective";
import { compactReasoningEffortLabel } from "../../conversation/turns/runtime-overrides";
import { contextSummary } from "../../display/runtime-status";
import { sortedModelMetadata } from "../../../../domain/catalog/metadata";
import type { ReasoningEffort } from "../../../../domain/catalog/metadata";
import type { RuntimeSnapshot } from "../../runtime/model";
import type { RuntimeSnapshot } from "../../runtime/snapshot";
import type { ChatState } from "../../state/reducer";
import type {
ComposerContextMeterCellViewModel,

View file

@ -1,4 +1,4 @@
import { pendingRequestsSignature as requestStateSignature } from "../../pending-requests/view-model";
import { pendingRequestsSignature as requestStateSignature } from "../../conversation/pending-requests/signatures";
import type { ChatPanelMessagesPorts } from "./ports";
export function chatPanelMessagesNode(ports: ChatPanelMessagesPorts) {

View file

@ -1,7 +1,7 @@
import type { ComponentChild as UiNode } from "preact";
import type { ChatState } from "../../state/reducer";
import type { ReasoningEffort } from "../../../../domain/catalog/metadata";
import type { RuntimeSnapshot } from "../../runtime/model";
import type { RuntimeSnapshot } from "../../runtime/snapshot";
import type { SendShortcut } from "../../../../shared/ui/keyboard";
import type { ToolbarActions } from "../../ui/toolbar";
import type { ToolbarThreadRow } from "../../ui/toolbar";

View file

@ -3,7 +3,7 @@ import { getThreadTitle } from "../../../../domain/threads/model";
import type { ChatThreadActions } from "../../threads/thread-actions";
import { runtimeConfigSections, rateLimitSummary } from "../../display/runtime-status";
import { connectionDiagnosticSections } from "../../display/diagnostics";
import type { RuntimeSnapshot } from "../../runtime/model";
import type { RuntimeSnapshot } from "../../runtime/snapshot";
import type { ChatAction, ChatState, ChatStateStore } from "../../state/reducer";
import type { ChatPanelShellState } from "../../ui/shell";
import type { ToolbarThreadRow, ToolbarViewModel } from "../../ui/toolbar";

View file

@ -1,7 +1,7 @@
import { parseRestoredThreadState } from "./snapshot";
import type { RestoredThreadState } from "../lifecycle";
export interface ChatViewStateControllerHost {
export interface ChatViewStateHost {
invalidateResumeWork: () => void;
clearRestoredThreadLifecycle: () => void;
clearDeferredRestoredThreadHydration: () => void;
@ -9,7 +9,7 @@ export interface ChatViewStateControllerHost {
restoreThreadPlaceholder: (restoredThread: RestoredThreadState) => void;
}
export function applyChatViewState(host: ChatViewStateControllerHost, state: unknown): void {
export function applyChatViewState(host: ChatViewStateHost, state: unknown): void {
const restoredThread = parseRestoredThreadState(state);
if (!restoredThread) {
host.invalidateResumeWork();

View file

@ -1,4 +1,4 @@
import type { ServerNotification, ServerRequest } from "../../../../app-server/types";
import type { RequestId, ServerNotification, ServerRequest } from "../../../../app-server/types";
import type { McpServerStartupStatus } from "../../../../app-server/diagnostics";
import type { ThreadConversationSummary } from "../../../../domain/threads/transcript";
import { classifyAppServerLog } from "./app-server-logs";
@ -6,9 +6,8 @@ import { activeTurnId, type ChatAction, type ChatState, type ChatStateStore } fr
import { createStructuredSystemItem, createSystemItem } from "../../display/system";
import type { DisplayDetailSection } from "../../display/types";
import { approvalResponse, type ApprovalAction, type PendingApproval } from "../requests/approval";
import type { RequestId } from "../requests/model";
import { userInputResponse, type PendingUserInput } from "../requests/user-input";
import { createApprovalResultItem, createUserInputResultItem } from "../../pending-requests/view-model";
import { createApprovalResultItem, createUserInputResultItem } from "../../conversation/pending-requests/result-items";
import { planChatNotification, type ChatNotificationEffect } from "./notification-plan";
import { routeServerRequest } from "./routing";

View file

@ -1,6 +1,6 @@
import { jsonPreview } from "../../../../utils";
import { permissionRows } from "../../display/permission-rows";
import type { RequestId } from "./model";
import type { RequestId } from "../../../../app-server/types";
type SimpleApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel";
export type CommandApprovalDecision =

View file

@ -1 +0,0 @@
export type RequestId = string | number;

View file

@ -1,6 +1,6 @@
import type { RequestId } from "./model";
import type { RequestId } from "../../../../app-server/types";
export type { RequestId } from "./model";
export type { RequestId } from "../../../../app-server/types";
interface UserInputRequestLike {
id: RequestId;

View file

@ -2,7 +2,7 @@ import { cloneRuntimeConfigSnapshot, emptyRuntimeConfigSnapshot, type RuntimeCon
import { findModelMetadataByIdOrName, type ModelMetadata } from "../../../domain/catalog/metadata";
import type { ApprovalPolicy, ApprovalsReviewer } from "../../../app-server/runtime-policy";
import { supportedEffortsForModelMetadata, type ReasoningEffort } from "../../../domain/catalog/metadata";
import type { RuntimeSnapshot } from "./model";
import type { RuntimeSnapshot } from "./snapshot";
export function runtimeConfigOrDefault(runtimeConfig: RuntimeConfigSnapshot | null): RuntimeConfigSnapshot {
return runtimeConfig ? cloneRuntimeConfigSnapshot(runtimeConfig) : emptyRuntimeConfigSnapshot();

View file

@ -1,55 +0,0 @@
import type { ActivePermissionProfile, RuntimeConfigSnapshot } from "../../../app-server/runtime-config";
import type { RateLimitSnapshot, ThreadTokenUsage } from "../../../app-server/runtime-metrics";
import type { ApprovalPolicy, ApprovalsReviewer, ServiceTier } from "../../../app-server/runtime-policy";
import type { ModelMetadata, ReasoningEffort } from "../../../domain/catalog/metadata";
export type CollaborationMode = "default" | "plan";
export type PendingRuntimeSetting<T> = { kind: "unchanged" } | { kind: "set"; value: T } | { kind: "resetToConfig" };
export type RequestedServiceTier = "fast" | "off";
export interface RuntimeSnapshot {
runtimeConfig: RuntimeConfigSnapshot | null;
activeThreadId: string | null;
activeModel: string | null;
activeReasoningEffort: ReasoningEffort | null;
activeCollaborationMode: CollaborationMode;
activeServiceTier: ServiceTier | null;
activeApprovalPolicy: ApprovalPolicy | null;
activeApprovalsReviewer: ApprovalsReviewer | null;
activePermissionProfile: ActivePermissionProfile | null;
requestedModel: PendingRuntimeSetting<string>;
requestedReasoningEffort: PendingRuntimeSetting<ReasoningEffort>;
requestedApprovalsReviewer: PendingRuntimeSetting<ApprovalsReviewer>;
selectedCollaborationMode: CollaborationMode;
requestedServiceTier: PendingRuntimeSetting<RequestedServiceTier>;
tokenUsage: ThreadTokenUsage | null;
rateLimit: RateLimitSnapshot | null;
hasThreadTurns: boolean;
availableModels: readonly ModelMetadata[];
}
export function unchangedRuntimeSetting<T>(): PendingRuntimeSetting<T> {
return { kind: "unchanged" };
}
export function setPendingRuntimeSetting<T>(value: T): PendingRuntimeSetting<T> {
return { kind: "set", value };
}
export function resetRuntimeSettingToConfig<T>(): PendingRuntimeSetting<T> {
return { kind: "resetToConfig" };
}
export function nextCollaborationMode(mode: CollaborationMode): CollaborationMode {
return mode === "plan" ? "default" : "plan";
}
export function collaborationModeLabel(mode: CollaborationMode): string {
return mode === "plan" ? "Plan" : "Default";
}
export function pendingRuntimeSettingLabel<T>(setting: PendingRuntimeSetting<T>): string {
if (setting.kind === "set") return String(setting.value);
if (setting.kind === "resetToConfig") return "(reset to config)";
return "(none)";
}

View file

@ -3,13 +3,14 @@ import type { RuntimeConfigSnapshot } from "../../../app-server/runtime-config";
import type { ApprovalsReviewer } from "../../../app-server/runtime-policy";
import type { ThreadSettingsUpdate } from "../../../app-server/thread-settings";
import type { ReasoningEffort } from "../../../domain/catalog/metadata";
import { autoReviewActive, fastModeActive, runtimeConfigOrDefault } from "./effective-settings";
import { autoReviewActive, fastModeActive, runtimeConfigOrDefault } from "./effective";
import {
pendingThreadSettingsUpdate as buildPendingThreadSettingsUpdate,
type PendingThreadSettingsUpdate,
type TurnCollaborationModeWarning,
} from "./turn-settings";
import { nextCollaborationMode, type CollaborationMode, type RequestedServiceTier, type RuntimeSnapshot } from "./model";
} from "./thread-settings";
import type { RuntimeSnapshot } from "./snapshot";
import { nextCollaborationMode, type CollaborationMode, type RequestedServiceTier } from "./settings";
import { modelOverrideMessage, reasoningEffortOverrideMessage } from "../conversation/turns/runtime-overrides";
import type { ChatAction, ChatState, ChatStateStore } from "../state/reducer";

View file

@ -0,0 +1,29 @@
export type CollaborationMode = "default" | "plan";
export type PendingRuntimeSetting<T> = { kind: "unchanged" } | { kind: "set"; value: T } | { kind: "resetToConfig" };
export type RequestedServiceTier = "fast" | "off";
export function unchangedRuntimeSetting<T>(): PendingRuntimeSetting<T> {
return { kind: "unchanged" };
}
export function setPendingRuntimeSetting<T>(value: T): PendingRuntimeSetting<T> {
return { kind: "set", value };
}
export function resetRuntimeSettingToConfig<T>(): PendingRuntimeSetting<T> {
return { kind: "resetToConfig" };
}
export function nextCollaborationMode(mode: CollaborationMode): CollaborationMode {
return mode === "plan" ? "default" : "plan";
}
export function collaborationModeLabel(mode: CollaborationMode): string {
return mode === "plan" ? "Plan" : "Default";
}
export function pendingRuntimeSettingLabel<T>(setting: PendingRuntimeSetting<T>): string {
if (setting.kind === "set") return String(setting.value);
if (setting.kind === "resetToConfig") return "(reset to config)";
return "(none)";
}

View file

@ -1,5 +1,30 @@
import type { ChatState } from "../state/reducer";
import type { RuntimeSnapshot } from "./model";
import type { ActivePermissionProfile, RuntimeConfigSnapshot } from "../../../app-server/runtime-config";
import type { RateLimitSnapshot, ThreadTokenUsage } from "../../../app-server/runtime-metrics";
import type { ApprovalPolicy, ApprovalsReviewer, ServiceTier } from "../../../app-server/runtime-policy";
import type { ModelMetadata, ReasoningEffort } from "../../../domain/catalog/metadata";
import type { CollaborationMode, PendingRuntimeSetting, RequestedServiceTier } from "./settings";
export interface RuntimeSnapshot {
runtimeConfig: RuntimeConfigSnapshot | null;
activeThreadId: string | null;
activeModel: string | null;
activeReasoningEffort: ReasoningEffort | null;
activeCollaborationMode: CollaborationMode;
activeServiceTier: ServiceTier | null;
activeApprovalPolicy: ApprovalPolicy | null;
activeApprovalsReviewer: ApprovalsReviewer | null;
activePermissionProfile: ActivePermissionProfile | null;
requestedModel: PendingRuntimeSetting<string>;
requestedReasoningEffort: PendingRuntimeSetting<ReasoningEffort>;
requestedApprovalsReviewer: PendingRuntimeSetting<ApprovalsReviewer>;
selectedCollaborationMode: CollaborationMode;
requestedServiceTier: PendingRuntimeSetting<RequestedServiceTier>;
tokenUsage: ThreadTokenUsage | null;
rateLimit: RateLimitSnapshot | null;
hasThreadTurns: boolean;
availableModels: readonly ModelMetadata[];
}
interface RuntimeSnapshotInput {
runtimeConfig: ChatState["connection"]["runtimeConfig"];

View file

@ -14,7 +14,7 @@ import {
type CollaborationMode,
type PendingRuntimeSetting,
type RequestedServiceTier,
} from "./model";
} from "./settings";
export interface ChatRuntimeState {
activeModel: string | null;

View file

@ -1,8 +1,9 @@
import { applyThreadSettingsValue, appServerCollaborationMode, type ThreadSettingsUpdate } from "../../../app-server/thread-settings";
import type { ServiceTierRequest } from "../../../app-server/thread-settings";
import { currentModel, currentReasoningEffort, fastServiceTierRequestValue } from "./effective-settings";
import { currentModel, currentReasoningEffort, fastServiceTierRequestValue } from "./effective";
import type { RuntimeConfigSnapshot } from "../../../app-server/runtime-config";
import type { PendingRuntimeSetting, RuntimeSnapshot } from "./model";
import type { RuntimeSnapshot } from "./snapshot";
import type { PendingRuntimeSetting } from "./settings";
export type TurnCollaborationModeWarning = "missing-model";

View file

@ -3,7 +3,7 @@ import type { Thread } from "../../../domain/threads/model";
import { parseServiceTier, type ServiceTier } from "../../../app-server/runtime-policy";
import { normalizeReasoningEffort, type ReasoningEffort } from "../../../domain/catalog/metadata";
import type { ChatRuntimeState } from "../runtime/state";
import type { CollaborationMode } from "../runtime/model";
import type { CollaborationMode } from "../runtime/settings";
import type { DisplayItem } from "../display/types";
import type { PendingTurnStart } from "../conversation/turns/turn-state";

View file

@ -9,7 +9,7 @@ import type { Diagnostics } from "../../../app-server/diagnostics";
import { createAppServerDiagnostics } from "../../../app-server/diagnostics";
import type { RuntimeConfigSnapshot } from "../../../app-server/runtime-config";
import type { RateLimitSnapshot, ThreadTokenUsage } from "../../../app-server/runtime-metrics";
import type { CollaborationMode } from "../runtime/model";
import type { CollaborationMode } from "../runtime/settings";
import {
commitPendingThreadSettingsRuntimeState,
clearRequestedApprovalsReviewerRuntimeState,
@ -25,8 +25,8 @@ import {
setSelectedCollaborationModeRuntimeState,
type ChatRuntimeState,
} from "../runtime/state";
import type { RequestedServiceTier } from "../runtime/model";
import type { RequestId } from "../protocol/requests/model";
import type { RequestedServiceTier } from "../runtime/settings";
import type { RequestId } from "../../../app-server/types";
import type { ComposerSuggestion } from "../conversation/composer/suggestions";
import { upsertDisplayItem } from "../display/stream-updates";
import type { DisplayItem } from "../display/types";
@ -53,7 +53,7 @@ import {
resolveChatRequest,
type ChatRequestState,
type RequestAction,
} from "../pending-requests/state";
} from "../conversation/pending-requests/state";
import {
initialChatTurnState,
transitionChatTurnLifecycleState,

View file

@ -1,18 +1,9 @@
import { activeTurnId as selectActiveTurnId, chatTurnBusy, pendingTurnStart } from "./reducer";
import type { ChatState, PendingTurnStart } from "./reducer";
import type { Thread } from "../../../domain/threads/model";
import type { PendingApproval } from "../protocol/requests/approval";
import type { PendingUserInput } from "../protocol/requests/user-input";
import type { DisplayItem } from "../display/types";
import { implementPlanCandidateFromState } from "../display/action-candidates";
export interface PendingRequestSnapshot {
approvals: readonly PendingApproval[];
pendingUserInputs: readonly PendingUserInput[];
userInputDrafts: ReadonlyMap<string, string>;
openDetails: ReadonlySet<string>;
}
export interface SubmissionStateSnapshot {
activeThreadId: string | null;
activeTurnId: string | null;
@ -22,15 +13,6 @@ export interface SubmissionStateSnapshot {
pendingTurnStart: PendingTurnStart | null;
}
export function pendingRequestSnapshot(state: ChatState): PendingRequestSnapshot {
return {
approvals: state.requests.approvals,
pendingUserInputs: state.requests.pendingUserInputs,
userInputDrafts: state.requests.userInputDrafts,
openDetails: state.ui.openDetails,
};
}
export function activeThreadId(state: ChatState): string | null {
return state.activeThread.id;
}

View file

@ -5,12 +5,11 @@ import type { ArchiveExportAdapter } from "../../../domain/threads/export";
import { createChatThreadActions } from "./thread-actions";
import { createChatThreadGoalActions } from "./thread-goal-actions";
import { ThreadHistoryController } from "./thread-history-controller";
import { createThreadIdentityActions } from "./thread-identity-actions";
import { createThreadIdentitySync } from "./thread-identity-sync";
import { ThreadRenameController } from "./thread-rename-controller";
import { ThreadResumeController } from "./thread-resume-controller";
import { createThreadSelectionActions } from "./thread-selection-controller";
import { createThreadSelectionActions } from "./thread-selection-actions";
import { RestoredThreadController } from "./restored-thread-controller";
import type { ToolbarPanelController } from "../panel/regions/toolbar";
import type { ChatStateStore } from "../state/reducer";
import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../lifecycle";
import type { CodexPanelSettings } from "../../../settings/model";
@ -169,7 +168,7 @@ export function createThreadControllerGroup(
return response?.dataBase64 ?? "";
}),
});
const threadIdentity = createThreadIdentityActions({
const threadIdentity = createThreadIdentitySync({
stateStore,
restoredThread,
invalidateResumeWork,
@ -198,7 +197,7 @@ function requireThreadController<T>(controller: T | null, name: string): T {
return controller;
}
interface ThreadSelectionControllerGroupPorts {
interface ThreadSelectionActionGroupPorts {
plugin: {
focusThreadInOpenView: (threadId: string) => Promise<boolean>;
};
@ -213,10 +212,10 @@ interface ThreadSelectionControllerGroupPorts {
};
}
export function createThreadSelectionControllerGroup(
context: ThreadSelectionControllerGroupPorts,
export function createThreadSelectionActionGroup(
context: ThreadSelectionActionGroupPorts,
refs: {
toolbarPanels: ToolbarPanelController;
closeForThreadSelection: () => void;
},
) {
const { plugin, thread, status } = context;
@ -225,7 +224,7 @@ export function createThreadSelectionControllerGroup(
const threadSelection = createThreadSelectionActions({
stateStore,
closeForThreadSelection: () => {
refs.toolbarPanels.closeForThreadSelection();
refs.closeForThreadSelection();
},
focusThreadInOpenView: (threadId) => plugin.focusThreadInOpenView(threadId),
resumeThread: thread.resumeThread,

View file

@ -0,0 +1,41 @@
import type { AppServerClient } from "../../../app-server/client";
import type { ArchiveExportAdapter } from "../../../domain/threads/export";
import type { CodexPanelSettings } from "../../../settings/model";
import type { ChatAction, ChatState, ChatStateStore } from "../state/reducer";
export interface ChatThreadActionsHost {
stateStore: ChatStateStore;
vaultPath: string;
settings: () => CodexPanelSettings;
archiveAdapter: () => ArchiveExportAdapter;
ensureConnected: () => Promise<void>;
currentClient: () => AppServerClient | null;
addSystemMessage: (text: string) => void;
setStatus: (status: string) => void;
setComposerText: (text: string) => void;
render: () => void;
openThreadInNewView: (threadId: string) => Promise<unknown>;
openThreadInCurrentPanel: (threadId: string) => Promise<void>;
notifyThreadArchived: (threadId: string) => void;
notifyThreadRenamed: (threadId: string, name: string) => void;
notifyActiveThreadIdentityChanged: () => void;
refreshThreads: () => Promise<void>;
refreshSharedThreadListFromOpenSurface: () => void;
}
export function threadActionState(host: ChatThreadActionsHost): ChatState {
return host.stateStore.getState();
}
export function threadActionDispatch(host: ChatThreadActionsHost, action: ChatAction): void {
host.stateStore.dispatch(action);
}
export function threadActionStillTargetsPanel(state: ChatState, threadId: string): boolean {
return state.activeThread.id === threadId;
}
export function threadActionStillTargetsOriginalPanel(state: ChatState, initialThreadId: string | null, threadId: string): boolean {
if (!initialThreadId) return true;
return initialThreadId === threadId && state.activeThread.id === threadId;
}

View file

@ -1,42 +1,18 @@
import { Notice } from "obsidian";
import { archiveThread } from "./thread-archive-actions";
import { compactThread } from "./thread-compact-actions";
import type { ChatThreadActionsHost } from "./thread-action-context";
import { forkThread, forkThreadFromTurn } from "./thread-fork-actions";
import { renameThread } from "./thread-rename-actions";
import { rollbackThread } from "./thread-rollback-actions";
import type { AppServerClient } from "../../../app-server/client";
import { threadFromAppServerThread } from "../../../app-server/thread-model";
import { transcriptEntriesFromAppServerTurn } from "../../../app-server/turn-model";
import { exportArchivedThreadMarkdown } from "../../../domain/threads/export";
import type { ArchiveExportAdapter } from "../../../domain/threads/export";
import { inheritedForkThreadName } from "../../../domain/threads/model";
import type { CodexPanelSettings } from "../../../settings/model";
import { chatTurnBusy, type ChatAction, type ChatState, type ChatStateStore } from "../state/reducer";
import { rollbackCandidateFromItems, turnsAfterTurnId } from "../display/action-candidates";
import { displayItemsFromTurns } from "../protocol/display-items";
import { resumedThreadActionFromActiveRuntime } from "./thread-resume";
export interface ChatThreadActionsHost {
stateStore: ChatStateStore;
vaultPath: string;
settings: () => CodexPanelSettings;
archiveAdapter: () => ArchiveExportAdapter;
ensureConnected: () => Promise<void>;
currentClient: () => AppServerClient | null;
addSystemMessage: (text: string) => void;
setStatus: (status: string) => void;
setComposerText: (text: string) => void;
render: () => void;
openThreadInNewView: (threadId: string) => Promise<unknown>;
openThreadInCurrentPanel: (threadId: string) => Promise<void>;
notifyThreadArchived: (threadId: string) => void;
notifyThreadRenamed: (threadId: string, name: string) => void;
notifyActiveThreadIdentityChanged: () => void;
refreshThreads: () => Promise<void>;
refreshSharedThreadListFromOpenSurface: () => void;
}
export type { ChatThreadActionsHost } from "./thread-action-context";
export interface ChatThreadActions {
compactThread: (threadId: string) => Promise<void>;
archiveThread: (threadId: string, saveMarkdown?: boolean) => Promise<void>;
forkThread: (threadId: string) => Promise<void>;
forkThreadFromTurn: (threadId: string, turnId: string | null, archiveSource: boolean) => Promise<void>;
renameThread: (threadId: string, name: string) => Promise<boolean>;
rollbackThread: (threadId: string) => Promise<void>;
}
@ -46,194 +22,7 @@ export function createChatThreadActions(host: ChatThreadActionsHost): ChatThread
archiveThread: (threadId, saveMarkdown) => archiveThread(host, threadId, saveMarkdown),
forkThread: (threadId) => forkThread(host, threadId),
forkThreadFromTurn: (threadId, turnId, archiveSource) => forkThreadFromTurn(host, threadId, turnId, archiveSource),
renameThread: (threadId, name) => renameThread(host, threadId, name),
rollbackThread: (threadId) => rollbackThread(host, threadId),
};
}
async function compactThread(host: ChatThreadActionsHost, threadId: string): Promise<void> {
await host.ensureConnected();
const client = host.currentClient();
if (!client) return;
const initialActiveThreadId = state(host).activeThread.id;
try {
await client.compactThread(threadId);
if (!threadActionStillTargetsOriginalPanel(state(host), initialActiveThreadId, threadId)) return;
host.addSystemMessage("Compaction requested.");
host.setStatus("Compaction requested.");
} catch (error) {
host.addSystemMessage(error instanceof Error ? error.message : String(error));
}
}
async function archiveThread(
host: ChatThreadActionsHost,
threadId: string,
saveMarkdown = host.settings().archiveExportEnabled,
): Promise<void> {
if (await archiveThreadOnServer(host, threadId, saveMarkdown)) {
host.notifyThreadArchived(threadId);
}
}
async function archiveThreadOnServer(
host: ChatThreadActionsHost,
threadId: string,
saveMarkdown = host.settings().archiveExportEnabled,
): Promise<boolean> {
if (chatTurnBusy(state(host))) {
host.addSystemMessage("Finish or interrupt the current turn before archiving threads.");
return false;
}
await host.ensureConnected();
const client = host.currentClient();
if (!client) return false;
try {
const settings = host.settings();
if (saveMarkdown) {
const response = await client.readThread(threadId, true);
const result = await exportArchivedThreadMarkdown(
{
...threadFromAppServerThread(response.thread, { archived: true }),
transcriptEntries: response.thread.turns.flatMap(transcriptEntriesFromAppServerTurn),
},
{ ...settings, vaultPath: host.vaultPath },
host.archiveAdapter(),
);
new Notice(`Saved archived thread to ${result.path}.`);
}
await client.archiveThread(threadId);
return true;
} catch (error) {
host.addSystemMessage(error instanceof Error ? error.message : String(error));
return false;
}
}
function forkThread(host: ChatThreadActionsHost, threadId: string): Promise<void> {
return forkThreadFromTurn(host, threadId, null, false);
}
async function forkThreadFromTurn(
host: ChatThreadActionsHost,
threadId: string,
turnId: string | null,
archiveSource: boolean,
): Promise<void> {
if (chatTurnBusy(state(host))) {
host.addSystemMessage("Finish or interrupt the current turn before forking threads.");
return;
}
await host.ensureConnected();
const client = host.currentClient();
if (!client) return;
const initialActiveThreadId = state(host).activeThread.id;
const turnsToDrop = turnId ? turnsAfterTurnId(state(host).transcript.displayItems, turnId) : 0;
if (turnsToDrop === null) {
host.addSystemMessage("Could not find the selected turn to fork.");
return;
}
try {
const sourceName = inheritedForkThreadName(threadId, state(host).threadList.listedThreads);
const response = await client.forkThread(threadId, host.vaultPath);
const forkedThreadId = response.thread.id;
if (turnsToDrop > 0) {
await client.rollbackThread(forkedThreadId, turnsToDrop);
}
if (!threadActionStillTargetsOriginalPanel(state(host), initialActiveThreadId, threadId)) return;
if (sourceName) {
try {
await client.setThreadName(forkedThreadId, sourceName);
host.notifyThreadRenamed(forkedThreadId, sourceName);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
host.addSystemMessage(`Forked thread ${forkedThreadId}, but could not copy the source thread name: ${message}`);
}
}
if (archiveSource) {
if (!(await archiveThreadOnServer(host, threadId))) return;
try {
await host.openThreadInCurrentPanel(forkedThreadId);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
host.addSystemMessage(`Archived thread ${threadId}, but could not open forked thread ${forkedThreadId}: ${message}`);
}
host.notifyThreadArchived(threadId);
return;
}
try {
await host.openThreadInNewView(forkedThreadId);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
host.addSystemMessage(`Forked thread ${forkedThreadId}, but could not open it in a new panel: ${message}`);
}
} catch (error) {
host.addSystemMessage(error instanceof Error ? error.message : String(error));
}
}
async function rollbackThread(host: ChatThreadActionsHost, threadId: string): Promise<void> {
if (chatTurnBusy(state(host))) {
host.addSystemMessage("Interrupt the current turn before rolling back.");
return;
}
await host.ensureConnected();
const client = host.currentClient();
if (!client) return;
const candidate = rollbackCandidateFromItems(state(host).transcript.displayItems);
if (!candidate) {
host.addSystemMessage("No completed turn to roll back.");
return;
}
try {
host.setStatus("Rolling back latest turn...");
const response = await client.rollbackThread(threadId);
if (!threadActionStillTargetsPanel(state(host), threadId)) return;
const thread = threadFromAppServerThread(response.thread);
dispatch(
host,
resumedThreadActionFromActiveRuntime({
thread,
cwd: response.thread.cwd,
runtime: state(host).runtime,
listedThreads: state(host).threadList.listedThreads,
}),
);
dispatch(host, {
type: "transcript/items-replaced",
items: displayItemsFromTurns(response.thread.turns),
historyCursor: null,
loadingHistory: false,
});
host.setComposerText(candidate.text);
host.addSystemMessage("Rolled back the latest turn. Local file changes were not reverted.");
host.render();
host.setStatus("Rolled back latest turn.");
host.notifyActiveThreadIdentityChanged();
await host.refreshThreads();
host.refreshSharedThreadListFromOpenSurface();
} catch (error) {
host.addSystemMessage(error instanceof Error ? error.message : String(error));
host.setStatus("Rollback failed.");
}
}
function state(host: ChatThreadActionsHost): ChatState {
return host.stateStore.getState();
}
function threadActionStillTargetsPanel(state: ChatState, threadId: string): boolean {
return state.activeThread.id === threadId;
}
function threadActionStillTargetsOriginalPanel(state: ChatState, initialThreadId: string | null, threadId: string): boolean {
if (!initialThreadId) return true;
return initialThreadId === threadId && state.activeThread.id === threadId;
}
function dispatch(host: ChatThreadActionsHost, action: ChatAction): void {
host.stateStore.dispatch(action);
}

View file

@ -0,0 +1,52 @@
import { Notice } from "obsidian";
import { threadFromAppServerThread } from "../../../app-server/thread-model";
import { transcriptEntriesFromAppServerTurn } from "../../../app-server/turn-model";
import { exportArchivedThreadMarkdown } from "../../../domain/threads/export";
import { chatTurnBusy } from "../state/reducer";
import type { ChatThreadActionsHost } from "./thread-action-context";
import { threadActionState } from "./thread-action-context";
export async function archiveThread(
host: ChatThreadActionsHost,
threadId: string,
saveMarkdown = host.settings().archiveExportEnabled,
): Promise<void> {
if (await archiveThreadOnServer(host, threadId, saveMarkdown)) {
host.notifyThreadArchived(threadId);
}
}
export async function archiveThreadOnServer(
host: ChatThreadActionsHost,
threadId: string,
saveMarkdown = host.settings().archiveExportEnabled,
): Promise<boolean> {
if (chatTurnBusy(threadActionState(host))) {
host.addSystemMessage("Finish or interrupt the current turn before archiving threads.");
return false;
}
await host.ensureConnected();
const client = host.currentClient();
if (!client) return false;
try {
const settings = host.settings();
if (saveMarkdown) {
const response = await client.readThread(threadId, true);
const result = await exportArchivedThreadMarkdown(
{
...threadFromAppServerThread(response.thread, { archived: true }),
transcriptEntries: response.thread.turns.flatMap(transcriptEntriesFromAppServerTurn),
},
{ ...settings, vaultPath: host.vaultPath },
host.archiveAdapter(),
);
new Notice(`Saved archived thread to ${result.path}.`);
}
await client.archiveThread(threadId);
return true;
} catch (error) {
host.addSystemMessage(error instanceof Error ? error.message : String(error));
return false;
}
}

View file

@ -0,0 +1,17 @@
import type { ChatThreadActionsHost } from "./thread-action-context";
import { threadActionState, threadActionStillTargetsOriginalPanel } from "./thread-action-context";
export async function compactThread(host: ChatThreadActionsHost, threadId: string): Promise<void> {
await host.ensureConnected();
const client = host.currentClient();
if (!client) return;
const initialActiveThreadId = threadActionState(host).activeThread.id;
try {
await client.compactThread(threadId);
if (!threadActionStillTargetsOriginalPanel(threadActionState(host), initialActiveThreadId, threadId)) return;
host.addSystemMessage("Compaction requested.");
host.setStatus("Compaction requested.");
} catch (error) {
host.addSystemMessage(error instanceof Error ? error.message : String(error));
}
}

View file

@ -0,0 +1,70 @@
import { inheritedForkThreadName } from "../../../domain/threads/model";
import { chatTurnBusy } from "../state/reducer";
import { turnsAfterTurnId } from "../display/action-candidates";
import { archiveThreadOnServer } from "./thread-archive-actions";
import type { ChatThreadActionsHost } from "./thread-action-context";
import { threadActionState, threadActionStillTargetsOriginalPanel } from "./thread-action-context";
export function forkThread(host: ChatThreadActionsHost, threadId: string): Promise<void> {
return forkThreadFromTurn(host, threadId, null, false);
}
export async function forkThreadFromTurn(
host: ChatThreadActionsHost,
threadId: string,
turnId: string | null,
archiveSource: boolean,
): Promise<void> {
if (chatTurnBusy(threadActionState(host))) {
host.addSystemMessage("Finish or interrupt the current turn before forking threads.");
return;
}
await host.ensureConnected();
const client = host.currentClient();
if (!client) return;
const initialActiveThreadId = threadActionState(host).activeThread.id;
const turnsToDrop = turnId ? turnsAfterTurnId(threadActionState(host).transcript.displayItems, turnId) : 0;
if (turnsToDrop === null) {
host.addSystemMessage("Could not find the selected turn to fork.");
return;
}
try {
const sourceName = inheritedForkThreadName(threadId, threadActionState(host).threadList.listedThreads);
const response = await client.forkThread(threadId, host.vaultPath);
const forkedThreadId = response.thread.id;
if (turnsToDrop > 0) {
await client.rollbackThread(forkedThreadId, turnsToDrop);
}
if (!threadActionStillTargetsOriginalPanel(threadActionState(host), initialActiveThreadId, threadId)) return;
if (sourceName) {
try {
await client.setThreadName(forkedThreadId, sourceName);
host.notifyThreadRenamed(forkedThreadId, sourceName);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
host.addSystemMessage(`Forked thread ${forkedThreadId}, but could not copy the source thread name: ${message}`);
}
}
if (archiveSource) {
if (!(await archiveThreadOnServer(host, threadId))) return;
try {
await host.openThreadInCurrentPanel(forkedThreadId);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
host.addSystemMessage(`Archived thread ${threadId}, but could not open forked thread ${forkedThreadId}: ${message}`);
}
host.notifyThreadArchived(threadId);
return;
}
try {
await host.openThreadInNewView(forkedThreadId);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
host.addSystemMessage(`Forked thread ${forkedThreadId}, but could not open it in a new panel: ${message}`);
}
} catch (error) {
host.addSystemMessage(error instanceof Error ? error.message : String(error));
}
}

View file

@ -2,7 +2,7 @@ import type { RestoredThreadController } from "./restored-thread-controller";
import { activeThreadId, listedThreads } from "../state/selectors";
import type { ChatStateStore } from "../state/reducer";
export interface ThreadIdentityActionsHost {
export interface ThreadIdentitySyncHost {
stateStore: ChatStateStore;
restoredThread: RestoredThreadController;
invalidateResumeWork: () => void;
@ -14,13 +14,13 @@ export interface ThreadIdentityActionsHost {
render: () => void;
}
export interface ThreadIdentityActions {
export interface ThreadIdentitySync {
clearActiveThreadContext: () => void;
notifyThreadArchived: (threadId: string) => void;
notifyThreadRenamed: (threadId: string, name: string | null) => void;
}
export function createThreadIdentityActions(host: ThreadIdentityActionsHost): ThreadIdentityActions {
export function createThreadIdentitySync(host: ThreadIdentitySyncHost): ThreadIdentitySync {
return {
clearActiveThreadContext: () => {
clearActiveThreadContext(host);
@ -34,7 +34,7 @@ export function createThreadIdentityActions(host: ThreadIdentityActionsHost): Th
};
}
function clearActiveThreadContext(host: ThreadIdentityActionsHost): void {
function clearActiveThreadContext(host: ThreadIdentitySyncHost): void {
host.invalidateResumeWork();
host.restoredThread.clear();
host.clearDeferredRestoredThreadHydration();
@ -44,13 +44,13 @@ function clearActiveThreadContext(host: ThreadIdentityActionsHost): void {
host.refreshLiveState();
}
function notifyThreadArchived(host: ThreadIdentityActionsHost, threadId: string): void {
function notifyThreadArchived(host: ThreadIdentitySyncHost, threadId: string): void {
if (activeThreadId(host.stateStore.getState()) !== threadId) return;
clearActiveThreadContext(host);
host.render();
}
function notifyThreadRenamed(host: ThreadIdentityActionsHost, threadId: string, name: string | null): void {
function notifyThreadRenamed(host: ThreadIdentitySyncHost, threadId: string, name: string | null): void {
let changed = false;
const renamedThreads = listedThreads(host.stateStore.getState()).map((thread) => {
if (thread.id !== threadId) return thread;

View file

@ -0,0 +1,41 @@
import type { AppServerClient } from "../../../app-server/client";
import type { ChatStateStore } from "../state/reducer";
export interface ThreadRenameActionsHost {
stateStore: ChatStateStore;
ensureConnected: () => Promise<void>;
currentClient: () => AppServerClient | null;
addSystemMessage: (text: string) => void;
notifyThreadRenamed: (threadId: string, name: string) => void;
render: () => void;
}
export async function renameThread(host: ThreadRenameActionsHost, threadId: string, value: string): Promise<boolean> {
const title = value.trim();
if (!title) return false;
await host.ensureConnected();
return renameConnectedThread(host, threadId, title);
}
export async function renameConnectedThread(host: ThreadRenameActionsHost, threadId: string, title: string): Promise<boolean> {
const client = host.currentClient();
if (!client) return false;
try {
await client.setThreadName(threadId, title);
host.stateStore.dispatch({
type: "thread-list/applied",
threads: host.stateStore
.getState()
.threadList.listedThreads.map((thread) => (thread.id === threadId ? { ...thread, name: title } : thread)),
});
host.notifyThreadRenamed(threadId, title);
return true;
} catch (error) {
host.addSystemMessage(error instanceof Error ? error.message : String(error));
return false;
} finally {
host.render();
}
}

View file

@ -13,6 +13,7 @@ import type { ChatAction, ChatState, ChatStateStore } from "../state/reducer";
import { generateThreadTitleWithCodex } from "../../../app-server/thread-title-generation";
import { completedConversationSummaryFromAppServerTurn } from "../../../app-server/turn-model";
import { firstNamingContextFromDisplayItems, namingContextFromDisplayItems } from "./thread-naming";
import { renameConnectedThread } from "./thread-rename-actions";
export interface ThreadRenameEditState {
draft: string;
@ -118,42 +119,8 @@ export class ThreadRenameController {
const client = this.host.currentClient();
if (!client) return;
try {
await client.setThreadName(threadId, title);
this.dispatch({
type: "thread-list/applied",
threads: this.state.threadList.listedThreads.map((thread) => (thread.id === threadId ? { ...thread, name: title } : thread)),
});
if (this.renameState === editingState) {
this.clear();
}
this.host.notifyThreadRenamed(threadId, title);
} catch (error) {
this.host.addSystemMessage(error instanceof Error ? error.message : String(error));
} finally {
this.host.render();
}
}
async rename(threadId: string, value: string): Promise<void> {
const title = value.trim();
if (!title) return;
await this.host.ensureConnected();
const client = this.host.currentClient();
if (!client) return;
try {
await client.setThreadName(threadId, title);
this.dispatch({
type: "thread-list/applied",
threads: this.state.threadList.listedThreads.map((thread) => (thread.id === threadId ? { ...thread, name: title } : thread)),
});
this.host.notifyThreadRenamed(threadId, title);
} catch (error) {
this.host.addSystemMessage(error instanceof Error ? error.message : String(error));
} finally {
this.host.render();
if (await renameConnectedThread(this.host, threadId, title)) {
if (this.renameState === editingState) this.clear();
}
}

View file

@ -0,0 +1,55 @@
import { threadFromAppServerThread } from "../../../app-server/thread-model";
import { rollbackCandidateFromItems } from "../display/action-candidates";
import { displayItemsFromTurns } from "../protocol/display-items";
import { chatTurnBusy } from "../state/reducer";
import type { ChatThreadActionsHost } from "./thread-action-context";
import { threadActionDispatch, threadActionState, threadActionStillTargetsPanel } from "./thread-action-context";
import { resumedThreadActionFromActiveRuntime } from "./thread-resume";
export async function rollbackThread(host: ChatThreadActionsHost, threadId: string): Promise<void> {
if (chatTurnBusy(threadActionState(host))) {
host.addSystemMessage("Interrupt the current turn before rolling back.");
return;
}
await host.ensureConnected();
const client = host.currentClient();
if (!client) return;
const candidate = rollbackCandidateFromItems(threadActionState(host).transcript.displayItems);
if (!candidate) {
host.addSystemMessage("No completed turn to roll back.");
return;
}
try {
host.setStatus("Rolling back latest turn...");
const response = await client.rollbackThread(threadId);
if (!threadActionStillTargetsPanel(threadActionState(host), threadId)) return;
const thread = threadFromAppServerThread(response.thread);
threadActionDispatch(
host,
resumedThreadActionFromActiveRuntime({
thread,
cwd: response.thread.cwd,
runtime: threadActionState(host).runtime,
listedThreads: threadActionState(host).threadList.listedThreads,
}),
);
threadActionDispatch(host, {
type: "transcript/items-replaced",
items: displayItemsFromTurns(response.thread.turns),
historyCursor: null,
loadingHistory: false,
});
host.setComposerText(candidate.text);
host.addSystemMessage("Rolled back the latest turn. Local file changes were not reverted.");
host.render();
host.setStatus("Rolled back latest turn.");
host.notifyActiveThreadIdentityChanged();
await host.refreshThreads();
host.refreshSharedThreadListFromOpenSurface();
} catch (error) {
host.addSystemMessage(error instanceof Error ? error.message : String(error));
host.setStatus("Rollback failed.");
}
}

View file

@ -1,7 +1,7 @@
import { canSwitchToThread } from "../state/selectors";
import type { ChatStateStore } from "../state/reducer";
export interface ThreadSelectionControllerHost {
export interface ThreadSelectionActionsHost {
stateStore: ChatStateStore;
closeForThreadSelection: () => void;
focusThreadInOpenView: (threadId: string) => Promise<boolean>;
@ -14,7 +14,7 @@ export interface ThreadSelectionActions {
selectThreadFromToolbar(threadId: string): Promise<void>;
}
export function createThreadSelectionActions(host: ThreadSelectionControllerHost): ThreadSelectionActions {
export function createThreadSelectionActions(host: ThreadSelectionActionsHost): ThreadSelectionActions {
const selectThread = async (threadId: string): Promise<void> => {
if (!canSwitchToThread(host.stateStore.getState(), threadId)) {
host.addSystemMessage("Finish or interrupt the current turn before switching threads.");

View file

@ -1,5 +1,4 @@
import type { ChatState } from "../../state/reducer";
import type { PendingRequestSnapshot } from "../../state/selectors";
import { chatTurnBusy } from "../../state/reducer";
import type { DisplayItem } from "../../display/types";
import {
@ -9,33 +8,8 @@ import {
isRollbackCandidateItem,
rollbackCandidateFromItems,
} from "../../display/action-candidates";
import type { ChatTurnDiffViewState } from "../../turn-diff/model";
import type { PendingRequestMessageActions } from "../pending-request-message";
import type { MessageStreamContext } from "./context";
export interface ChatMessageStreamActionPort {
rollbackThread: (threadId: string) => void;
forkThreadFromTurn: (threadId: string, turnId: string, archiveSource: boolean) => void;
implementPlan: (item: DisplayItem) => void;
openTurnDiff: (state: ChatTurnDiffViewState) => void;
}
export interface ChatMessageStreamRequestPort {
pendingSignature: () => string;
pendingSnapshot: () => PendingRequestSnapshot;
pendingActions: () => PendingRequestMessageActions;
consumePendingAutoFocus: () => boolean;
}
export interface ChatMessageStreamContextPort {
vaultPath: string;
setOpenDetail: (key: string, open: boolean) => void;
loadOlderTurns: () => void;
renderMarkdown: (element: HTMLElement, text: string) => void;
copyMessageText: (text: string) => void;
actions: ChatMessageStreamActionPort;
requests: ChatMessageStreamRequestPort;
}
import type { ChatMessageStreamContextPort } from "./ports";
export function createMessageStreamContext(state: ChatState, port: ChatMessageStreamContextPort): MessageStreamContext {
const busy = chatTurnBusy(state);

View file

@ -1,7 +1,7 @@
import type { ComponentChild as UiNode } from "preact";
import type { ChatTurnLifecycleState } from "../../state/reducer";
import type { PendingRequestSnapshot } from "../../state/selectors";
import type { PendingRequestBlockSnapshot } from "../../conversation/pending-requests/snapshot";
import type { DisplayItem } from "../../display/types";
import type { PendingRequestMessageActions } from "../pending-request-message";
import type { ChatTurnDiffViewState } from "../../turn-diff/model";
@ -57,7 +57,7 @@ export interface MessageStreamContext extends MessageStreamLayoutContext, Messag
export interface PendingRequestBlockContext {
signature: string;
snapshot: () => PendingRequestSnapshot;
snapshot: () => PendingRequestBlockSnapshot;
actions: () => PendingRequestMessageActions;
consumeAutoFocus: () => boolean;
}

View file

@ -1,5 +1,32 @@
import type { ChatAction, ChatState } from "../../state/reducer";
import type { ChatMessageStreamActionPort, ChatMessageStreamContextPort, ChatMessageStreamRequestPort } from "./view-model";
import type { PendingRequestBlockSnapshot } from "../../conversation/pending-requests/snapshot";
import type { DisplayItem } from "../../display/types";
import type { ChatTurnDiffViewState } from "../../turn-diff/model";
import type { PendingRequestMessageActions } from "../pending-request-message";
export interface ChatMessageStreamActionPort {
rollbackThread: (threadId: string) => void;
forkThreadFromTurn: (threadId: string, turnId: string, archiveSource: boolean) => void;
implementPlan: (item: DisplayItem) => void;
openTurnDiff: (state: ChatTurnDiffViewState) => void;
}
export interface ChatMessageStreamRequestPort {
pendingSignature: () => string;
pendingSnapshot: () => PendingRequestBlockSnapshot;
pendingActions: () => PendingRequestMessageActions;
consumePendingAutoFocus: () => boolean;
}
export interface ChatMessageStreamContextPort {
vaultPath: string;
setOpenDetail: (key: string, open: boolean) => void;
loadOlderTurns: () => void;
renderMarkdown: (element: HTMLElement, text: string) => void;
copyMessageText: (text: string) => void;
actions: ChatMessageStreamActionPort;
requests: ChatMessageStreamRequestPort;
}
export interface MessageStreamContextPortOptions {
vaultPath: string;

View file

@ -1,7 +1,8 @@
import type { ChatState } from "../../state/reducer";
import type { MessageStreamScrollIntent, MessageStreamVirtualizerHandle } from "../message-virtualizer";
import { messageStreamBlocks } from "./stream-blocks";
import { createMessageStreamContext, type ChatMessageStreamContextPort } from "./view-model";
import { createMessageStreamContext } from "./context-model";
import type { ChatMessageStreamContextPort } from "./ports";
import type { MessageStreamRenderState } from "./render";
export interface MessageStreamRenderStateOptions {

View file

@ -5,7 +5,7 @@ import { copyTextWithNotice } from "../../../../shared/ui/clipboard";
import type { ChatAction, ChatState, ChatStateStore } from "../../state/reducer";
import type { ComposerBoundaryScrollAction } from "../../conversation/composer/boundary-scroll";
import type { MessageStreamScrollIntent, MessageStreamVirtualizerHandle } from "../message-virtualizer";
import type { ChatMessageStreamActionPort, ChatMessageStreamContextPort, ChatMessageStreamRequestPort } from "./view-model";
import type { ChatMessageStreamActionPort, ChatMessageStreamContextPort, ChatMessageStreamRequestPort } from "./ports";
import { createMessageStreamContextPort } from "./ports";
import { MarkdownMessageRenderer } from "./markdown-renderer";
import { messageStreamBlocksNode, type MessageStreamRenderState } from "./render";

View file

@ -6,7 +6,8 @@ import type { DisplayDetailSection, DisplayItem } from "./display/types";
import type { ReasoningEffort } from "../../domain/catalog/metadata";
import type { ModelMetadata } from "../../domain/catalog/metadata";
import type { Thread } from "../../domain/threads/model";
import { collaborationModeLabel as formatCollaborationModeLabel, type RuntimeSnapshot } from "./runtime/model";
import type { RuntimeSnapshot } from "./runtime/snapshot";
import { collaborationModeLabel as formatCollaborationModeLabel } from "./runtime/settings";
import { chatTurnBusy, createChatStateStore, type ChatState, type ChatAction } from "./state/reducer";
import type { OpenCodexPanelSnapshot } from "../../workspace/open-panel-snapshot";
import type { SharedAppServerMetadata } from "../../app-server/shared-cache-state";

View file

@ -2,10 +2,10 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { scheduleAppServerWarmup } from "../../../../src/features/chat/connection/app-server-warmup-controller";
import { scheduleAppServerWarmup } from "../../../../src/features/chat/connection/app-server-warmup";
import { ChatViewDeferredTasks } from "../../../../src/features/chat/lifecycle";
function createController({
function createWarmupHost({
opened = true,
closing = false,
connected = false,
@ -21,13 +21,13 @@ function createController({
return { host, ensureConnected };
}
describe("AppServerWarmupController", () => {
describe("scheduleAppServerWarmup", () => {
beforeEach(() => {
vi.useFakeTimers();
});
it("connects on the next tick when the opened view is disconnected", async () => {
const { host, ensureConnected } = createController();
const { host, ensureConnected } = createWarmupHost();
scheduleAppServerWarmup(host);
await vi.advanceTimersByTimeAsync(0);
@ -36,8 +36,8 @@ describe("AppServerWarmupController", () => {
});
it("does not schedule warmup when the view is closed or already connected", async () => {
const closed = createController({ opened: false });
const connected = createController({ connected: true });
const closed = createWarmupHost({ opened: false });
const connected = createWarmupHost({ connected: true });
scheduleAppServerWarmup(closed.host);
scheduleAppServerWarmup(connected.host);
@ -49,7 +49,7 @@ describe("AppServerWarmupController", () => {
});
it("skips a scheduled warmup if the view is closing", async () => {
const { host, ensureConnected } = createController({ closing: true });
const { host, ensureConnected } = createWarmupHost({ closing: true });
scheduleAppServerWarmup(host);
await vi.advanceTimersByTimeAsync(0);

View file

@ -1,15 +1,15 @@
import { describe, expect, it, vi } from "vitest";
import type { AppServerClient } from "../../../../src/app-server/client";
import type { AppServerMcpServerStatus } from "../../../../src/app-server/diagnostics";
import { emptyRuntimeConfigSnapshot } from "../../../../src/app-server/runtime-config";
import type { RateLimitSnapshot } from "../../../../src/app-server/runtime-metrics";
import { threadFromAppServerThread, type AppServerThread } from "../../../../src/app-server/thread-model";
import { createChatServerDiagnosticsActions } from "../../../../src/features/chat/protocol/client-actions/diagnostics-actions";
import { createChatServerMetadataActions } from "../../../../src/features/chat/protocol/client-actions/metadata-actions";
import { createChatServerThreadActions } from "../../../../src/features/chat/protocol/client-actions/thread-actions";
import { createChatState, createChatStateStore } from "../../../../src/features/chat/state/reducer";
import type { AppServerModel, AppServerSkillMetadata } from "../../../../src/app-server/catalog-model";
import type { AppServerClient } from "../../../../../src/app-server/client";
import type { AppServerMcpServerStatus } from "../../../../../src/app-server/diagnostics";
import { emptyRuntimeConfigSnapshot } from "../../../../../src/app-server/runtime-config";
import type { RateLimitSnapshot } from "../../../../../src/app-server/runtime-metrics";
import { threadFromAppServerThread, type AppServerThread } from "../../../../../src/app-server/thread-model";
import { createChatServerDiagnosticsActions } from "../../../../../src/features/chat/connection/server-actions/diagnostics";
import { createChatServerMetadataActions } from "../../../../../src/features/chat/connection/server-actions/metadata";
import { createChatServerThreadActions } from "../../../../../src/features/chat/connection/server-actions/threads";
import { createChatState, createChatStateStore } from "../../../../../src/features/chat/state/reducer";
import type { AppServerModel, AppServerSkillMetadata } from "../../../../../src/app-server/catalog-model";
describe("chat server actions", () => {
it("publishes newly started threads before the first turn completes", async () => {

View file

@ -1,10 +1,10 @@
import { describe, expect, it, vi } from "vitest";
import { ChatInboundController } from "../../../../src/features/chat/protocol/inbound/controller";
import { createChatState, createChatStateStore } from "../../../../src/features/chat/state/reducer";
import { PendingRequestController } from "../../../../src/features/chat/pending-requests/controller";
import { toPendingUserInput } from "../../../../src/features/chat/protocol/requests/user-input";
import type { ServerRequest } from "../../../../src/app-server/types";
import { ChatInboundController } from "../../../../../src/features/chat/protocol/inbound/controller";
import { createChatState, createChatStateStore } from "../../../../../src/features/chat/state/reducer";
import { PendingRequestController } from "../../../../../src/features/chat/conversation/pending-requests/controller";
import { toPendingUserInput } from "../../../../../src/features/chat/protocol/requests/user-input";
import type { ServerRequest } from "../../../../../src/app-server/types";
function expectPresent<T>(value: T | null | undefined): T {
if (value === null || value === undefined) throw new Error("Expected value to be present");

View file

@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest";
import { initialChatRequestState, resolveChatRequest, type ChatRequestState } from "../../../../src/features/chat/pending-requests/state";
import {
initialChatRequestState,
resolveChatRequest,
type ChatRequestState,
} from "../../../../../src/features/chat/conversation/pending-requests/state";
describe("chat pending request state", () => {
it("ignores stale request resolutions", () => {

View file

@ -1,9 +1,9 @@
import { describe, expect, it, vi } from "vitest";
import { applyChatViewState, type ChatViewStateControllerHost } from "../../../../src/features/chat/panel/view-state-controller";
import { applyChatViewState, type ChatViewStateHost } from "../../../../src/features/chat/panel/view-state";
function createController(overrides: Partial<ChatViewStateControllerHost> = {}) {
const host: ChatViewStateControllerHost = {
function createHost(overrides: Partial<ChatViewStateHost> = {}) {
const host: ChatViewStateHost = {
invalidateResumeWork: vi.fn(),
clearRestoredThreadLifecycle: vi.fn(),
clearDeferredRestoredThreadHydration: vi.fn(),
@ -14,9 +14,9 @@ function createController(overrides: Partial<ChatViewStateControllerHost> = {})
return { host };
}
describe("ChatViewStateController", () => {
describe("applyChatViewState", () => {
it("restores a thread placeholder from persisted view state", () => {
const { host } = createController();
const { host } = createHost();
applyChatViewState(host, { threadId: "thread", threadTitle: "Title" });
@ -30,7 +30,7 @@ describe("ChatViewStateController", () => {
});
it("clears restored lifecycle and schedules warmup when no thread is restored", () => {
const { host } = createController();
const { host } = createHost();
applyChatViewState(host, { version: 1 });

View file

@ -6,7 +6,10 @@ import {
toPendingUserInput,
userInputResponse,
} from "../../../../../src/features/chat/protocol/requests/user-input";
import { pendingRequestFocusSignature, pendingRequestsSignature } from "../../../../../src/features/chat/pending-requests/view-model";
import {
pendingRequestFocusSignature,
pendingRequestsSignature,
} from "../../../../../src/features/chat/conversation/pending-requests/signatures";
import type { ServerRequest } from "../../../../../src/app-server/types";
function expectPresent<T>(value: T | null | undefined): T {

View file

@ -1,9 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import {
createChatRuntimeSettingsActions,
type ChatRuntimeSettingsActions,
} from "../../../../src/features/chat/runtime/runtime-settings-actions";
import { createChatRuntimeSettingsActions, type ChatRuntimeSettingsActions } from "../../../../src/features/chat/runtime/settings-actions";
import { createChatState, createChatStateStore, type ChatState } from "../../../../src/features/chat/state/reducer";
import { runtimeSnapshotForChatState } from "../../../../src/features/chat/runtime/snapshot";
import type { ActiveThreadSettingsAppliedAction } from "../../../../src/features/chat/state/actions";

View file

@ -249,6 +249,33 @@ describe("createChatThreadActions", () => {
expect(host.notifyThreadArchived).not.toHaveBeenCalled();
});
it("renames a thread and notifies shared surfaces", async () => {
const client = clientMock();
const host = hostMock({ client, displayItems: [] });
host.stateStore.dispatch({ type: "thread-list/applied", threads: [{ ...panelThread("thread"), name: "Old" }] });
const controller = createChatThreadActions(host);
await expect(controller.renameThread("thread", " Slash command title ")).resolves.toBe(true);
expect(host.ensureConnected).toHaveBeenCalledOnce();
expect(client.setThreadName).toHaveBeenCalledWith("thread", "Slash command title");
expect(host.stateStore.getState().threadList.listedThreads[0]?.name).toBe("Slash command title");
expect(host.notifyThreadRenamed).toHaveBeenCalledWith("thread", "Slash command title");
expect(host.render).toHaveBeenCalledOnce();
});
it("ignores empty thread rename titles", async () => {
const client = clientMock();
const host = hostMock({ client, displayItems: [] });
const controller = createChatThreadActions(host);
await expect(controller.renameThread("thread", " ")).resolves.toBe(false);
expect(host.ensureConnected).not.toHaveBeenCalled();
expect(client.setThreadName).not.toHaveBeenCalled();
expect(host.notifyThreadRenamed).not.toHaveBeenCalled();
});
it("applies rollback response turns directly before refreshing the shell", async () => {
const client = clientMock();
const host = hostMock({ client, displayItems: turnItems() });

View file

@ -1,7 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { createChatState, createChatStateStore } from "../../../../src/features/chat/state/reducer";
import { createThreadIdentityActions } from "../../../../src/features/chat/threads/thread-identity-actions";
import { createThreadIdentitySync } from "../../../../src/features/chat/threads/thread-identity-sync";
import type { RestoredThreadController } from "../../../../src/features/chat/threads/restored-thread-controller";
import type { RestoredThreadPlaceholderState } from "../../../../src/features/chat/lifecycle";
import type { Thread } from "../../../../src/domain/threads/model";
@ -38,10 +38,10 @@ function createController() {
refreshLiveState: vi.fn(),
render: vi.fn(),
};
return { controller: createThreadIdentityActions(host), host, restoredPlaceholder, restoredRename, stateStore };
return { controller: createThreadIdentitySync(host), host, restoredPlaceholder, restoredRename, stateStore };
}
describe("createThreadIdentityActions", () => {
describe("createThreadIdentitySync", () => {
it("clears the active thread when it is archived", () => {
const { controller, host, stateStore } = createController();
stateStore.dispatch({

View file

@ -105,33 +105,6 @@ describe("ThreadRenameController", () => {
expect(controller.editState("thread")).toEqual({ draft: "New draft", generating: false });
});
it("renames a thread without entering inline edit state", async () => {
const setThreadName = vi.fn().mockResolvedValue({});
const client = fakeClient({ setThreadName });
const { controller, notifyThreadRenamed, stateStore } = controllerFixture({
currentClient: () => client,
});
await controller.rename("thread", " Slash command title ");
expect(setThreadName).toHaveBeenCalledWith("thread", "Slash command title");
expect(stateStore.getState().threadList.listedThreads[0]?.name).toBe("Slash command title");
expect(notifyThreadRenamed).toHaveBeenCalledWith("thread", "Slash command title");
expect(controller.editState("thread")).toBeNull();
});
it("ignores empty slash command rename titles", async () => {
const setThreadName = vi.fn().mockResolvedValue({});
const client = fakeClient({ setThreadName });
const { controller } = controllerFixture({
currentClient: () => client,
});
await controller.rename("thread", " ");
expect(setThreadName).not.toHaveBeenCalled();
});
it("keeps an edited draft when auto-name generation finishes later", async () => {
const generatedTitle = deferred<string | null>();
const { controller } = controllerFixture({

View file

@ -3,8 +3,8 @@ import { describe, expect, it, vi } from "vitest";
import { createChatState, createChatStateStore, type ChatStateStore } from "../../../../src/features/chat/state/reducer";
import {
createThreadSelectionActions,
type ThreadSelectionControllerHost,
} from "../../../../src/features/chat/threads/thread-selection-controller";
type ThreadSelectionActionsHost,
} from "../../../../src/features/chat/threads/thread-selection-actions";
function resumeThreadState(stateStore: ChatStateStore, threadId: string): void {
stateStore.dispatch({
@ -20,9 +20,9 @@ function resumeThreadState(stateStore: ChatStateStore, threadId: string): void {
});
}
function createController(overrides: Partial<ThreadSelectionControllerHost> = {}) {
function createController(overrides: Partial<ThreadSelectionActionsHost> = {}) {
const stateStore = createChatStateStore(createChatState());
const host: ThreadSelectionControllerHost = {
const host: ThreadSelectionActionsHost = {
stateStore,
closeForThreadSelection: vi.fn(),
focusThreadInOpenView: vi.fn().mockResolvedValue(false),
@ -33,7 +33,7 @@ function createController(overrides: Partial<ThreadSelectionControllerHost> = {}
return { controller: createThreadSelectionActions(host), host, stateStore };
}
describe("ThreadSelectionController", () => {
describe("ThreadSelectionActions", () => {
it("focuses an already open thread without resuming it", async () => {
const { controller, host } = createController({
focusThreadInOpenView: vi.fn().mockResolvedValue(true),

View file

@ -2,7 +2,7 @@
import { describe, expect, it, vi } from "vitest";
import type { PendingRequestSnapshot } from "../../../../../src/features/chat/state/selectors";
import type { PendingRequestBlockSnapshot } from "../../../../../src/features/chat/conversation/pending-requests/snapshot";
import type { PendingApproval } from "../../../../../src/features/chat/protocol/requests/approval";
import type { PendingUserInput } from "../../../../../src/features/chat/protocol/requests/user-input";
import type { PendingRequestBlockContext } from "../../../../../src/features/chat/ui/message-stream/context";
@ -527,7 +527,7 @@ describe("pending request renderer decisions", () => {
renderMarkdown: (element, text) => element.createDiv({ text }),
pendingRequests: pendingRequestContext({
signature: "approval:1",
snapshot: emptyPendingRequestSnapshot({ approvals: [pendingApproval()] }),
snapshot: emptyPendingRequestBlockSnapshot({ approvals: [pendingApproval()] }),
consumeAutoFocus,
}),
}),
@ -542,7 +542,7 @@ describe("pending request renderer decisions", () => {
});
it("does not build pending request nodes when no pending block is inserted", () => {
const pendingSnapshot = vi.fn(() => emptyPendingRequestSnapshot());
const pendingSnapshot = vi.fn(() => emptyPendingRequestBlockSnapshot());
const consumeAutoFocus = vi.fn(() => true);
const blocks = messageStreamBlocks({
@ -587,7 +587,7 @@ describe("pending request renderer decisions", () => {
...baseContext,
pendingRequests: pendingRequestContext({
signature: "request:1",
snapshot: emptyPendingRequestSnapshot({ approvals: [pendingApproval()] }),
snapshot: emptyPendingRequestBlockSnapshot({ approvals: [pendingApproval()] }),
}),
}),
);
@ -603,13 +603,13 @@ describe("pending request renderer decisions", () => {
function pendingRequestContext(options: {
signature: string;
snapshot?: PendingRequestSnapshot | (() => PendingRequestSnapshot);
snapshot?: PendingRequestBlockSnapshot | (() => PendingRequestBlockSnapshot);
actions?: ReturnType<typeof pendingRequestActions>;
consumeAutoFocus?: () => boolean;
}): PendingRequestBlockContext {
const snapshot = options.snapshot;
const snapshotFn =
typeof snapshot === "function" ? (snapshot as () => PendingRequestSnapshot) : () => snapshot ?? emptyPendingRequestSnapshot();
typeof snapshot === "function" ? (snapshot as () => PendingRequestBlockSnapshot) : () => snapshot ?? emptyPendingRequestBlockSnapshot();
return {
signature: options.signature,
snapshot: snapshotFn,
@ -618,7 +618,7 @@ function pendingRequestContext(options: {
};
}
function emptyPendingRequestSnapshot(overrides: Partial<PendingRequestSnapshot> = {}): PendingRequestSnapshot {
function emptyPendingRequestBlockSnapshot(overrides: Partial<PendingRequestBlockSnapshot> = {}): PendingRequestBlockSnapshot {
return {
approvals: [],
pendingUserInputs: [],

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { collaborationModeLabel, nextCollaborationMode } from "../../src/features/chat/runtime/model";
import { collaborationModeLabel, nextCollaborationMode } from "../../src/features/chat/runtime/settings";
describe("runtime collaboration mode", () => {
it("toggles between Default and Plan mode", () => {

View file

@ -26,13 +26,14 @@ import {
runtimeConfigOrDefault,
serviceTierLabel,
supportedReasoningEfforts,
} from "../../src/features/chat/runtime/effective-settings";
import { resetRuntimeSettingToConfig, setPendingRuntimeSetting, type RuntimeSnapshot } from "../../src/features/chat/runtime/model";
} from "../../src/features/chat/runtime/effective";
import type { RuntimeSnapshot } from "../../src/features/chat/runtime/snapshot";
import { resetRuntimeSettingToConfig, setPendingRuntimeSetting } from "../../src/features/chat/runtime/settings";
import {
pendingThreadSettingsUpdate,
requestedTurnCollaborationModeSettings,
serviceTierRequestForThreadStart,
} from "../../src/features/chat/runtime/turn-settings";
} from "../../src/features/chat/runtime/thread-settings";
import { contextSummary, runtimeConfigSections, rateLimitSummary } from "../../src/features/chat/display/runtime-status";
describe("runtime settings", () => {

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { isFastServiceTier } from "../../src/features/chat/runtime/effective-settings";
import { isFastServiceTier } from "../../src/features/chat/runtime/effective";
describe("service tier runtime state", () => {
it("recognizes Codex fast tier aliases without rejecting other tier ids", () => {