refactor: consolidate thin module ownership

This commit is contained in:
murashit 2026-07-21 20:51:06 +09:00
parent 67289765c6
commit f4616cf945
23 changed files with 84 additions and 105 deletions

View file

@ -17,7 +17,6 @@ import type { SelectionRewriteTransport } from "./features/selection-rewrite/tra
import { openThreadPicker, type ThreadPickerController } from "./features/thread-picker/modal.obsidian";
import { createThreadOperationsTransport, createThreadTitleTransport } from "./features/threads/app-server/workflow-transports";
import { createThreadCatalog, type ThreadCatalog, type ThreadCatalogEvent } from "./features/threads/catalog/thread-catalog";
import { createThreadNameMutationCoordinator } from "./features/threads/workflows/thread-name-mutation-coordinator";
import type { ThreadsViewHost, ThreadsViewSettingsAccess } from "./features/threads-view/session";
import type { ThreadsViewPanelActivity } from "./features/threads-view/state";
import type { ThreadsRuntimeView } from "./features/threads-view/view.obsidian";
@ -49,7 +48,7 @@ export class CodexExecutionRuntime implements AppServerClientAccess {
private readonly appServerQueries: AppServerQueryCache;
private readonly threadCatalog: ThreadCatalog;
readonly settingsDynamicData: SettingsDynamicDataAccess;
private readonly threadNameMutations = createThreadNameMutationCoordinator();
private readonly threadNameMutations = createKeyedOperationQueue<string>();
private readonly threadGoalOperations = createThreadGoalOperationCoordinator();
private readonly runtimeSettingsCommitQueue = createKeyedOperationQueue<string>();
private readonly shortLivedClients = new Set<AppServerClient>();

View file

@ -1,5 +1,4 @@
import { RUNNING_EXECUTION_STATE, type ThreadStreamExecutionState } from "../../../domain/thread-stream/execution-state";
import type { ExecutionState } from "../../../domain/thread-stream/items";
import { type ExecutionState, RUNNING_EXECUTION_STATE, type ThreadStreamExecutionState } from "../../../domain/thread-stream/items";
export { RUNNING_EXECUTION_STATE };

View file

@ -1,7 +0,0 @@
export interface DailyNoteReferenceCandidate {
keyword: "today" | "tomorrow" | "yesterday";
display: string;
name: string;
path: string;
linktext: string;
}

View file

@ -1,6 +1,28 @@
import type { VaultFileReference } from "../../../../domain/chat/input";
import type { DailyNoteReferenceCandidate } from "./daily-note-references";
import type { NoteCandidate } from "./suggestions";
export interface DailyNoteReferenceCandidate {
keyword: "today" | "tomorrow" | "yesterday";
display: string;
name: string;
path: string;
linktext: string;
}
export interface NoteHeadingCandidate {
heading: string;
linkHeading: string;
level: number;
}
export interface NoteCandidate {
basename: string;
displayName: string;
path: string;
mtime: number;
linktext: string;
headings: NoteHeadingCandidate[];
recentIndex: number | null;
}
export interface NoteCandidateProvider {
candidates(sourcePath: string): readonly NoteCandidate[];

View file

@ -19,7 +19,7 @@ import {
type SelectionContextReference,
selectionContextReferenceMarker,
} from "./context-references";
import type { DailyNoteReferenceCandidate } from "./daily-note-references";
import type { DailyNoteReferenceCandidate, NoteCandidate, NoteHeadingCandidate } from "./note-context";
import { isSlashCommandName, SLASH_COMMANDS, type SlashCommandName, slashCommandSubcommands } from "./slash-commands";
import {
partialThreadTitleQuery,
@ -51,22 +51,6 @@ export interface ComposerSuggestionOptions {
tagCandidates?: readonly string[] | (() => readonly string[]);
}
export interface NoteCandidate {
basename: string;
displayName: string;
path: string;
mtime: number;
linktext: string;
headings: NoteHeadingCandidate[];
recentIndex: number | null;
}
interface NoteHeadingCandidate {
heading: string;
linkHeading: string;
level: number;
}
interface NoteCandidateMatch {
file: NoteCandidate;
match: SearchResult;

View file

@ -1,4 +0,0 @@
import type { ExecutionState } from "./items";
export type ThreadStreamExecutionState = Exclude<ExecutionState, null>;
export const RUNNING_EXECUTION_STATE: ThreadStreamExecutionState = "running";

View file

@ -1,5 +1,4 @@
import { RUNNING_EXECUTION_STATE } from "../execution-state";
import type { ThreadStreamItem, ThreadStreamItemKind } from "../items";
import { RUNNING_EXECUTION_STATE, type ThreadStreamItem, type ThreadStreamItemKind } from "../items";
export const STREAMED_COMMAND_RUNNING_TEXT = "Command running";
export const STREAMED_MCP_PROGRESS_LABEL = "mcp progress";

View file

@ -19,6 +19,8 @@ export type ThreadStreamItemKind =
| "reviewResult";
type ThreadStreamRole = "user" | "assistant" | "system" | "tool";
export type ExecutionState = "running" | "completed" | "failed" | null;
export type ThreadStreamExecutionState = Exclude<ExecutionState, null>;
export const RUNNING_EXECUTION_STATE: ThreadStreamExecutionState = "running";
type DialogueState = "streaming" | "completed";
interface ThreadStreamBase {

View file

@ -10,7 +10,6 @@ import type { KeyedOperationQueue } from "../../../shared/runtime/keyed-operatio
import type { ThreadCatalogEventSink, ThreadCatalogPaginatedActiveReader } from "../../threads/catalog/thread-catalog";
import type { ArchiveExportDestination, ArchiveExportSettings } from "../../threads/workflows/archive-export";
import type { ThreadTitleTransport } from "../../threads/workflows/ports";
import type { ThreadNameMutationCoordinator } from "../../threads/workflows/thread-name-mutation-coordinator";
import type { TurnDiffViewState } from "../../turn-diff/model";
import type { ThreadGoalOperationCoordinator } from "../application/threads/goal-actions";
@ -21,7 +20,7 @@ export interface CodexChatHost {
readonly workspace: WorkspacePanels;
readonly appServerQueries: ChatAppServerQueries;
readonly threadCatalog: ChatThreadCatalog;
readonly threadNameMutations: ThreadNameMutationCoordinator;
readonly threadNameMutations: KeyedOperationQueue<string>;
readonly threadTitleTransport: ThreadTitleTransport;
readonly threadGoalOperations: ThreadGoalOperationCoordinator;
readonly runtimeSettingsCommitQueue: KeyedOperationQueue<string>;

View file

@ -1,7 +1,7 @@
import { type App, moment, normalizePath, TFile } from "obsidian";
import { appHasDailyNotesPluginLoaded, getDailyNoteSettings, type IPeriodicNoteSettings } from "obsidian-daily-notes-interface";
import type { DailyNoteReferenceCandidate } from "../../application/composer/daily-note-references";
import type { DailyNoteReferenceCandidate } from "../../application/composer/note-context";
import { displayNameForFile, linktextForFile } from "./vault-note-links.obsidian";
const RELATIVE_DAILY_NOTES = [

View file

@ -2,8 +2,7 @@ import type { App, EventRef } from "obsidian";
import { stripHeadingForLink, TFile } from "obsidian";
import type { VaultFileReference } from "../../../../domain/chat/input";
import type { NoteCandidateProvider } from "../../application/composer/note-context";
import type { NoteCandidate } from "../../application/composer/suggestions";
import type { NoteCandidate, NoteCandidateProvider } from "../../application/composer/note-context";
import { configuredDailyNoteReferences } from "./vault-daily-note-references.obsidian";
import { displayNameForFile, linktextForFile } from "./vault-note-links.obsidian";

View file

@ -14,14 +14,13 @@ import {
selectionContextReferenceMarker,
} from "../application/composer/context-references";
import type { ComposerInputSnapshot } from "../application/composer/input-snapshot";
import type { NoteCandidateProvider } from "../application/composer/note-context";
import type { NoteCandidate, NoteCandidateProvider } from "../application/composer/note-context";
import { activePanelOperationForSlashCommandSuggestion } from "../application/composer/slash-commands";
import {
activeComposerSuggestions,
applyComposerSuggestionInsertion,
type ComposerSuggestion,
composerSuggestionNavigationDirection,
type NoteCandidate,
nextComposerSuggestionIndex,
} from "../application/composer/suggestions";
import { type ThreadCommandTarget, threadCommandTargetForDraft } from "../application/composer/thread-title-argument";

View file

@ -6,11 +6,11 @@ import type { Thread } from "../../domain/threads/model";
import type { ThreadRenameLifecycleEvent } from "../../domain/threads/rename-lifecycle";
import { DeferredTask } from "../../shared/runtime/deferred-task";
import { isStaleExecutionRuntimeError } from "../../shared/runtime/execution-runtime-lifetime";
import type { KeyedOperationQueue } from "../../shared/runtime/keyed-operation-queue";
import { OwnerLifetime } from "../../shared/runtime/owner-lifetime";
import type { ThreadCatalogEventSink, ThreadCatalogPaginatedActiveReader } from "../threads/catalog/thread-catalog";
import type { ArchiveExportDestination, ArchiveExportSettings } from "../threads/workflows/archive-export";
import type { ThreadOperationsTransport, ThreadTitleTransport } from "../threads/workflows/ports";
import type { ThreadNameMutationCoordinator } from "../threads/workflows/thread-name-mutation-coordinator";
import { createThreadOperations, type ThreadOperations } from "../threads/workflows/thread-operations";
import { createThreadTitleService, type ThreadTitleService } from "../threads/workflows/thread-title-service";
import { isThreadsArchiveConfirmPointer, renderThreadsViewShell, unmountThreadsViewShell } from "./shell.dom";
@ -19,7 +19,7 @@ export interface ThreadsViewHost {
readonly settings: ThreadsViewSettingsAccess;
readonly vaultPath: string;
readonly threadCatalog: ThreadsViewThreadCatalog;
readonly threadNameMutations: ThreadNameMutationCoordinator;
readonly threadNameMutations: KeyedOperationQueue<string>;
readonly threadOperationsTransport: ThreadOperationsTransport;
readonly threadTitleTransport: ThreadTitleTransport;
openNewPanel(): Promise<unknown>;

View file

@ -1,9 +0,0 @@
import { createKeyedOperationQueue } from "../../../shared/runtime/keyed-operation-queue";
export interface ThreadNameMutationCoordinator {
run<T>(threadId: string, operation: () => Promise<T>): Promise<T>;
}
export function createThreadNameMutationCoordinator(): ThreadNameMutationCoordinator {
return createKeyedOperationQueue();
}

View file

@ -1,13 +1,13 @@
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";
import type { ThreadNameMutationCoordinator } from "./thread-name-mutation-coordinator";
export interface ThreadOperationsHost {
transport: ThreadOperationsTransport;
nameMutations: ThreadNameMutationCoordinator;
nameMutations: KeyedOperationQueue<string>;
archiveExport: {
settings(): ArchiveExportSettings;
enabled(): boolean;

View file

@ -12,10 +12,10 @@ import {
} from "../../../../../src/features/chat/application/threads/auto-title-coordinator";
import { threadTitleContextFromThreadStreamItems } from "../../../../../src/features/chat/application/threads/title-context";
import { createThreadOperationsTransport } from "../../../../../src/features/threads/app-server/workflow-transports";
import { createThreadNameMutationCoordinator } from "../../../../../src/features/threads/workflows/thread-name-mutation-coordinator";
import { createThreadOperations } from "../../../../../src/features/threads/workflows/thread-operations";
import { createThreadTitleService } from "../../../../../src/features/threads/workflows/thread-title-service";
import { DEFAULT_SETTINGS } from "../../../../../src/settings/model";
import { createKeyedOperationQueue } from "../../../../../src/shared/runtime/keyed-operation-queue";
import { deferred } from "../../../../support/async";
describe("AutoTitleCoordinator", () => {
@ -193,7 +193,7 @@ function coordinatorFixture(
transport: createThreadOperationsTransport({
withClient: async (operation) => operation(currentClient()),
}),
nameMutations: createThreadNameMutationCoordinator(),
nameMutations: createKeyedOperationQueue(),
archiveExport: {
settings: () => DEFAULT_SETTINGS,
enabled: () => false,

View file

@ -10,7 +10,6 @@ import type { ChatPanelEnvironment } from "../../../../src/features/chat/host/co
import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/session/deferred-work";
import { ChatPanelSessionRuntime } from "../../../../src/features/chat/host/session-runtime";
import { createChatThreadStreamScrollBinding } from "../../../../src/features/chat/panel/thread-stream-scroll-binding";
import { createThreadNameMutationCoordinator } from "../../../../src/features/threads/workflows/thread-name-mutation-coordinator";
import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../../../../src/settings/model";
import { StaleExecutionRuntimeError } from "../../../../src/shared/runtime/execution-runtime-lifetime";
import { createKeyedOperationQueue } from "../../../../src/shared/runtime/keyed-operation-queue";
@ -269,7 +268,7 @@ describe("ChatPanelSessionRuntime actions", () => {
},
appServerQueries,
threadCatalog,
threadNameMutations: createThreadNameMutationCoordinator(),
threadNameMutations: createKeyedOperationQueue(),
threadGoalOperations: createThreadGoalOperationCoordinator(),
runtimeSettingsCommitQueue: createKeyedOperationQueue(),
},

View file

@ -12,7 +12,6 @@ import type { Thread } from "../../../../src/domain/threads/model";
import { createThreadGoalOperationCoordinator } from "../../../../src/features/chat/application/threads/goal-actions";
import type { ChatRuntimeView, ChatViewRuntimeOwner, CodexChatHost } from "../../../../src/features/chat/host/contracts";
import type { ThreadCatalogEvent } from "../../../../src/features/threads/catalog/thread-catalog";
import { createThreadNameMutationCoordinator } from "../../../../src/features/threads/workflows/thread-name-mutation-coordinator";
import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../../../../src/settings/model";
import { createKeyedOperationQueue } from "../../../../src/shared/runtime/keyed-operation-queue";
import { notices } from "../../../mocks/obsidian";
@ -558,7 +557,7 @@ export function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexCha
activeThreads = threads;
emitActiveThreads();
},
threadNameMutations: overrides.threadNameMutations ?? createThreadNameMutationCoordinator(),
threadNameMutations: overrides.threadNameMutations ?? createKeyedOperationQueue(),
threadTitleTransport: {
persistedContext: vi.fn().mockResolvedValue(null),
generateTitle: vi.fn().mockResolvedValue(null),

View file

@ -6,9 +6,9 @@ import type { ObservedPaginatedResult } from "../../../src/app-server/query/obse
import type * as ThreadTitleGeneratorModule from "../../../src/app-server/services/thread-title-generation";
import type { Thread } from "../../../src/domain/threads/model";
import { createThreadOperationsTransport, createThreadTitleTransport } from "../../../src/features/threads/app-server/workflow-transports";
import { createThreadNameMutationCoordinator } from "../../../src/features/threads/workflows/thread-name-mutation-coordinator";
import type { ThreadsViewHost } from "../../../src/features/threads-view/session";
import { DEFAULT_SETTINGS } from "../../../src/settings/model";
import { createKeyedOperationQueue } from "../../../src/shared/runtime/keyed-operation-queue";
import { notices } from "../../mocks/obsidian";
import { deferred, waitForAsyncWork } from "../../support/async";
import { changeInputValue, installObsidianDomShims } from "../../support/dom";
@ -844,7 +844,7 @@ function threadsHost(overrides: Record<string, unknown> = {}) {
}),
},
vaultPath: "/vault",
threadNameMutations: createThreadNameMutationCoordinator(),
threadNameMutations: createKeyedOperationQueue(),
threadOperationsTransport: createThreadOperationsTransport(clientAccess),
threadTitleTransport: createThreadTitleTransport({
clientAccess,

View file

@ -1,37 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { createThreadNameMutationCoordinator } from "../../../../src/features/threads/workflows/thread-name-mutation-coordinator";
import { deferred } from "../../../support/async";
describe("ThreadNameMutationCoordinator", () => {
it("does not let a failed mutation block the next mutation for the same thread", async () => {
const coordinator = createThreadNameMutationCoordinator();
const firstMutation = deferred<void>();
const nextMutation = vi.fn().mockResolvedValue("saved");
const failed = coordinator.run("thread", async () => {
await firstMutation.promise;
throw new Error("Save failed.");
});
const failure = expect(failed).rejects.toThrow("Save failed.");
const next = coordinator.run("thread", nextMutation);
expect(nextMutation).not.toHaveBeenCalled();
firstMutation.resolve();
await failure;
await expect(next).resolves.toBe("saved");
expect(nextMutation).toHaveBeenCalledOnce();
});
it("does not serialize mutations for different threads", async () => {
const coordinator = createThreadNameMutationCoordinator();
const firstMutation = deferred<void>();
const blocked = coordinator.run("first", () => firstMutation.promise);
await expect(coordinator.run("second", async () => "saved")).resolves.toBe("saved");
firstMutation.resolve();
await blocked;
});
});

View file

@ -5,13 +5,13 @@ import type { ThreadRecord } from "../../../../src/app-server/protocol/thread";
import type { Thread } from "../../../../src/domain/threads/model";
import { createThreadOperationsTransport } from "../../../../src/features/threads/app-server/workflow-transports";
import type { ArchiveExportDestination } from "../../../../src/features/threads/workflows/archive-export";
import { createThreadNameMutationCoordinator } from "../../../../src/features/threads/workflows/thread-name-mutation-coordinator";
import {
type ArchiveThreadResult,
createThreadOperations,
type ThreadOperationsHost,
} from "../../../../src/features/threads/workflows/thread-operations";
import { DEFAULT_SETTINGS } from "../../../../src/settings/model";
import { createKeyedOperationQueue } from "../../../../src/shared/runtime/keyed-operation-queue";
import { deferred } from "../../../support/async";
import { legacyTurnContextManifestText } from "../../../support/legacy-turn-context-manifest";
@ -228,7 +228,7 @@ function operationsFixture(options: { client?: MockClient | null | (() => MockCl
return result;
},
}),
nameMutations: createThreadNameMutationCoordinator(),
nameMutations: createKeyedOperationQueue(),
archiveExport: {
settings: archiveExportSettings,
enabled: () => false,

View file

@ -0,0 +1,37 @@
import { describe, expect, it, vi } from "vitest";
import { createKeyedOperationQueue } from "../../../src/shared/runtime/keyed-operation-queue";
import { deferred } from "../../support/async";
describe("KeyedOperationQueue", () => {
it("does not let a failed operation block the next operation for the same key", async () => {
const queue = createKeyedOperationQueue<string>();
const firstOperation = deferred<void>();
const nextOperation = vi.fn().mockResolvedValue("saved");
const failed = queue.run("key", async () => {
await firstOperation.promise;
throw new Error("Save failed.");
});
const failure = expect(failed).rejects.toThrow("Save failed.");
const next = queue.run("key", nextOperation);
expect(nextOperation).not.toHaveBeenCalled();
firstOperation.resolve();
await failure;
await expect(next).resolves.toBe("saved");
expect(nextOperation).toHaveBeenCalledOnce();
});
it("does not serialize operations for different keys", async () => {
const queue = createKeyedOperationQueue<string>();
const firstOperation = deferred<void>();
const blocked = queue.run("first", () => firstOperation.promise);
await expect(queue.run("second", async () => "saved")).resolves.toBe("saved");
firstOperation.resolve();
await blocked;
});
});

View file

@ -8,7 +8,6 @@ import type { Thread } from "../../src/domain/threads/model";
import { createThreadGoalOperationCoordinator } from "../../src/features/chat/application/threads/goal-actions";
import type { CodexChatHost } from "../../src/features/chat/host/contracts";
import type { CodexChatView } from "../../src/features/chat/host/view.obsidian";
import { createThreadNameMutationCoordinator } from "../../src/features/threads/workflows/thread-name-mutation-coordinator";
import type CodexPanelPlugin from "../../src/main";
import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../../src/settings/model";
import { createKeyedOperationQueue } from "../../src/shared/runtime/keyed-operation-queue";
@ -129,7 +128,7 @@ function chatHostFixture(): CodexChatHost {
withClient: vi.fn(() => Promise.reject(new Error("Unexpected app-server client request."))),
},
appServerContext: { codexPath: settings.codexPath, vaultPath: "/vault" },
threadNameMutations: createThreadNameMutationCoordinator(),
threadNameMutations: createKeyedOperationQueue(),
threadTitleTransport: {
persistedContext: vi.fn().mockResolvedValue(null),
generateTitle: vi.fn().mockResolvedValue(null),