murashit_codex-panel/src/features/chat/application/conversation/slash-command-executor.ts
2026-06-18 21:21:10 +09:00

86 lines
3.8 KiB
TypeScript

import type { AppServerClient } from "../../../../app-server/connection/client";
import { codexTextInputWithAttachments, type CodexInput } from "../../../../domain/chat/input";
import { readReferencedThreadConversationSummaries } from "../../../../app-server/threads";
import { referencedThreadPromptBundle, REFERENCED_THREAD_TURN_LIMIT } from "../../../../domain/threads/reference";
import type { Thread } from "../../../../domain/threads/model";
import { shortThreadId } from "../../../../utils";
import {
executeSlashCommand as runSlashCommand,
type SlashCommandExecutionPorts,
type SlashCommandExecutionResult,
type ThreadReferenceInput,
} from "./slash-command-execution";
import type { SlashCommandName } from "../composer/slash-commands";
import type { ReasoningEffort } from "../../../../domain/catalog/metadata";
import { findModelMetadataByIdOrName, supportedEffortsForModelMetadata } from "../../../../domain/catalog/metadata";
import { submissionStateSnapshot } from "../state/selectors";
import type { ChatStateStore } from "../state/store";
import { currentModel, runtimeConfigOrDefault } from "../../domain/runtime/effective";
import { runtimeSnapshotForChatState } from "../runtime/snapshot";
export interface SlashCommandExecutorHost extends SlashCommandExecutionPorts {
stateStore: ChatStateStore;
currentClient: () => AppServerClient | null;
codexInput: (text: string) => CodexInput;
setStatus: (status: string) => void;
}
function referencedThreadStatus(thread: Thread, includedTurns: number): string {
return `Referencing ${shortThreadId(thread.id)} (${String(includedTurns)}/${String(REFERENCED_THREAD_TURN_LIMIT)} turns).`;
}
function referencedThreadUnreadableMessage(): string {
return "Referenced thread has no readable conversation turns.";
}
export async function executeSlashCommandWithState(
host: SlashCommandExecutorHost,
command: SlashCommandName,
args: string,
): Promise<SlashCommandExecutionResult | undefined> {
const state = submissionStateSnapshot(host.stateStore.getState());
const client = host.currentClient();
if (!client && command !== "reconnect" && command !== "compact") return;
return runSlashCommand(command, args, {
...host,
activeThreadId: state.activeThreadId,
listedThreads: state.listedThreads,
busy: state.busy,
referThread: (thread, message) => {
if (!client) return Promise.resolve(null);
return referencedThreadInput(host, client, thread, message);
},
supportedReasoningEfforts: () => supportedReasoningEfforts(host.stateStore.getState()),
});
}
function supportedReasoningEfforts(state: ReturnType<ChatStateStore["getState"]>): ReasoningEffort[] {
const config = runtimeConfigOrDefault(state.connection.runtimeConfig);
const model = findModelMetadataByIdOrName(state.connection.availableModels, currentModel(runtimeSnapshotForChatState(state), config));
return supportedEffortsForModelMetadata(model);
}
async function referencedThreadInput(
host: SlashCommandExecutorHost,
client: AppServerClient,
thread: Thread,
message: string,
): Promise<ThreadReferenceInput | null> {
try {
const turns = await readReferencedThreadConversationSummaries(client, thread.id, REFERENCED_THREAD_TURN_LIMIT);
if (turns.length === 0) {
host.addSystemMessage(referencedThreadUnreadableMessage());
return null;
}
const reference = referencedThreadPromptBundle(thread, turns, message);
const messageInput = host.codexInput(message);
host.setStatus(referencedThreadStatus(thread, turns.length));
return {
input: codexTextInputWithAttachments(reference.prompt, messageInput),
referencedThread: reference.referencedThread,
};
} catch (error) {
host.addSystemMessage(error instanceof Error ? error.message : String(error));
return null;
}
}