mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Normalize chat conversation and runtime modules
This commit is contained in:
parent
806a3f44e6
commit
230f35627f
69 changed files with 637 additions and 593 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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"];
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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: {
|
||||
|
|
@ -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 = "";
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 })),
|
||||
});
|
||||
}
|
||||
19
src/features/chat/conversation/pending-requests/snapshot.ts
Normal file
19
src/features/chat/conversation/pending-requests/snapshot.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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[];
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export type RequestId = string | number;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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)";
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
||||
29
src/features/chat/runtime/settings.ts
Normal file
29
src/features/chat/runtime/settings.ts
Normal 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)";
|
||||
}
|
||||
|
|
@ -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"];
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
type CollaborationMode,
|
||||
type PendingRuntimeSetting,
|
||||
type RequestedServiceTier,
|
||||
} from "./model";
|
||||
} from "./settings";
|
||||
|
||||
export interface ChatRuntimeState {
|
||||
activeModel: string | null;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
41
src/features/chat/threads/thread-action-context.ts
Normal file
41
src/features/chat/threads/thread-action-context.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
52
src/features/chat/threads/thread-archive-actions.ts
Normal file
52
src/features/chat/threads/thread-archive-actions.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
17
src/features/chat/threads/thread-compact-actions.ts
Normal file
17
src/features/chat/threads/thread-compact-actions.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
70
src/features/chat/threads/thread-fork-actions.ts
Normal file
70
src/features/chat/threads/thread-fork-actions.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
41
src/features/chat/threads/thread-rename-actions.ts
Normal file
41
src/features/chat/threads/thread-rename-actions.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
55
src/features/chat/threads/thread-rollback-actions.ts
Normal file
55
src/features/chat/threads/thread-rollback-actions.ts
Normal 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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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 () => {
|
||||
|
|
@ -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");
|
||||
|
|
@ -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", () => {
|
||||
|
|
@ -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 });
|
||||
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -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() });
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
@ -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: [],
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue