mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Narrow chat submission controller dependencies
This commit is contained in:
parent
cd43d0c96a
commit
bfe03c2fae
9 changed files with 339 additions and 155 deletions
|
|
@ -165,45 +165,65 @@ export function createChatViewControllerAssembly(host: ChatViewControllerAssembl
|
|||
});
|
||||
turnSubmission = new TurnSubmissionController({
|
||||
state: submissionState,
|
||||
vaultPath: host.plugin.vaultPath,
|
||||
currentClient: host.getClient,
|
||||
ensureRestoredThreadLoaded: host.ensureRestoredThreadLoaded,
|
||||
startThread: (preview) => appServer.startThread(preview),
|
||||
notifyActiveThreadIdentityChanged: host.effects.thread.notifyIdentityChanged,
|
||||
resetThreadTurnPresence: host.effects.thread.resetTurnPresence,
|
||||
applyPendingThreadSettings: () => runtimeSettings.applyPendingThreadSettings(),
|
||||
codexInput: (text) => composerController.codexInput(text),
|
||||
setDraft: (text, options) => {
|
||||
composerController.setDraft(text, options);
|
||||
connection: {
|
||||
vaultPath: host.plugin.vaultPath,
|
||||
currentClient: host.getClient,
|
||||
},
|
||||
restoredThread: {
|
||||
ensureRestoredThreadLoaded: host.ensureRestoredThreadLoaded,
|
||||
},
|
||||
thread: {
|
||||
startThread: (preview) => appServer.startThread(preview),
|
||||
notifyActiveThreadIdentityChanged: host.effects.thread.notifyIdentityChanged,
|
||||
resetThreadTurnPresence: host.effects.thread.resetTurnPresence,
|
||||
},
|
||||
runtime: {
|
||||
applyPendingThreadSettings: () => runtimeSettings.applyPendingThreadSettings(),
|
||||
},
|
||||
composer: {
|
||||
codexInput: (text) => composerController.codexInput(text),
|
||||
setDraft: (text, options) => {
|
||||
composerController.setDraft(text, options);
|
||||
},
|
||||
},
|
||||
view: {
|
||||
forceMessagesToBottom: host.effects.scroll.forceBottom,
|
||||
render: host.effects.render.now,
|
||||
scheduleRender: host.effects.render.schedule,
|
||||
},
|
||||
status: {
|
||||
setStatus: host.effects.status.set,
|
||||
addSystemMessage: host.effects.status.addSystemMessage,
|
||||
},
|
||||
forceMessagesToBottom: host.effects.scroll.forceBottom,
|
||||
render: host.effects.render.now,
|
||||
scheduleRender: host.effects.render.schedule,
|
||||
setStatus: host.effects.status.set,
|
||||
addSystemMessage: host.effects.status.addSystemMessage,
|
||||
});
|
||||
slashCommands = new SlashCommandController({
|
||||
state: submissionState,
|
||||
currentClient: host.getClient,
|
||||
codexInput: (text) => composerController.codexInput(text),
|
||||
startNewThread: host.startNewThread,
|
||||
resumeThread: host.selectThread,
|
||||
forkThread: (threadId) => threadActions.forkThread(threadId),
|
||||
rollbackThread: (threadId) => threadActions.rollbackThread(threadId),
|
||||
archiveThread: (threadId) => threadActions.archiveThread(threadId),
|
||||
toggleFastMode: () => runtimeSettings.toggleFastMode(),
|
||||
toggleCollaborationMode: () => runtimeSettings.toggleCollaborationMode(),
|
||||
toggleAutoReview: () => void runtimeSettings.toggleAutoReview(),
|
||||
addSystemMessage: host.effects.status.addSystemMessage,
|
||||
addStructuredSystemMessage: host.effects.status.addStructuredSystemMessage,
|
||||
setStatus: host.effects.status.set,
|
||||
setRequestedModel: (model) => runtimeSettings.setRequestedModel(model),
|
||||
setRequestedReasoningEffort: (effort) => runtimeSettings.setRequestedReasoningEffort(effort),
|
||||
statusSummaryLines: host.statusSummaryLines,
|
||||
connectionDiagnosticDetails: host.connectionDiagnosticDetails,
|
||||
mcpStatusLines: host.mcpStatusLines,
|
||||
modelStatusLines: host.modelStatusLines,
|
||||
effortStatusLines: host.effortStatusLines,
|
||||
threads: {
|
||||
startNewThread: host.startNewThread,
|
||||
resumeThread: host.selectThread,
|
||||
forkThread: (threadId) => threadActions.forkThread(threadId),
|
||||
rollbackThread: (threadId) => threadActions.rollbackThread(threadId),
|
||||
archiveThread: (threadId) => threadActions.archiveThread(threadId),
|
||||
},
|
||||
runtime: {
|
||||
toggleFastMode: () => runtimeSettings.toggleFastMode(),
|
||||
toggleCollaborationMode: () => runtimeSettings.toggleCollaborationMode(),
|
||||
toggleAutoReview: () => void runtimeSettings.toggleAutoReview(),
|
||||
setRequestedModel: (model) => runtimeSettings.setRequestedModel(model),
|
||||
setRequestedReasoningEffort: (effort) => runtimeSettings.setRequestedReasoningEffort(effort),
|
||||
},
|
||||
status: {
|
||||
addSystemMessage: host.effects.status.addSystemMessage,
|
||||
addStructuredSystemMessage: host.effects.status.addStructuredSystemMessage,
|
||||
setStatus: host.effects.status.set,
|
||||
statusSummaryLines: host.statusSummaryLines,
|
||||
connectionDiagnosticDetails: host.connectionDiagnosticDetails,
|
||||
mcpStatusLines: host.mcpStatusLines,
|
||||
modelStatusLines: host.modelStatusLines,
|
||||
effortStatusLines: host.effortStatusLines,
|
||||
},
|
||||
});
|
||||
messageRenderer = new ChatMessageRenderer({
|
||||
app: host.app,
|
||||
|
|
@ -245,19 +265,27 @@ export function createChatViewControllerAssembly(host: ChatViewControllerAssembl
|
|||
composer: composerController,
|
||||
slashCommands,
|
||||
turnSubmission,
|
||||
currentClient: host.getClient,
|
||||
ensureConnected: host.effects.client.ensureConnected,
|
||||
setStatus: host.effects.status.set,
|
||||
addSystemMessage: host.effects.status.addSystemMessage,
|
||||
connection: {
|
||||
currentClient: host.getClient,
|
||||
ensureConnected: host.effects.client.ensureConnected,
|
||||
},
|
||||
status: {
|
||||
setStatus: host.effects.status.set,
|
||||
addSystemMessage: host.effects.status.addSystemMessage,
|
||||
},
|
||||
});
|
||||
serverRequestResponder = new ServerRequestResponder({
|
||||
currentClient: host.getClient,
|
||||
});
|
||||
planImplementation = new PlanImplementationController({
|
||||
state: submissionState,
|
||||
currentClient: host.getClient,
|
||||
ensureConnected: host.effects.client.ensureConnected,
|
||||
sendTurnText: (text) => turnSubmission.sendTurnText(text),
|
||||
connection: {
|
||||
currentClient: host.getClient,
|
||||
ensureConnected: host.effects.client.ensureConnected,
|
||||
},
|
||||
submission: {
|
||||
sendTurnText: (text) => turnSubmission.sendTurnText(text),
|
||||
},
|
||||
});
|
||||
const connection = new ConnectionManager(() => host.plugin.settings.codexPath, host.plugin.vaultPath, {
|
||||
onNotification: (notification) => {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,41 @@
|
|||
import type { AppServerClient } from "../../../../app-server/client";
|
||||
import { parseSlashCommand } from "../../composer/suggestions";
|
||||
import type { ChatComposerController } from "../../chat-composer-controller";
|
||||
import type { SlashCommandController } from "./slash-command-controller";
|
||||
import type { TurnSubmissionController } from "./turn-submission-controller";
|
||||
import type { SlashCommandExecutionResult } from "../../slash-commands";
|
||||
import type { SlashCommandName } from "../../composer/slash-commands";
|
||||
import type { ReferencedThreadDisplay } from "../../../../domain/threads/reference";
|
||||
import type { UserInput } from "../../../../generated/app-server/v2/UserInput";
|
||||
import type { SubmissionStatePort } from "../state-ports";
|
||||
|
||||
export interface ComposerDraftPort {
|
||||
readonly trimmedDraft: string;
|
||||
setDraft(text: string, options?: { clearSuggestions?: boolean; focus?: boolean }): void;
|
||||
}
|
||||
|
||||
export interface ComposerSlashCommandPort {
|
||||
execute(command: SlashCommandName, args: string): Promise<SlashCommandExecutionResult | undefined>;
|
||||
}
|
||||
|
||||
export interface ComposerTurnSubmissionPort {
|
||||
sendTurnText(text: string, codexInputOverride?: UserInput[], referencedThread?: ReferencedThreadDisplay): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ComposerConnectionPort {
|
||||
ensureConnected: () => Promise<void>;
|
||||
currentClient: () => AppServerClient | null;
|
||||
}
|
||||
|
||||
export interface ComposerStatusPort {
|
||||
setStatus: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
}
|
||||
|
||||
export interface ComposerSubmissionControllerHost {
|
||||
state: SubmissionStatePort;
|
||||
composer: ChatComposerController;
|
||||
slashCommands: SlashCommandController;
|
||||
turnSubmission: TurnSubmissionController;
|
||||
currentClient: () => AppServerClient | null;
|
||||
ensureConnected: () => Promise<void>;
|
||||
setStatus: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
composer: ComposerDraftPort;
|
||||
slashCommands: ComposerSlashCommandPort;
|
||||
turnSubmission: ComposerTurnSubmissionPort;
|
||||
connection: ComposerConnectionPort;
|
||||
status: ComposerStatusPort;
|
||||
}
|
||||
|
||||
export class ComposerSubmissionController {
|
||||
|
|
@ -33,8 +55,8 @@ export class ComposerSubmissionController {
|
|||
const text = this.host.composer.trimmedDraft;
|
||||
if (!text) return;
|
||||
|
||||
await this.host.ensureConnected();
|
||||
if (!this.host.currentClient()) return;
|
||||
await this.host.connection.ensureConnected();
|
||||
if (!this.host.connection.currentClient()) return;
|
||||
|
||||
const slashCommand = parseSlashCommand(text);
|
||||
if (slashCommand) {
|
||||
|
|
@ -52,13 +74,13 @@ export class ComposerSubmissionController {
|
|||
private async interruptTurn(): Promise<void> {
|
||||
const state = this.host.state.snapshot();
|
||||
const turnId = state.activeTurnId;
|
||||
const client = this.host.currentClient();
|
||||
const client = this.host.connection.currentClient();
|
||||
if (!client || !state.activeThreadId || !turnId) return;
|
||||
try {
|
||||
await client.interruptTurn(state.activeThreadId, turnId);
|
||||
this.host.setStatus("Interrupt requested.");
|
||||
this.host.status.setStatus("Interrupt requested.");
|
||||
} catch (error) {
|
||||
this.host.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
this.host.status.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,19 @@ import type { SubmissionStatePort } from "../state-ports";
|
|||
|
||||
const IMPLEMENT_PLAN_PROMPT = "Please implement this plan.";
|
||||
|
||||
export interface PlanImplementationConnectionPort {
|
||||
currentClient(): AppServerClient | null;
|
||||
ensureConnected(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PlanImplementationSubmissionPort {
|
||||
sendTurnText(text: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PlanImplementationControllerHost {
|
||||
state: SubmissionStatePort;
|
||||
currentClient: () => AppServerClient | null;
|
||||
ensureConnected: () => Promise<void>;
|
||||
sendTurnText: (text: string) => Promise<void>;
|
||||
connection: PlanImplementationConnectionPort;
|
||||
submission: PlanImplementationSubmissionPort;
|
||||
}
|
||||
|
||||
export class PlanImplementationController {
|
||||
|
|
@ -20,10 +28,10 @@ export class PlanImplementationController {
|
|||
|
||||
async implement(item: DisplayItem): Promise<void> {
|
||||
if (!this.canImplement(item)) return;
|
||||
await this.host.ensureConnected();
|
||||
if (!this.host.currentClient() || !this.host.state.snapshot().activeThreadId) return;
|
||||
await this.host.connection.ensureConnected();
|
||||
if (!this.host.connection.currentClient() || !this.host.state.snapshot().activeThreadId) return;
|
||||
|
||||
this.host.state.prepareImplementationTurn();
|
||||
await this.host.sendTurnText(IMPLEMENT_PLAN_PROMPT);
|
||||
await this.host.submission.sendTurnText(IMPLEMENT_PLAN_PROMPT);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,23 +12,26 @@ import type { ReasoningEffort } from "../../../../generated/app-server/Reasoning
|
|||
import type { UserInput } from "../../../../generated/app-server/v2/UserInput";
|
||||
import type { SubmissionStatePort } from "../state-ports";
|
||||
|
||||
export interface SlashCommandControllerHost {
|
||||
state: SubmissionStatePort;
|
||||
currentClient: () => AppServerClient | null;
|
||||
codexInput: (text: string) => UserInput[];
|
||||
export interface SlashCommandThreadPort {
|
||||
startNewThread: () => Promise<void>;
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
forkThread: (threadId: string) => Promise<void>;
|
||||
rollbackThread: (threadId: string) => Promise<void>;
|
||||
archiveThread: (threadId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface SlashCommandRuntimePort {
|
||||
toggleFastMode: () => void | Promise<void>;
|
||||
toggleCollaborationMode: () => void | Promise<void>;
|
||||
toggleAutoReview: () => void | Promise<void>;
|
||||
setRequestedModel: (model: string | null) => boolean | undefined | Promise<boolean | undefined>;
|
||||
setRequestedReasoningEffort: (effort: ReasoningEffort | null) => boolean | undefined | Promise<boolean | undefined>;
|
||||
}
|
||||
|
||||
export interface SlashCommandStatusPort {
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: DisplayDetailSection[]) => void;
|
||||
setStatus: (status: string) => void;
|
||||
setRequestedModel: (model: string | null) => boolean | undefined | Promise<boolean | undefined>;
|
||||
setRequestedReasoningEffort: (effort: ReasoningEffort | null) => boolean | undefined | Promise<boolean | undefined>;
|
||||
statusSummaryLines: () => string[];
|
||||
connectionDiagnosticDetails: () => DisplayDetailSection[];
|
||||
mcpStatusLines: () => Promise<string[]>;
|
||||
|
|
@ -36,6 +39,15 @@ export interface SlashCommandControllerHost {
|
|||
effortStatusLines: () => string[];
|
||||
}
|
||||
|
||||
export interface SlashCommandControllerHost {
|
||||
state: SubmissionStatePort;
|
||||
currentClient: () => AppServerClient | null;
|
||||
codexInput: (text: string) => UserInput[];
|
||||
threads: SlashCommandThreadPort;
|
||||
runtime: SlashCommandRuntimePort;
|
||||
status: SlashCommandStatusPort;
|
||||
}
|
||||
|
||||
export class SlashCommandController {
|
||||
constructor(private readonly host: SlashCommandControllerHost) {}
|
||||
|
||||
|
|
@ -46,35 +58,35 @@ export class SlashCommandController {
|
|||
return runSlashCommand(command, args, {
|
||||
activeThreadId: state.activeThreadId,
|
||||
listedThreads: state.listedThreads,
|
||||
startNewThread: () => this.host.startNewThread(),
|
||||
resumeThread: (threadId) => this.host.resumeThread(threadId),
|
||||
startNewThread: () => this.host.threads.startNewThread(),
|
||||
resumeThread: (threadId) => this.host.threads.resumeThread(threadId),
|
||||
referThread: (thread, message) => this.referencedThreadInput(client, thread, message),
|
||||
forkThread: (threadId) => this.host.forkThread(threadId),
|
||||
rollbackThread: (threadId) => this.host.rollbackThread(threadId),
|
||||
forkThread: (threadId) => this.host.threads.forkThread(threadId),
|
||||
rollbackThread: (threadId) => this.host.threads.rollbackThread(threadId),
|
||||
compactThread: async (threadId) => {
|
||||
await client.compactThread(threadId);
|
||||
},
|
||||
archiveThread: (threadId) => this.host.archiveThread(threadId),
|
||||
archiveThread: (threadId) => this.host.threads.archiveThread(threadId),
|
||||
busy: state.busy,
|
||||
toggleFastMode: () => this.host.toggleFastMode(),
|
||||
toggleCollaborationMode: () => this.host.toggleCollaborationMode(),
|
||||
toggleAutoReview: () => this.host.toggleAutoReview(),
|
||||
toggleFastMode: () => this.host.runtime.toggleFastMode(),
|
||||
toggleCollaborationMode: () => this.host.runtime.toggleCollaborationMode(),
|
||||
toggleAutoReview: () => this.host.runtime.toggleAutoReview(),
|
||||
addSystemMessage: (text) => {
|
||||
this.host.addSystemMessage(text);
|
||||
this.host.status.addSystemMessage(text);
|
||||
},
|
||||
addStructuredSystemMessage: (text, details) => {
|
||||
this.host.addStructuredSystemMessage(text, details);
|
||||
this.host.status.addStructuredSystemMessage(text, details);
|
||||
},
|
||||
setStatus: (status) => {
|
||||
this.host.setStatus(status);
|
||||
this.host.status.setStatus(status);
|
||||
},
|
||||
setRequestedModel: (model) => this.host.setRequestedModel(model),
|
||||
setRequestedReasoningEffort: (effort) => this.host.setRequestedReasoningEffort(effort),
|
||||
statusSummaryLines: () => this.host.statusSummaryLines(),
|
||||
connectionDiagnosticDetails: () => this.host.connectionDiagnosticDetails(),
|
||||
mcpStatusLines: () => this.host.mcpStatusLines(),
|
||||
modelStatusLines: () => this.host.modelStatusLines(),
|
||||
effortStatusLines: () => this.host.effortStatusLines(),
|
||||
setRequestedModel: (model) => this.host.runtime.setRequestedModel(model),
|
||||
setRequestedReasoningEffort: (effort) => this.host.runtime.setRequestedReasoningEffort(effort),
|
||||
statusSummaryLines: () => this.host.status.statusSummaryLines(),
|
||||
connectionDiagnosticDetails: () => this.host.status.connectionDiagnosticDetails(),
|
||||
mcpStatusLines: () => this.host.status.mcpStatusLines(),
|
||||
modelStatusLines: () => this.host.status.modelStatusLines(),
|
||||
effortStatusLines: () => this.host.status.effortStatusLines(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -83,14 +95,14 @@ export class SlashCommandController {
|
|||
const response = await client.threadTurnsList(thread.id, null, REFERENCED_THREAD_TURN_LIMIT);
|
||||
const turns = referencedThreadTurns(response.data);
|
||||
if (turns.length === 0) {
|
||||
this.host.addSystemMessage("Referenced thread has no readable conversation turns.");
|
||||
this.host.status.addSystemMessage("Referenced thread has no readable conversation turns.");
|
||||
return null;
|
||||
}
|
||||
const reference = buildReferencedThreadInput(thread, turns, message, this.host.codexInput(message));
|
||||
this.host.setStatus(reference.status);
|
||||
this.host.status.setStatus(reference.status);
|
||||
return reference;
|
||||
} catch (error) {
|
||||
this.host.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
this.host.status.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,30 +10,58 @@ import {
|
|||
} from "./turn-submission";
|
||||
import type { SubmissionStatePort } from "../state-ports";
|
||||
|
||||
export interface TurnSubmissionControllerHost {
|
||||
state: SubmissionStatePort;
|
||||
export interface TurnSubmissionConnectionPort {
|
||||
vaultPath: string;
|
||||
currentClient: () => AppServerClient | null;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionRestoredThreadPort {
|
||||
ensureRestoredThreadLoaded: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionThreadPort {
|
||||
startThread: (preview?: string) => Promise<unknown>;
|
||||
notifyActiveThreadIdentityChanged: () => void;
|
||||
resetThreadTurnPresence: (hadTurns: boolean) => void;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionRuntimePort {
|
||||
applyPendingThreadSettings: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionComposerPort {
|
||||
codexInput: (text: string) => UserInput[];
|
||||
setDraft: (text: string, options?: { focus?: boolean; clearSuggestions?: boolean }) => void;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionViewPort {
|
||||
forceMessagesToBottom: () => void;
|
||||
render: () => void;
|
||||
scheduleRender: () => void;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionStatusPort {
|
||||
setStatus: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionControllerHost {
|
||||
state: SubmissionStatePort;
|
||||
connection: TurnSubmissionConnectionPort;
|
||||
restoredThread: TurnSubmissionRestoredThreadPort;
|
||||
thread: TurnSubmissionThreadPort;
|
||||
runtime: TurnSubmissionRuntimePort;
|
||||
composer: TurnSubmissionComposerPort;
|
||||
view: TurnSubmissionViewPort;
|
||||
status: TurnSubmissionStatusPort;
|
||||
}
|
||||
|
||||
export class TurnSubmissionController {
|
||||
constructor(private readonly host: TurnSubmissionControllerHost) {}
|
||||
|
||||
async sendTurnText(text: string, codexInputOverride?: UserInput[], referencedThread?: ReferencedThreadDisplay): Promise<void> {
|
||||
if (!(await this.host.ensureRestoredThreadLoaded())) return;
|
||||
const client = this.host.currentClient();
|
||||
if (!(await this.host.restoredThread.ensureRestoredThreadLoaded())) return;
|
||||
const client = this.host.connection.currentClient();
|
||||
if (!client) return;
|
||||
|
||||
if (this.state.busy) {
|
||||
|
|
@ -44,16 +72,16 @@ export class TurnSubmissionController {
|
|||
let optimisticUserId: string | null = null;
|
||||
try {
|
||||
if (!this.state.activeThreadId) {
|
||||
const threadResponse = await this.host.startThread(text);
|
||||
const threadResponse = await this.host.thread.startThread(text);
|
||||
if (!threadResponse) return;
|
||||
this.host.notifyActiveThreadIdentityChanged();
|
||||
this.host.resetThreadTurnPresence(false);
|
||||
this.host.thread.notifyActiveThreadIdentityChanged();
|
||||
this.host.thread.resetThreadTurnPresence(false);
|
||||
}
|
||||
const activeThreadId = this.state.activeThreadId;
|
||||
if (!activeThreadId) return;
|
||||
if (!(await this.host.applyPendingThreadSettings())) return;
|
||||
if (!(await this.host.runtime.applyPendingThreadSettings())) return;
|
||||
|
||||
const codexInput = codexInputOverride ?? this.host.codexInput(text);
|
||||
const codexInput = codexInputOverride ?? this.host.composer.codexInput(text);
|
||||
optimisticUserId = `local-user-${String(Date.now())}`;
|
||||
const optimistic = optimisticTurnStart({
|
||||
id: optimisticUserId,
|
||||
|
|
@ -62,11 +90,11 @@ export class TurnSubmissionController {
|
|||
referencedThread,
|
||||
});
|
||||
this.host.state.optimisticTurnStarted(optimistic.item, optimistic.pendingTurnStart);
|
||||
this.host.forceMessagesToBottom();
|
||||
this.host.setDraft("");
|
||||
this.host.render();
|
||||
this.host.view.forceMessagesToBottom();
|
||||
this.host.composer.setDraft("");
|
||||
this.host.view.render();
|
||||
|
||||
const response = await client.startTurn(activeThreadId, this.host.vaultPath, codexInput, optimisticUserId);
|
||||
const response = await client.startTurn(activeThreadId, this.host.connection.vaultPath, codexInput, optimisticUserId);
|
||||
const pendingStart = this.state.pendingTurnStart;
|
||||
if (
|
||||
shouldAcknowledgeTurnStart({
|
||||
|
|
@ -83,7 +111,7 @@ export class TurnSubmissionController {
|
|||
pendingTurnStart: pendingStart,
|
||||
});
|
||||
this.host.state.turnStartAcknowledged(response.turn.id, displayItems);
|
||||
this.host.setStatus("Turn running...");
|
||||
this.host.status.setStatus("Turn running...");
|
||||
}
|
||||
} catch (error) {
|
||||
const displayItems = cleanupFailedTurnStart({
|
||||
|
|
@ -92,10 +120,10 @@ export class TurnSubmissionController {
|
|||
pendingTurnStart: this.state.pendingTurnStart,
|
||||
});
|
||||
this.host.state.turnStartFailed(displayItems);
|
||||
this.host.setDraft(text);
|
||||
this.host.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
this.host.composer.setDraft(text);
|
||||
this.host.status.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
this.host.scheduleRender();
|
||||
this.host.view.scheduleRender();
|
||||
}
|
||||
|
||||
private async steerCurrentTurn(
|
||||
|
|
@ -107,13 +135,13 @@ export class TurnSubmissionController {
|
|||
const threadId = this.state.activeThreadId;
|
||||
const expectedTurnId = this.state.activeTurnId;
|
||||
if (!threadId || !expectedTurnId) {
|
||||
this.host.addSystemMessage("Current turn is not steerable yet.");
|
||||
this.host.status.addSystemMessage("Current turn is not steerable yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
const codexInput = codexInputOverride ?? this.host.codexInput(text);
|
||||
const codexInput = codexInputOverride ?? this.host.composer.codexInput(text);
|
||||
const localSteerId = `local-steer-${String(Date.now())}`;
|
||||
this.host.setDraft("", { clearSuggestions: true });
|
||||
this.host.composer.setDraft("", { clearSuggestions: true });
|
||||
|
||||
try {
|
||||
await client.steerTurn(threadId, expectedTurnId, codexInput, localSteerId);
|
||||
|
|
@ -126,14 +154,14 @@ export class TurnSubmissionController {
|
|||
codexInput,
|
||||
}),
|
||||
);
|
||||
this.host.forceMessagesToBottom();
|
||||
this.host.setStatus("Steered current turn.");
|
||||
this.host.view.forceMessagesToBottom();
|
||||
this.host.status.setStatus("Steered current turn.");
|
||||
} catch (error) {
|
||||
this.host.setDraft(text, { focus: true });
|
||||
this.host.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
this.host.composer.setDraft(text, { focus: true });
|
||||
this.host.status.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
||||
this.host.scheduleRender();
|
||||
this.host.view.scheduleRender();
|
||||
}
|
||||
|
||||
private get state() {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@ import type { AppServerClient } from "../../../../../src/app-server/client";
|
|||
import { createChatState, createChatStateStore } from "../../../../../src/features/chat/chat-state";
|
||||
import { ComposerSubmissionController } from "../../../../../src/features/chat/controllers/submission/composer-submission-controller";
|
||||
import { createSubmissionStatePort } from "../../../../../src/features/chat/controllers/state-ports";
|
||||
import type { ChatComposerController } from "../../../../../src/features/chat/chat-composer-controller";
|
||||
import type { SlashCommandController } from "../../../../../src/features/chat/controllers/submission/slash-command-controller";
|
||||
import type { TurnSubmissionController } from "../../../../../src/features/chat/controllers/submission/turn-submission-controller";
|
||||
import type { Thread } from "../../../../../src/generated/app-server/v2/Thread";
|
||||
|
||||
function thread(id: string): Thread {
|
||||
|
|
@ -47,13 +44,17 @@ function createController(draft: string) {
|
|||
return draft;
|
||||
},
|
||||
setDraft,
|
||||
} as unknown as ChatComposerController,
|
||||
slashCommands: { execute } as unknown as SlashCommandController,
|
||||
turnSubmission: { sendTurnText } as unknown as TurnSubmissionController,
|
||||
currentClient: () => client,
|
||||
ensureConnected: vi.fn().mockResolvedValue(undefined),
|
||||
setStatus: vi.fn(),
|
||||
addSystemMessage: vi.fn(),
|
||||
},
|
||||
slashCommands: { execute },
|
||||
turnSubmission: { sendTurnText },
|
||||
connection: {
|
||||
currentClient: () => client,
|
||||
ensureConnected: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
status: {
|
||||
setStatus: vi.fn(),
|
||||
addSystemMessage: vi.fn(),
|
||||
},
|
||||
});
|
||||
return { controller, execute, interruptTurn, sendTurnText, setDraft, stateStore };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,13 +37,19 @@ function resumeThread(stateStore: ChatStateStore, displayItems: readonly Display
|
|||
|
||||
function createController({ client = {} as AppServerClient } = {}) {
|
||||
const stateStore = createChatStateStore(createChatState());
|
||||
const ensureConnected = vi.fn().mockResolvedValue(undefined);
|
||||
const sendTurnText = vi.fn().mockResolvedValue(undefined);
|
||||
const host: PlanImplementationControllerHost = {
|
||||
state: createSubmissionStatePort(stateStore),
|
||||
currentClient: () => client,
|
||||
ensureConnected: vi.fn().mockResolvedValue(undefined),
|
||||
sendTurnText: vi.fn().mockResolvedValue(undefined),
|
||||
connection: {
|
||||
currentClient: () => client,
|
||||
ensureConnected,
|
||||
},
|
||||
submission: {
|
||||
sendTurnText,
|
||||
},
|
||||
};
|
||||
return { controller: new PlanImplementationController(host), host, stateStore };
|
||||
return { controller: new PlanImplementationController(host), ensureConnected, sendTurnText, stateStore };
|
||||
}
|
||||
|
||||
describe("PlanImplementationController", () => {
|
||||
|
|
@ -61,28 +67,28 @@ describe("PlanImplementationController", () => {
|
|||
});
|
||||
|
||||
it("switches out of plan mode and submits the implementation prompt", async () => {
|
||||
const { controller, host, stateStore } = createController();
|
||||
const { controller, ensureConnected, sendTurnText, stateStore } = createController();
|
||||
const plan = planItem("plan");
|
||||
resumeThread(stateStore, [plan]);
|
||||
stateStore.dispatch({ type: "ui/panel-set", panel: "model" });
|
||||
|
||||
await controller.implement(plan);
|
||||
|
||||
expect(host.ensureConnected).toHaveBeenCalledOnce();
|
||||
expect(ensureConnected).toHaveBeenCalledOnce();
|
||||
expect(stateStore.getState().selectedCollaborationMode).toBe("default");
|
||||
expect(stateStore.getState().runtimePicker).toBeNull();
|
||||
expect(host.sendTurnText).toHaveBeenCalledWith("Please implement this plan.");
|
||||
expect(sendTurnText).toHaveBeenCalledWith("Please implement this plan.");
|
||||
});
|
||||
|
||||
it("ignores stale plan items", async () => {
|
||||
const { controller, host, stateStore } = createController();
|
||||
const { controller, ensureConnected, sendTurnText, stateStore } = createController();
|
||||
const first = planItem("first");
|
||||
const latest = planItem("latest");
|
||||
resumeThread(stateStore, [first, latest]);
|
||||
|
||||
await controller.implement(first);
|
||||
|
||||
expect(host.ensureConnected).not.toHaveBeenCalled();
|
||||
expect(host.sendTurnText).not.toHaveBeenCalled();
|
||||
expect(ensureConnected).not.toHaveBeenCalled();
|
||||
expect(sendTurnText).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import { createChatState, createChatStateStore } from "../../../../../src/featur
|
|||
import {
|
||||
SlashCommandController,
|
||||
type SlashCommandControllerHost,
|
||||
type SlashCommandRuntimePort,
|
||||
type SlashCommandStatusPort,
|
||||
type SlashCommandThreadPort,
|
||||
} from "../../../../../src/features/chat/controllers/submission/slash-command-controller";
|
||||
import { createSubmissionStatePort } from "../../../../../src/features/chat/controllers/state-ports";
|
||||
import type { Thread } from "../../../../../src/generated/app-server/v2/Thread";
|
||||
|
|
@ -36,34 +39,53 @@ function thread(id: string, name: string | null = null): Thread {
|
|||
};
|
||||
}
|
||||
|
||||
function createHost(overrides: Partial<SlashCommandControllerHost> = {}) {
|
||||
interface SlashCommandHostOverrides extends Partial<Omit<SlashCommandControllerHost, "threads" | "runtime" | "status">> {
|
||||
threads?: Partial<SlashCommandThreadPort>;
|
||||
runtime?: Partial<SlashCommandRuntimePort>;
|
||||
status?: Partial<SlashCommandStatusPort>;
|
||||
}
|
||||
|
||||
function createHost(overrides: SlashCommandHostOverrides = {}) {
|
||||
const { threads: threadOverrides, runtime: runtimeOverrides, status: statusOverrides, ...hostOverrides } = overrides;
|
||||
const stateStore = createChatStateStore(createChatState());
|
||||
const compactThread = vi.fn().mockResolvedValue({});
|
||||
const threadTurnsList = vi.fn().mockResolvedValue({ data: [] });
|
||||
const client = { compactThread, threadTurnsList } as unknown as AppServerClient;
|
||||
const host: SlashCommandControllerHost = {
|
||||
state: createSubmissionStatePort(stateStore),
|
||||
currentClient: () => client,
|
||||
codexInput: vi.fn((text: string) => textInput(text)),
|
||||
const threads: SlashCommandThreadPort = {
|
||||
startNewThread: vi.fn().mockResolvedValue(undefined),
|
||||
resumeThread: vi.fn().mockResolvedValue(undefined),
|
||||
forkThread: vi.fn().mockResolvedValue(undefined),
|
||||
rollbackThread: vi.fn().mockResolvedValue(undefined),
|
||||
archiveThread: vi.fn().mockResolvedValue(undefined),
|
||||
...threadOverrides,
|
||||
};
|
||||
const runtime: SlashCommandRuntimePort = {
|
||||
toggleFastMode: vi.fn(),
|
||||
toggleCollaborationMode: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
setRequestedModel: vi.fn(),
|
||||
setRequestedReasoningEffort: vi.fn(),
|
||||
...runtimeOverrides,
|
||||
};
|
||||
const status: SlashCommandStatusPort = {
|
||||
addSystemMessage: vi.fn(),
|
||||
addStructuredSystemMessage: vi.fn(),
|
||||
setStatus: vi.fn(),
|
||||
setRequestedModel: vi.fn(),
|
||||
setRequestedReasoningEffort: vi.fn(),
|
||||
statusSummaryLines: () => [],
|
||||
connectionDiagnosticDetails: () => [],
|
||||
mcpStatusLines: vi.fn().mockResolvedValue([]),
|
||||
modelStatusLines: () => [],
|
||||
effortStatusLines: () => [],
|
||||
...overrides,
|
||||
...statusOverrides,
|
||||
};
|
||||
const host: SlashCommandControllerHost = {
|
||||
state: createSubmissionStatePort(stateStore),
|
||||
currentClient: () => client,
|
||||
codexInput: vi.fn((text: string) => textInput(text)),
|
||||
threads,
|
||||
runtime,
|
||||
status,
|
||||
...hostOverrides,
|
||||
};
|
||||
return { compactThread, host, stateStore, threadTurnsList };
|
||||
}
|
||||
|
|
@ -75,7 +97,7 @@ describe("SlashCommandController", () => {
|
|||
|
||||
const result = await controller.execute("clear", "");
|
||||
|
||||
expect(host.startNewThread).toHaveBeenCalledOnce();
|
||||
expect(host.threads.startNewThread).toHaveBeenCalledOnce();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
|
|
@ -101,7 +123,7 @@ describe("SlashCommandController", () => {
|
|||
await controller.execute("compact", "");
|
||||
|
||||
expect(compactThread).toHaveBeenCalledWith("thread");
|
||||
expect(host.setStatus).toHaveBeenCalledWith("Compaction requested.");
|
||||
expect(host.status.setStatus).toHaveBeenCalledWith("Compaction requested.");
|
||||
});
|
||||
|
||||
it("reports unreadable referenced threads", async () => {
|
||||
|
|
@ -116,6 +138,6 @@ describe("SlashCommandController", () => {
|
|||
|
||||
expect(threadTurnsList).toHaveBeenCalledWith("other", null, 20);
|
||||
expect(result).toBeUndefined();
|
||||
expect(host.addSystemMessage).toHaveBeenCalledWith("Referenced thread has no readable conversation turns.");
|
||||
expect(host.status.addSystemMessage).toHaveBeenCalledWith("Referenced thread has no readable conversation turns.");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ import { createChatState, createChatStateStore } from "../../../../../src/featur
|
|||
import {
|
||||
TurnSubmissionController,
|
||||
type TurnSubmissionControllerHost,
|
||||
type TurnSubmissionComposerPort,
|
||||
type TurnSubmissionConnectionPort,
|
||||
type TurnSubmissionRestoredThreadPort,
|
||||
type TurnSubmissionRuntimePort,
|
||||
type TurnSubmissionStatusPort,
|
||||
type TurnSubmissionThreadPort,
|
||||
type TurnSubmissionViewPort,
|
||||
} from "../../../../../src/features/chat/controllers/submission/turn-submission-controller";
|
||||
import { createSubmissionStatePort } from "../../../../../src/features/chat/controllers/state-ports";
|
||||
import type { Thread } from "../../../../../src/generated/app-server/v2/Thread";
|
||||
|
|
@ -36,7 +43,29 @@ function thread(id: string): Thread {
|
|||
};
|
||||
}
|
||||
|
||||
function createHost(overrides: Partial<TurnSubmissionControllerHost> = {}) {
|
||||
interface TurnSubmissionHostOverrides extends Partial<
|
||||
Omit<TurnSubmissionControllerHost, "connection" | "restoredThread" | "thread" | "runtime" | "composer" | "view" | "status">
|
||||
> {
|
||||
connection?: Partial<TurnSubmissionConnectionPort>;
|
||||
restoredThread?: Partial<TurnSubmissionRestoredThreadPort>;
|
||||
thread?: Partial<TurnSubmissionThreadPort>;
|
||||
runtime?: Partial<TurnSubmissionRuntimePort>;
|
||||
composer?: Partial<TurnSubmissionComposerPort>;
|
||||
view?: Partial<TurnSubmissionViewPort>;
|
||||
status?: Partial<TurnSubmissionStatusPort>;
|
||||
}
|
||||
|
||||
function createHost(overrides: TurnSubmissionHostOverrides = {}) {
|
||||
const {
|
||||
connection: connectionOverrides,
|
||||
restoredThread: restoredThreadOverrides,
|
||||
thread: threadOverrides,
|
||||
runtime: runtimeOverrides,
|
||||
composer: composerOverrides,
|
||||
view: viewOverrides,
|
||||
status: statusOverrides,
|
||||
...hostOverrides
|
||||
} = overrides;
|
||||
const stateStore = createChatStateStore(createChatState());
|
||||
const startTurn = vi.fn().mockResolvedValue({ turn: { id: "turn" } });
|
||||
const steerTurn = vi.fn().mockResolvedValue({});
|
||||
|
|
@ -44,11 +73,16 @@ function createHost(overrides: Partial<TurnSubmissionControllerHost> = {}) {
|
|||
startTurn,
|
||||
steerTurn,
|
||||
} as unknown as AppServerClient;
|
||||
const host: TurnSubmissionControllerHost = {
|
||||
state: createSubmissionStatePort(stateStore),
|
||||
const connection: TurnSubmissionConnectionPort = {
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
...connectionOverrides,
|
||||
};
|
||||
const restoredThread: TurnSubmissionRestoredThreadPort = {
|
||||
ensureRestoredThreadLoaded: vi.fn().mockResolvedValue(true),
|
||||
...restoredThreadOverrides,
|
||||
};
|
||||
const threadPort: TurnSubmissionThreadPort = {
|
||||
startThread: vi.fn().mockImplementation(async () => {
|
||||
stateStore.dispatch({
|
||||
type: "thread/resumed",
|
||||
|
|
@ -65,15 +99,38 @@ function createHost(overrides: Partial<TurnSubmissionControllerHost> = {}) {
|
|||
}),
|
||||
notifyActiveThreadIdentityChanged: vi.fn(),
|
||||
resetThreadTurnPresence: vi.fn(),
|
||||
...threadOverrides,
|
||||
};
|
||||
const runtime: TurnSubmissionRuntimePort = {
|
||||
applyPendingThreadSettings: vi.fn().mockResolvedValue(true),
|
||||
...runtimeOverrides,
|
||||
};
|
||||
const composer: TurnSubmissionComposerPort = {
|
||||
codexInput: vi.fn((text: string) => textInput(text)),
|
||||
setDraft: vi.fn(),
|
||||
...composerOverrides,
|
||||
};
|
||||
const view: TurnSubmissionViewPort = {
|
||||
forceMessagesToBottom: vi.fn(),
|
||||
render: vi.fn(),
|
||||
scheduleRender: vi.fn(),
|
||||
...viewOverrides,
|
||||
};
|
||||
const status: TurnSubmissionStatusPort = {
|
||||
setStatus: vi.fn(),
|
||||
addSystemMessage: vi.fn(),
|
||||
...overrides,
|
||||
...statusOverrides,
|
||||
};
|
||||
const host: TurnSubmissionControllerHost = {
|
||||
state: createSubmissionStatePort(stateStore),
|
||||
connection,
|
||||
restoredThread,
|
||||
thread: threadPort,
|
||||
runtime,
|
||||
composer,
|
||||
view,
|
||||
status,
|
||||
...hostOverrides,
|
||||
};
|
||||
return { host, startTurn, stateStore, steerTurn };
|
||||
}
|
||||
|
|
@ -85,14 +142,14 @@ describe("TurnSubmissionController", () => {
|
|||
|
||||
await controller.sendTurnText("hello");
|
||||
|
||||
expect(host.startThread).toHaveBeenCalledWith("hello");
|
||||
expect(host.notifyActiveThreadIdentityChanged).toHaveBeenCalledOnce();
|
||||
expect(host.resetThreadTurnPresence).toHaveBeenCalledWith(false);
|
||||
expect(host.thread.startThread).toHaveBeenCalledWith("hello");
|
||||
expect(host.thread.notifyActiveThreadIdentityChanged).toHaveBeenCalledOnce();
|
||||
expect(host.thread.resetThreadTurnPresence).toHaveBeenCalledWith(false);
|
||||
expect(startTurn).toHaveBeenCalledWith("thread", "/vault", textInput("hello"), expect.stringMatching(/^local-user-\d+$/));
|
||||
expect(stateStore.getState().turnLifecycle).toEqual({ kind: "running", turnId: "turn" });
|
||||
expect(host.setDraft).toHaveBeenCalledWith("");
|
||||
expect(host.setStatus).toHaveBeenCalledWith("Turn running...");
|
||||
expect(host.scheduleRender).toHaveBeenCalledOnce();
|
||||
expect(host.composer.setDraft).toHaveBeenCalledWith("");
|
||||
expect(host.status.setStatus).toHaveBeenCalledWith("Turn running...");
|
||||
expect(host.view.scheduleRender).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("steers a running turn instead of starting another turn", async () => {
|
||||
|
|
@ -115,7 +172,7 @@ describe("TurnSubmissionController", () => {
|
|||
|
||||
expect(steerTurn).toHaveBeenCalledWith("thread", "turn", textInput("follow up"), expect.stringMatching(/^local-steer-\d+$/));
|
||||
expect(startTurn).not.toHaveBeenCalled();
|
||||
expect(host.setStatus).toHaveBeenCalledWith("Steered current turn.");
|
||||
expect(host.status.setStatus).toHaveBeenCalledWith("Steered current turn.");
|
||||
const localSteerId = steerTurn.mock.calls[0]?.[3];
|
||||
expect(
|
||||
stateStore.getState().displayItems.some((item) => item.kind === "message" && item.id === localSteerId && item.text === "follow up"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue