mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
Replace stateless thread services with factories
This commit is contained in:
parent
0de3db212e
commit
16413a52df
7 changed files with 140 additions and 99 deletions
|
|
@ -13,8 +13,8 @@ import { getThreadTitle } from "../../../domain/threads/model";
|
|||
import type { SharedServerMetadata } from "../../../domain/server/metadata";
|
||||
import { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work";
|
||||
import { shortThreadId } from "../../../utils";
|
||||
import { ThreadOperations } from "../../threads/thread-operations";
|
||||
import { ThreadTitleService } from "../../threads/thread-title-service";
|
||||
import { createThreadOperations, type ThreadOperations } from "../../threads/thread-operations";
|
||||
import { createThreadTitleService, type ThreadTitleService } from "../../threads/thread-title-service";
|
||||
import { PendingRequestController } from "../application/pending-requests/controller";
|
||||
import type { MessageStreamItem, MessageStreamNoticeSection } from "../domain/message-stream/items";
|
||||
import { createStructuredSystemItem, createSystemItem } from "../domain/message-stream/factories/system-items";
|
||||
|
|
@ -619,7 +619,7 @@ export class ChatPanelSession implements ChatSurfaceHandle {
|
|||
|
||||
private createThreadTitleService(currentClient: CurrentAppServerClient): ThreadTitleService {
|
||||
const environment = this.environment;
|
||||
return new ThreadTitleService({
|
||||
return createThreadTitleService({
|
||||
settings: {
|
||||
current: () => environment.plugin.settingsRef.settings,
|
||||
vaultPath: environment.plugin.settingsRef.vaultPath,
|
||||
|
|
@ -669,7 +669,7 @@ export class ChatPanelSession implements ChatSurfaceHandle {
|
|||
|
||||
private createThreadOperations(currentClient: CurrentAppServerClient, ensureConnected: () => Promise<void>): ThreadOperations {
|
||||
const environment = this.environment;
|
||||
return new ThreadOperations({
|
||||
return createThreadOperations({
|
||||
connection: {
|
||||
ensureConnected,
|
||||
currentClient,
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import type { OpenCodexPanelSnapshot } from "../../workspace/open-panel-snapshot
|
|||
import type { SharedThreadCatalog } from "../../workspace/shared-thread-catalog";
|
||||
import { ConnectionWorkTracker } from "../../shared/lifecycle/connection-work";
|
||||
import type { ArchiveExportAdapter } from "../../app-server/services/thread-archive-markdown";
|
||||
import { ThreadOperations } from "../threads/thread-operations";
|
||||
import { ThreadTitleService } from "../threads/thread-title-service";
|
||||
import { createThreadOperations, type ThreadOperations } from "../threads/thread-operations";
|
||||
import { createThreadTitleService, type ThreadTitleService } from "../threads/thread-title-service";
|
||||
import { renderThreadsView, unmountThreadsView } from "./renderer";
|
||||
import {
|
||||
completedThreadAutoNameState,
|
||||
|
|
@ -77,7 +77,7 @@ export class CodexThreadsSession {
|
|||
constructor(private readonly environment: CodexThreadsSessionEnvironment) {
|
||||
this.deferredTasks = createThreadsViewDeferredTasks(() => this.viewWindow());
|
||||
this.connection = new ConnectionManager(() => this.host.settings.codexPath, this.host.vaultPath);
|
||||
this.operations = new ThreadOperations({
|
||||
this.operations = createThreadOperations({
|
||||
connection: {
|
||||
ensureConnected: () => this.ensureConnected(),
|
||||
currentClient: () => this.client,
|
||||
|
|
@ -92,7 +92,7 @@ export class CodexThreadsSession {
|
|||
new Notice(message);
|
||||
},
|
||||
});
|
||||
this.titleService = new ThreadTitleService({
|
||||
this.titleService = createThreadTitleService({
|
||||
settings: {
|
||||
current: () => this.host.settings,
|
||||
vaultPath: this.host.vaultPath,
|
||||
|
|
|
|||
|
|
@ -21,57 +21,79 @@ export interface ThreadOperationsHost {
|
|||
notice(message: string): void;
|
||||
}
|
||||
|
||||
export interface ArchiveThreadOptions {
|
||||
interface ArchiveThreadOptions {
|
||||
saveMarkdown?: boolean;
|
||||
closeOpenPanels?: boolean;
|
||||
}
|
||||
|
||||
export interface RenameThreadOptions {
|
||||
interface RenameThreadOptions {
|
||||
shouldPublish?: () => boolean;
|
||||
}
|
||||
|
||||
export class ThreadOperations {
|
||||
constructor(private readonly host: ThreadOperationsHost) {}
|
||||
|
||||
async renameThread(threadId: string, value: string, options: RenameThreadOptions = {}): Promise<boolean> {
|
||||
const rename = threadRenameFromValue(value);
|
||||
if (!rename) return false;
|
||||
|
||||
await this.host.connection.ensureConnected();
|
||||
return this.renameConnectedThread(threadId, rename, options);
|
||||
}
|
||||
|
||||
private async renameConnectedThread(threadId: string, rename: ThreadRename, options: RenameThreadOptions = {}): Promise<boolean> {
|
||||
const client = this.host.connection.currentClient();
|
||||
if (!client) return false;
|
||||
|
||||
const result = await renameThreadOnAppServer(client, threadId, rename);
|
||||
if (options.shouldPublish?.() ?? true) {
|
||||
this.host.catalog.renameThreadInCatalog(threadId, result.name);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async archiveThread(threadId: string, options: ArchiveThreadOptions = {}): Promise<ArchiveThreadResult | null> {
|
||||
await this.host.connection.ensureConnected();
|
||||
const client = this.host.connection.currentClient();
|
||||
if (!client) return null;
|
||||
|
||||
const settings = this.host.settings.current();
|
||||
const result = await archiveThreadOnAppServer(client, threadId, {
|
||||
settings,
|
||||
vaultPath: this.host.settings.vaultPath,
|
||||
archiveAdapter: () => this.host.archiveAdapter(),
|
||||
saveMarkdown: options.saveMarkdown ?? settings.archiveExportEnabled,
|
||||
});
|
||||
if (result.exportedPath) {
|
||||
this.host.notice(`Saved archived thread to ${result.exportedPath}.`);
|
||||
}
|
||||
if (options.closeOpenPanels === undefined) {
|
||||
this.host.catalog.archiveThreadInCatalog(threadId);
|
||||
} else {
|
||||
this.host.catalog.archiveThreadInCatalog(threadId, { closeOpenPanels: options.closeOpenPanels });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export interface ThreadOperations {
|
||||
renameThread(threadId: string, value: string, options?: RenameThreadOptions): Promise<boolean>;
|
||||
archiveThread(threadId: string, options?: ArchiveThreadOptions): Promise<ArchiveThreadResult | null>;
|
||||
}
|
||||
|
||||
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 rename = threadRenameFromValue(value);
|
||||
if (!rename) return false;
|
||||
|
||||
await host.connection.ensureConnected();
|
||||
return renameConnectedThread(host, threadId, rename, options);
|
||||
}
|
||||
|
||||
async function renameConnectedThread(
|
||||
host: ThreadOperationsHost,
|
||||
threadId: string,
|
||||
rename: ThreadRename,
|
||||
options: RenameThreadOptions = {},
|
||||
): Promise<boolean> {
|
||||
const client = host.connection.currentClient();
|
||||
if (!client) return false;
|
||||
|
||||
const result = await renameThreadOnAppServer(client, threadId, rename);
|
||||
if (options.shouldPublish?.() ?? true) {
|
||||
host.catalog.renameThreadInCatalog(threadId, result.name);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function archiveThread(
|
||||
host: ThreadOperationsHost,
|
||||
threadId: string,
|
||||
options: ArchiveThreadOptions = {},
|
||||
): Promise<ArchiveThreadResult | null> {
|
||||
await host.connection.ensureConnected();
|
||||
const client = host.connection.currentClient();
|
||||
if (!client) return null;
|
||||
|
||||
const settings = host.settings.current();
|
||||
const result = await archiveThreadOnAppServer(client, threadId, {
|
||||
settings,
|
||||
vaultPath: host.settings.vaultPath,
|
||||
archiveAdapter: () => host.archiveAdapter(),
|
||||
saveMarkdown: options.saveMarkdown ?? settings.archiveExportEnabled,
|
||||
});
|
||||
if (result.exportedPath) {
|
||||
host.notice(`Saved archived thread to ${result.exportedPath}.`);
|
||||
}
|
||||
if (options.closeOpenPanels === undefined) {
|
||||
host.catalog.archiveThreadInCatalog(threadId);
|
||||
} else {
|
||||
host.catalog.archiveThreadInCatalog(threadId, { closeOpenPanels: options.closeOpenPanels });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,42 +21,57 @@ export interface ThreadTitleServiceHost {
|
|||
generateThreadTitle?(context: ThreadTitleContext): Promise<string | null>;
|
||||
}
|
||||
|
||||
export class ThreadTitleService {
|
||||
constructor(private readonly host: ThreadTitleServiceHost) {}
|
||||
|
||||
async generateTitle(threadId: string): Promise<string> {
|
||||
const context = await this.resolveContext(threadId);
|
||||
if (!context) throw new Error(THREAD_TITLE_CONTEXT_UNAVAILABLE_MESSAGE);
|
||||
|
||||
const title = await this.generate(context);
|
||||
if (!title) throw new Error("Codex did not return a usable thread title.");
|
||||
return title;
|
||||
}
|
||||
|
||||
async resolveContext(threadId: string): Promise<ThreadTitleContext | null> {
|
||||
const client = this.host.currentClient();
|
||||
const persistedContext = client
|
||||
? await findThreadTitleContext({
|
||||
threadId,
|
||||
readTurns: (id, cursor, limit, sortDirection) => readCompletedConversationSummariesPage(client, id, cursor, limit, sortDirection),
|
||||
})
|
||||
: null;
|
||||
return persistedContext ?? this.host.visibleContext?.(threadId) ?? null;
|
||||
}
|
||||
|
||||
completedTurnContext(turnId: string, completedSummary: ThreadConversationSummary | null): ThreadTitleContext | null {
|
||||
return (
|
||||
this.host.visibleCompletedTurnContext?.(turnId) ??
|
||||
(completedSummary ? threadTitleContextFromConversationSummary(completedSummary) : null)
|
||||
);
|
||||
}
|
||||
|
||||
async generate(context: ThreadTitleContext): Promise<string | null> {
|
||||
if (this.host.generateThreadTitle) return this.host.generateThreadTitle(context);
|
||||
const settings = this.host.settings.current();
|
||||
return generateThreadTitleWithCodex(settings.codexPath, this.host.settings.vaultPath, context, {
|
||||
threadNamingModel: settings.threadNamingModel,
|
||||
threadNamingEffort: settings.threadNamingEffort,
|
||||
});
|
||||
}
|
||||
export interface ThreadTitleService {
|
||||
generateTitle(threadId: string): Promise<string>;
|
||||
resolveContext(threadId: string): Promise<ThreadTitleContext | null>;
|
||||
completedTurnContext(turnId: string, completedSummary: ThreadConversationSummary | null): ThreadTitleContext | null;
|
||||
generate(context: ThreadTitleContext): Promise<string | null>;
|
||||
}
|
||||
|
||||
export function createThreadTitleService(host: ThreadTitleServiceHost): ThreadTitleService {
|
||||
return {
|
||||
generateTitle: (threadId) => generateTitle(host, threadId),
|
||||
resolveContext: (threadId) => resolveThreadTitleContext(host, threadId),
|
||||
completedTurnContext: (turnId, completedSummary) => completedTurnContext(host, turnId, completedSummary),
|
||||
generate: (context) => generateTitleFromContext(host, context),
|
||||
};
|
||||
}
|
||||
|
||||
async function generateTitle(host: ThreadTitleServiceHost, threadId: string): Promise<string> {
|
||||
const context = await resolveThreadTitleContext(host, threadId);
|
||||
if (!context) throw new Error(THREAD_TITLE_CONTEXT_UNAVAILABLE_MESSAGE);
|
||||
|
||||
const title = await generateTitleFromContext(host, context);
|
||||
if (!title) throw new Error("Codex did not return a usable thread title.");
|
||||
return title;
|
||||
}
|
||||
|
||||
async function resolveThreadTitleContext(host: ThreadTitleServiceHost, threadId: string): Promise<ThreadTitleContext | null> {
|
||||
const client = host.currentClient();
|
||||
const persistedContext = client
|
||||
? await findThreadTitleContext({
|
||||
threadId,
|
||||
readTurns: (id, cursor, limit, sortDirection) => readCompletedConversationSummariesPage(client, id, cursor, limit, sortDirection),
|
||||
})
|
||||
: null;
|
||||
return persistedContext ?? host.visibleContext?.(threadId) ?? null;
|
||||
}
|
||||
|
||||
function completedTurnContext(
|
||||
host: ThreadTitleServiceHost,
|
||||
turnId: string,
|
||||
completedSummary: ThreadConversationSummary | null,
|
||||
): ThreadTitleContext | null {
|
||||
return (
|
||||
host.visibleCompletedTurnContext?.(turnId) ?? (completedSummary ? threadTitleContextFromConversationSummary(completedSummary) : null)
|
||||
);
|
||||
}
|
||||
|
||||
async function generateTitleFromContext(host: ThreadTitleServiceHost, context: ThreadTitleContext): Promise<string | null> {
|
||||
if (host.generateThreadTitle) return host.generateThreadTitle(context);
|
||||
const settings = host.settings.current();
|
||||
return generateThreadTitleWithCodex(settings.codexPath, host.settings.vaultPath, context, {
|
||||
threadNamingModel: settings.threadNamingModel,
|
||||
threadNamingEffort: settings.threadNamingEffort,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { createChatStateStore } from "../../../../src/features/chat/application/
|
|||
import { messageStreamItems } from "../../../../src/features/chat/application/state/message-stream";
|
||||
import { AutoTitleController } from "../../../../src/features/chat/application/threads/auto-title-controller";
|
||||
import { threadTitleContextFromMessageStreamItems } from "../../../../src/features/chat/application/threads/title-context";
|
||||
import { ThreadTitleService } from "../../../../src/features/threads/thread-title-service";
|
||||
import { createThreadTitleService } from "../../../../src/features/threads/thread-title-service";
|
||||
import type { Thread } from "../../../../src/domain/threads/model";
|
||||
import type { ThreadTitleContext } from "../../../../src/domain/threads/title-generation-model";
|
||||
import { DEFAULT_SETTINGS } from "../../../../src/settings/model";
|
||||
|
|
@ -134,7 +134,7 @@ function controllerFixture(
|
|||
stateStore.dispatch({ type: "thread-list/applied", threads: [threadFixture("thread")] });
|
||||
const currentClient = overrides.currentClient ?? (() => fakeClient());
|
||||
const notifyThreadRenamed = vi.fn();
|
||||
const titleService = new ThreadTitleService({
|
||||
const titleService = createThreadTitleService({
|
||||
settings: {
|
||||
current: () => ({ ...DEFAULT_SETTINGS, codexPath: "codex" }),
|
||||
vaultPath: "/vault",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { AppServerClient } from "../../../src/app-server/connection/client"
|
|||
import type { ArchiveThreadResult } from "../../../src/app-server/services/thread-archive";
|
||||
import type { ArchiveExportAdapter } from "../../../src/app-server/services/thread-archive-markdown";
|
||||
import { DEFAULT_SETTINGS } from "../../../src/settings/model";
|
||||
import { ThreadOperations, type ThreadOperationsHost } from "../../../src/features/threads/thread-operations";
|
||||
import { createThreadOperations, type ThreadOperationsHost } from "../../../src/features/threads/thread-operations";
|
||||
|
||||
const archiveMock = vi.hoisted(() => ({
|
||||
archiveThreadOnAppServer: vi.fn(),
|
||||
|
|
@ -77,7 +77,7 @@ function operationsFixture(options: { client?: MockClient | null } = {}) {
|
|||
catalog,
|
||||
notice,
|
||||
};
|
||||
return { operations: new ThreadOperations(host), client, catalog, notice };
|
||||
return { operations: createThreadOperations(host), client, catalog, notice };
|
||||
}
|
||||
|
||||
type MockClient = ReturnType<typeof clientMock>;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import { describe, expect, it, vi } from "vitest";
|
|||
import type { AppServerClient } from "../../../src/app-server/connection/client";
|
||||
import { THREAD_TITLE_CONTEXT_UNAVAILABLE_MESSAGE, type ThreadTitleContext } from "../../../src/domain/threads/title-generation-model";
|
||||
import { DEFAULT_SETTINGS } from "../../../src/settings/model";
|
||||
import { ThreadTitleService } from "../../../src/features/threads/thread-title-service";
|
||||
import {
|
||||
createThreadTitleService,
|
||||
type ThreadTitleService,
|
||||
type ThreadTitleServiceHost,
|
||||
} from "../../../src/features/threads/thread-title-service";
|
||||
|
||||
describe("ThreadTitleService", () => {
|
||||
it("generates a title from visible context without saving it", async () => {
|
||||
|
|
@ -45,8 +49,8 @@ describe("ThreadTitleService", () => {
|
|||
});
|
||||
});
|
||||
|
||||
function titleService(options: Partial<ConstructorParameters<typeof ThreadTitleService>[0]> = {}): ThreadTitleService {
|
||||
return new ThreadTitleService({
|
||||
function titleService(options: Partial<ThreadTitleServiceHost> = {}): ThreadTitleService {
|
||||
return createThreadTitleService({
|
||||
settings: {
|
||||
current: () => ({ ...DEFAULT_SETTINGS, codexPath: "codex" }),
|
||||
vaultPath: "/vault",
|
||||
|
|
|
|||
Loading…
Reference in a new issue