murashit_codex-panel/src/features/threads/workflows/thread-operations.ts
2026-07-21 20:59:09 +09:00

106 lines
3.7 KiB
TypeScript

import { normalizeExplicitThreadName, type Thread } from "../../../domain/threads/model";
import { threadDisplayTitle } from "../../../domain/threads/title";
import type { KeyedOperationQueue } from "../../../shared/runtime/keyed-operation-queue";
import type { ThreadCatalogEventSink } from "../catalog/thread-catalog";
import { type ArchiveExportDestination, type ArchiveExportSettings, exportArchivedThreadMarkdown } from "./archive-export";
import type { ThreadOperationsTransport } from "./ports";
export interface ThreadOperationsHost {
transport: ThreadOperationsTransport;
nameMutations: KeyedOperationQueue<string>;
archiveExport: {
settings(): ArchiveExportSettings;
enabled(): boolean;
vaultPath: string;
vaultConfigDir: string;
};
archiveDestination(): ArchiveExportDestination;
catalog: ThreadCatalogEventSink;
referenceThreads(): readonly Thread[];
notice(message: string): void;
}
interface ArchiveThreadOptions {
saveMarkdown?: boolean;
shouldPublish?: () => boolean;
}
export interface ArchiveThreadResult {
exportedPath: string | null;
}
interface RenameThreadOptions {
shouldStart?: () => boolean;
shouldPublish?: () => boolean;
}
export interface ThreadOperations {
renameThread(threadId: string, value: string, options?: RenameThreadOptions): Promise<boolean>;
archiveThread(threadId: string, options?: ArchiveThreadOptions): Promise<ArchiveThreadResult>;
}
export function createThreadOperations(host: ThreadOperationsHost): ThreadOperations {
return {
renameThread: (threadId, value, options) => renameThread(host, threadId, value, options),
archiveThread: (threadId, options) => archiveThread(host, threadId, options),
};
}
async function renameThread(
host: ThreadOperationsHost,
threadId: string,
value: string,
options: RenameThreadOptions = {},
): Promise<boolean> {
const name = normalizeExplicitThreadName(value);
if (!name) return false;
return host.nameMutations.run(threadId, async () => {
if (!(options.shouldStart?.() ?? true)) return false;
await host.transport.renameThread(threadId, name);
if (options.shouldPublish?.() ?? true) {
host.catalog.apply({ type: "thread-renamed", threadId, name });
}
return true;
});
}
async function archiveThread(
host: ThreadOperationsHost,
threadId: string,
options: ArchiveThreadOptions = {},
): Promise<ArchiveThreadResult> {
const shouldExport = options.saveMarkdown ?? host.archiveExport.enabled();
const exportedPath = await host.transport.archiveThread(
threadId,
shouldExport
? async (thread) => {
const archiveSettings = host.archiveExport.settings();
const threads = host.referenceThreads();
const titleById = new Map(threads.map((item) => [item.id, threadDisplayTitle(item)] as const));
const result = await exportArchivedThreadMarkdown(
{
...thread,
transcriptEntries: thread.transcriptEntries.map((entry) => {
if (!entry.referencedThread) return entry;
const title = titleById.get(entry.referencedThread.threadId);
return title ? { ...entry, referencedThread: { ...entry.referencedThread, title } } : entry;
}),
},
{
...archiveSettings,
vaultPath: host.archiveExport.vaultPath,
vaultConfigDir: host.archiveExport.vaultConfigDir,
},
host.archiveDestination(),
);
return result.path;
}
: undefined,
);
if (!(options.shouldPublish?.() ?? true)) return { exportedPath };
if (exportedPath) {
host.notice(`Saved archived thread to ${exportedPath}.`);
}
host.catalog.apply({ type: "thread-archived", threadId });
return { exportedPath };
}