mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Separate settings app-server dynamic data
This commit is contained in:
parent
ca53d77fed
commit
f068607509
10 changed files with 255 additions and 92 deletions
|
|
@ -43,6 +43,10 @@
|
|||
"path": "./scripts/lint/no-generated-app-server-boundary-imports.grit",
|
||||
"includes": ["**/src/**/*.ts", "**/src/**/*.tsx", "!**/src/app-server/connection/**"]
|
||||
},
|
||||
{
|
||||
"path": "./scripts/lint/no-settings-app-server-boundary-imports.grit",
|
||||
"includes": ["**/src/settings/**/*.ts", "**/src/settings/**/*.tsx", "!**/src/settings/app-server/**"]
|
||||
},
|
||||
|
||||
// Shared source layering boundaries.
|
||||
{
|
||||
|
|
|
|||
18
scripts/lint/no-settings-app-server-boundary-imports.grit
Normal file
18
scripts/lint/no-settings-app-server-boundary-imports.grit
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
language js
|
||||
|
||||
private pattern js_module_reference() {
|
||||
or {
|
||||
JsImport(),
|
||||
JsExportNamedFromClause(),
|
||||
JsExportFromClause(),
|
||||
TsImportType(),
|
||||
JsImportCallExpression()
|
||||
}
|
||||
}
|
||||
|
||||
js_module_reference() as $stmt where {
|
||||
$stmt <: contains `$source` where {
|
||||
$source <: r"^[\"'](?:(?:\.\./)+app-server|src/app-server|\./app-server|(?:\.\./)+settings/app-server|src/settings/app-server)(?:/.*)?[\"']$"
|
||||
},
|
||||
register_diagnostic(span=$stmt, message="Settings modules must depend on settings-owned dynamic data ports instead of importing app-server modules or settings app-server adapters.", severity="error")
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import type { ThreadsViewPanelActivity } from "./features/threads-view/state";
|
|||
import { CodexThreadsView } from "./features/threads-view/view.obsidian";
|
||||
import { persistedTurnDiffViewState, type TurnDiffViewState } from "./features/turn-diff/model";
|
||||
import { CodexTurnDiffView } from "./features/turn-diff/view.obsidian";
|
||||
import { createSettingsAppServerDynamicData } from "./settings/app-server/dynamic-data";
|
||||
import type { CodexPanelSettingTabHost } from "./settings/host";
|
||||
import type { CodexPanelSettings } from "./settings/model";
|
||||
import { WorkspacePanelCoordinator } from "./workspace/panel-coordinator";
|
||||
|
|
@ -175,14 +176,16 @@ export class CodexPanelRuntime implements AppServerClientAccess {
|
|||
settingTabHost(): CodexPanelSettingTabHost {
|
||||
return {
|
||||
settings: this.options.settingsRef.settings,
|
||||
vaultPath: this.options.settingsRef.vaultPath,
|
||||
clientAccess: this,
|
||||
dynamicData: createSettingsAppServerDynamicData({
|
||||
vaultPath: this.options.settingsRef.vaultPath,
|
||||
clientAccess: this,
|
||||
appServerQueries: this.appServerSharedQueries,
|
||||
threadCatalog: this.threadCatalog,
|
||||
}),
|
||||
saveSettings: () => this.options.saveSettings(),
|
||||
refreshOpenViews: () => {
|
||||
this.refreshOpenViews();
|
||||
},
|
||||
appServerQueries: this.appServerSharedQueries,
|
||||
threadCatalog: this.threadCatalog,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
80
src/settings/app-server/dynamic-data.ts
Normal file
80
src/settings/app-server/dynamic-data.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import type { AppServerClient } from "../../app-server/connection/client";
|
||||
import type { AppServerClientAccess } from "../../app-server/connection/client-access";
|
||||
import { isStaleAppServerSharedQueryContextError } from "../../app-server/query/shared-queries";
|
||||
import type { ThreadCatalogArchivedReader, ThreadCatalogEventSink } from "../../app-server/query/thread-catalog";
|
||||
import { listHookCatalog, setHookItemEnabled, trustHookItem } from "../../app-server/services/catalog";
|
||||
import { restoreArchivedThread as restoreArchivedThreadOnAppServer } from "../../app-server/services/threads";
|
||||
import type { ModelMetadata } from "../../domain/catalog/metadata";
|
||||
import type { ObservedResultListener } from "../../shared/query/observed-result";
|
||||
import { type SettingsDynamicDataAccess, type SettingsHookCatalog, StaleSettingsDynamicDataContextError } from "../dynamic-data";
|
||||
|
||||
interface SettingsAppServerQueries {
|
||||
modelsSnapshot(): readonly ModelMetadata[] | null;
|
||||
observeModelsResult(listener: ObservedResultListener<readonly ModelMetadata[]>, options?: { emitCurrent?: boolean }): () => void;
|
||||
fetchModels(): Promise<readonly ModelMetadata[]>;
|
||||
refreshModels(): Promise<readonly ModelMetadata[]>;
|
||||
notifyContextChanged(): void;
|
||||
}
|
||||
|
||||
type SettingsArchivedThreadCatalog = ThreadCatalogArchivedReader & ThreadCatalogEventSink;
|
||||
|
||||
export interface SettingsAppServerDynamicDataOptions {
|
||||
vaultPath: string;
|
||||
clientAccess: AppServerClientAccess;
|
||||
appServerQueries: SettingsAppServerQueries;
|
||||
threadCatalog: SettingsArchivedThreadCatalog;
|
||||
}
|
||||
|
||||
export function createSettingsAppServerDynamicData(options: SettingsAppServerDynamicDataOptions): SettingsDynamicDataAccess {
|
||||
const withSettingsConnection = <T>(operation: (client: AppServerClient) => Promise<T>): Promise<T> =>
|
||||
options.clientAccess.withClient(operation, {
|
||||
serverRequests: { kind: "reject", message: "Codex Panel settings does not handle server requests." },
|
||||
});
|
||||
|
||||
return {
|
||||
modelsSnapshot: () => options.appServerQueries.modelsSnapshot(),
|
||||
observeModelsResult: (listener, observeOptions) => options.appServerQueries.observeModelsResult(listener, observeOptions),
|
||||
fetchModels: () => mapStaleContextError(() => options.appServerQueries.fetchModels()),
|
||||
refreshModels: () => mapStaleContextError(() => options.appServerQueries.refreshModels()),
|
||||
archivedThreadsSnapshot: () => options.threadCatalog.archivedSnapshot(),
|
||||
observeArchivedThreadsResult: (listener, observeOptions) => options.threadCatalog.observeArchived(listener, observeOptions),
|
||||
refreshArchivedThreads: () => mapStaleContextError(() => options.threadCatalog.refreshArchived()),
|
||||
loadHooks: () => withSettingsConnection((client) => loadSettingsHookCatalog(client, options.vaultPath)),
|
||||
trustHook: (hook) => withSettingsConnection((client) => trustHookItem(client, hook)),
|
||||
setHookEnabled: (hook, enabled) => withSettingsConnection((client) => setHookItemEnabled(client, hook, enabled)),
|
||||
restoreArchivedThread: async (threadId, mutationOptions) => {
|
||||
const thread = await withSettingsConnection((client) => restoreArchivedThreadOnAppServer(client, threadId));
|
||||
if (mutationOptions?.shouldPublish?.() ?? true) {
|
||||
options.threadCatalog.apply({ type: "thread-restored", thread });
|
||||
}
|
||||
return thread;
|
||||
},
|
||||
deleteArchivedThread: async (threadId, mutationOptions) => {
|
||||
await withSettingsConnection((client) => client.deleteThread(threadId));
|
||||
if (mutationOptions?.shouldPublish?.() ?? true) {
|
||||
options.threadCatalog.apply({ type: "thread-deleted", threadId });
|
||||
}
|
||||
},
|
||||
notifyContextChanged: () => {
|
||||
options.appServerQueries.notifyContextChanged();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function loadSettingsHookCatalog(client: AppServerClient, cwd: string): Promise<SettingsHookCatalog> {
|
||||
const hooks = await listHookCatalog(client, cwd);
|
||||
const hookCount = hooks.hooks.length;
|
||||
return {
|
||||
...hooks,
|
||||
status: `Loaded ${String(hookCount)} hook${hookCount === 1 ? "" : "s"}.`,
|
||||
};
|
||||
}
|
||||
|
||||
async function mapStaleContextError<T>(operation: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (isStaleAppServerSharedQueryContextError(error)) throw new StaleSettingsDynamicDataContextError();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
41
src/settings/dynamic-data.ts
Normal file
41
src/settings/dynamic-data.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import type { HookItem, ModelMetadata } from "../domain/catalog/metadata";
|
||||
import type { Thread } from "../domain/threads/model";
|
||||
import type { ObservedResultListener } from "../shared/query/observed-result";
|
||||
|
||||
export interface SettingsHookCatalog {
|
||||
hooks: readonly HookItem[];
|
||||
warnings: readonly string[];
|
||||
errors: readonly string[];
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface SettingsDynamicDataMutationOptions {
|
||||
shouldPublish?: () => boolean;
|
||||
}
|
||||
|
||||
export interface SettingsDynamicDataAccess {
|
||||
modelsSnapshot(): readonly ModelMetadata[] | null;
|
||||
observeModelsResult(listener: ObservedResultListener<readonly ModelMetadata[]>, options?: { emitCurrent?: boolean }): () => void;
|
||||
fetchModels(): Promise<readonly ModelMetadata[]>;
|
||||
refreshModels(): Promise<readonly ModelMetadata[]>;
|
||||
archivedThreadsSnapshot(): readonly Thread[] | null;
|
||||
observeArchivedThreadsResult(listener: ObservedResultListener<readonly Thread[]>, options?: { emitCurrent?: boolean }): () => void;
|
||||
refreshArchivedThreads(): Promise<readonly Thread[]>;
|
||||
loadHooks(): Promise<SettingsHookCatalog>;
|
||||
trustHook(hook: HookItem): Promise<void>;
|
||||
setHookEnabled(hook: HookItem, enabled: boolean): Promise<void>;
|
||||
restoreArchivedThread(threadId: string, options?: SettingsDynamicDataMutationOptions): Promise<Thread>;
|
||||
deleteArchivedThread(threadId: string, options?: SettingsDynamicDataMutationOptions): Promise<void>;
|
||||
notifyContextChanged(): void;
|
||||
}
|
||||
|
||||
export class StaleSettingsDynamicDataContextError extends Error {
|
||||
constructor() {
|
||||
super("Settings dynamic data context changed while loading Codex details.");
|
||||
this.name = "StaleSettingsDynamicDataContextError";
|
||||
}
|
||||
}
|
||||
|
||||
export function isStaleSettingsDynamicDataContextError(error: unknown): error is StaleSettingsDynamicDataContextError {
|
||||
return error instanceof StaleSettingsDynamicDataContextError;
|
||||
}
|
||||
|
|
@ -1,13 +1,10 @@
|
|||
import type { AppServerClient } from "../app-server/connection/client";
|
||||
import { isStaleAppServerSharedQueryContextError } from "../app-server/query/shared-queries";
|
||||
import { type HookCatalog, listHookCatalog, setHookItemEnabled, trustHookItem } from "../app-server/services/catalog";
|
||||
import { restoreArchivedThread as restoreArchivedThreadOnAppServer } from "../app-server/services/threads";
|
||||
import type { HookItem, ModelMetadata, ReasoningEffort } from "../domain/catalog/metadata";
|
||||
import { findModelMetadataByIdOrName, sortedModelMetadata, supportedEffortsForModelMetadata } from "../domain/catalog/metadata";
|
||||
import type { Thread } from "../domain/threads/model";
|
||||
import { threadArchiveDisplayTitle } from "../domain/threads/title";
|
||||
import type { ObservedResult } from "../shared/query/observed-result";
|
||||
import { observedValue } from "../shared/query/observed-result";
|
||||
import { isStaleSettingsDynamicDataContextError } from "./dynamic-data";
|
||||
import type { SettingsDynamicSectionsHost } from "./host";
|
||||
import {
|
||||
createSettingsDynamicSectionLifecycle,
|
||||
|
|
@ -15,19 +12,6 @@ import {
|
|||
transitionSettingsDynamicSectionLifecycle,
|
||||
} from "./lifecycle";
|
||||
|
||||
interface LoadedHookCatalog extends HookCatalog {
|
||||
status: string;
|
||||
}
|
||||
|
||||
async function loadHookCatalog(client: AppServerClient, cwd: string): Promise<LoadedHookCatalog> {
|
||||
const hooks = await listHookCatalog(client, cwd);
|
||||
const hookCount = hooks.hooks.length;
|
||||
return {
|
||||
...hooks,
|
||||
status: `Loaded ${String(hookCount)} hook${hookCount === 1 ? "" : "s"}.`,
|
||||
};
|
||||
}
|
||||
|
||||
interface SettingsDynamicSectionsControllerCallbacks {
|
||||
display(target: SettingsDynamicSectionsDisplayTarget): void;
|
||||
notify(message: string): void;
|
||||
|
|
@ -75,8 +59,8 @@ export class SettingsDynamicSectionsController {
|
|||
|
||||
activate(): void {
|
||||
if (this.unsubscribeModels) return;
|
||||
this.models = [...(this.host.appServerQueries.modelsSnapshot() ?? [])];
|
||||
const archivedThreads = this.host.threadCatalog.archivedSnapshot();
|
||||
this.models = [...(this.host.dynamicData.modelsSnapshot() ?? [])];
|
||||
const archivedThreads = this.host.dynamicData.archivedThreadsSnapshot();
|
||||
if (archivedThreads) {
|
||||
this.archivedThreads = [...archivedThreads];
|
||||
this.archivedThreadsLoaded = true;
|
||||
|
|
@ -86,13 +70,13 @@ export class SettingsDynamicSectionsController {
|
|||
operationToken: this.archivedThreadsOperationToken,
|
||||
});
|
||||
}
|
||||
this.unsubscribeModels = this.host.appServerQueries.observeModelsResult(
|
||||
this.unsubscribeModels = this.host.dynamicData.observeModelsResult(
|
||||
(result) => {
|
||||
this.receiveObservedModelsResult(result);
|
||||
},
|
||||
{ emitCurrent: false },
|
||||
);
|
||||
this.unsubscribeArchivedThreads = this.host.threadCatalog.observeArchived(
|
||||
this.unsubscribeArchivedThreads = this.host.dynamicData.observeArchivedThreadsResult(
|
||||
(result) => {
|
||||
this.receiveObservedArchivedThreadsResult(result);
|
||||
},
|
||||
|
|
@ -112,7 +96,7 @@ export class SettingsDynamicSectionsController {
|
|||
this.modelsOperationToken += 1;
|
||||
this.hooksOperationToken += 1;
|
||||
this.archivedThreadsOperationToken += 1;
|
||||
this.models = [...(this.host.appServerQueries.modelsSnapshot() ?? [])];
|
||||
this.models = [...(this.host.dynamicData.modelsSnapshot() ?? [])];
|
||||
this.modelsLifecycle = createSettingsDynamicSectionLifecycle();
|
||||
this.hooks = [];
|
||||
this.hookWarnings = [];
|
||||
|
|
@ -179,9 +163,9 @@ export class SettingsDynamicSectionsController {
|
|||
let failedCount = 0;
|
||||
try {
|
||||
const [modelsResult, hooksResult, archivedThreadsResult] = await Promise.allSettled([
|
||||
options.forceModels === false ? this.host.appServerQueries.fetchModels() : this.host.appServerQueries.refreshModels(),
|
||||
this.withSettingsConnection((client) => loadHookCatalog(client, this.host.vaultPath)),
|
||||
this.host.threadCatalog.refreshArchived(),
|
||||
options.forceModels === false ? this.host.dynamicData.fetchModels() : this.host.dynamicData.refreshModels(),
|
||||
this.host.dynamicData.loadHooks(),
|
||||
this.host.dynamicData.refreshArchivedThreads(),
|
||||
] as const);
|
||||
if (this.isStaleDynamicSectionsRefreshOperation(operationToken)) return;
|
||||
|
||||
|
|
@ -194,7 +178,7 @@ export class SettingsDynamicSectionsController {
|
|||
status: `Loaded ${String(modelsResult.value.length)} model${modelsResult.value.length === 1 ? "" : "s"}.`,
|
||||
operationToken: modelsOperationToken,
|
||||
});
|
||||
} else if (isStaleAppServerSharedQueryContextError(modelsResult.reason)) {
|
||||
} else if (isStaleSettingsDynamicDataContextError(modelsResult.reason)) {
|
||||
return;
|
||||
} else {
|
||||
failedCount += 1;
|
||||
|
|
@ -208,9 +192,9 @@ export class SettingsDynamicSectionsController {
|
|||
if (this.isStaleHooksOperation(hooksOperationToken)) {
|
||||
// A newer hooks operation owns this section.
|
||||
} else if (hooksResult.status === "fulfilled") {
|
||||
this.hooks = hooksResult.value.hooks;
|
||||
this.hookWarnings = hooksResult.value.warnings;
|
||||
this.hookErrors = hooksResult.value.errors;
|
||||
this.hooks = [...hooksResult.value.hooks];
|
||||
this.hookWarnings = [...hooksResult.value.warnings];
|
||||
this.hookErrors = [...hooksResult.value.errors];
|
||||
this.hooksLoaded = true;
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "loaded",
|
||||
|
|
@ -236,7 +220,7 @@ export class SettingsDynamicSectionsController {
|
|||
status: archivedThreadsStatus(archivedThreadsResult.value.length),
|
||||
operationToken: archivedThreadsOperationToken,
|
||||
});
|
||||
} else if (isStaleAppServerSharedQueryContextError(archivedThreadsResult.reason)) {
|
||||
} else if (isStaleSettingsDynamicDataContextError(archivedThreadsResult.reason)) {
|
||||
return;
|
||||
} else {
|
||||
failedCount += 1;
|
||||
|
|
@ -310,7 +294,7 @@ export class SettingsDynamicSectionsController {
|
|||
failureStatus: (error) => `Could not trust hook: ${errorMessage(error)}`,
|
||||
failureNotice: "Could not trust Codex hook.",
|
||||
operation: async (operationToken) => {
|
||||
await this.withSettingsConnection((client) => trustHookItem(client, hook));
|
||||
await this.host.dynamicData.trustHook(hook);
|
||||
if (this.isStaleHooksOperation(operationToken)) return;
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "loaded",
|
||||
|
|
@ -329,7 +313,7 @@ export class SettingsDynamicSectionsController {
|
|||
failureStatus: (error) => `Could not update hook: ${errorMessage(error)}`,
|
||||
failureNotice: "Could not update Codex hook.",
|
||||
operation: async (operationToken) => {
|
||||
await this.withSettingsConnection((client) => setHookItemEnabled(client, hook, enabled));
|
||||
await this.host.dynamicData.setHookEnabled(hook, enabled);
|
||||
if (this.isStaleHooksOperation(operationToken)) return;
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "loaded",
|
||||
|
|
@ -348,10 +332,11 @@ export class SettingsDynamicSectionsController {
|
|||
failureStatus: (error) => `Could not restore archived thread: ${errorMessage(error)}`,
|
||||
failureNotice: "Could not restore archived Codex thread.",
|
||||
operation: async (operationToken) => {
|
||||
const restoredThread = await this.withSettingsConnection((client) => restoreArchivedThreadOnAppServer(client, threadId));
|
||||
const restoredThread = await this.host.dynamicData.restoreArchivedThread(threadId, {
|
||||
shouldPublish: () => !this.isStaleArchivedThreadsOperation(operationToken),
|
||||
});
|
||||
if (this.isStaleArchivedThreadsOperation(operationToken)) return;
|
||||
this.archivedThreads = this.archivedThreads.filter((thread) => thread.id !== threadId);
|
||||
this.host.threadCatalog.apply({ type: "thread-restored", thread: restoredThread });
|
||||
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
||||
type: "loaded",
|
||||
status: `Restored "${threadArchiveDisplayTitle(restoredThread)}".`,
|
||||
|
|
@ -370,10 +355,11 @@ export class SettingsDynamicSectionsController {
|
|||
failureStatus: (error) => `Could not delete archived thread: ${errorMessage(error)}`,
|
||||
failureNotice: "Could not delete archived Codex thread.",
|
||||
operation: async (operationToken) => {
|
||||
await this.withSettingsConnection((client) => client.deleteThread(threadId));
|
||||
await this.host.dynamicData.deleteArchivedThread(threadId, {
|
||||
shouldPublish: () => !this.isStaleArchivedThreadsOperation(operationToken),
|
||||
});
|
||||
if (this.isStaleArchivedThreadsOperation(operationToken)) return;
|
||||
this.archivedThreads = this.archivedThreads.filter((thread) => thread.id !== threadId);
|
||||
this.host.threadCatalog.apply({ type: "thread-deleted", threadId });
|
||||
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
||||
type: "loaded",
|
||||
status: `Deleted "${title}".`,
|
||||
|
|
@ -407,11 +393,11 @@ export class SettingsDynamicSectionsController {
|
|||
failureStatus: (error) => `Could not load hooks: ${errorMessage(error)}`,
|
||||
failureNotice: "Could not load Codex hooks.",
|
||||
operation: async (operationToken) => {
|
||||
const hooks = await this.withSettingsConnection((client) => loadHookCatalog(client, this.host.vaultPath));
|
||||
const hooks = await this.host.dynamicData.loadHooks();
|
||||
if (this.isStaleHooksOperation(operationToken)) return;
|
||||
this.hooks = hooks.hooks;
|
||||
this.hookWarnings = hooks.warnings;
|
||||
this.hookErrors = hooks.errors;
|
||||
this.hooks = [...hooks.hooks];
|
||||
this.hookWarnings = [...hooks.warnings];
|
||||
this.hookErrors = [...hooks.errors];
|
||||
this.hooksLoaded = true;
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "loaded",
|
||||
|
|
@ -467,12 +453,6 @@ export class SettingsDynamicSectionsController {
|
|||
}
|
||||
}
|
||||
|
||||
private async withSettingsConnection<T>(operation: (client: AppServerClient) => Promise<T>): Promise<T> {
|
||||
return this.host.clientAccess.withClient(operation, {
|
||||
serverRequests: { kind: "reject", message: "Codex Panel settings does not handle server requests." },
|
||||
});
|
||||
}
|
||||
|
||||
private nextDynamicSectionsRefreshOperationToken(): number {
|
||||
this.dynamicSectionsRefreshOperationToken += 1;
|
||||
return this.dynamicSectionsRefreshOperationToken;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,9 @@
|
|||
import type { AppServerClientAccess } from "../app-server/connection/client-access";
|
||||
import type { ThreadCatalogArchivedReader, ThreadCatalogEventSink } from "../app-server/query/thread-catalog";
|
||||
import type { ModelMetadata } from "../domain/catalog/metadata";
|
||||
import type { ObservedResultListener } from "../shared/query/observed-result";
|
||||
import type { SettingsDynamicDataAccess } from "./dynamic-data";
|
||||
import type { CodexPanelSettings } from "./model";
|
||||
|
||||
interface SettingsAppServerQueries {
|
||||
modelsSnapshot(): readonly ModelMetadata[] | null;
|
||||
observeModelsResult(listener: ObservedResultListener<readonly ModelMetadata[]>, options?: { emitCurrent?: boolean }): () => void;
|
||||
fetchModels(): Promise<readonly ModelMetadata[]>;
|
||||
refreshModels(): Promise<readonly ModelMetadata[]>;
|
||||
notifyContextChanged(): void;
|
||||
}
|
||||
|
||||
export interface SettingsDynamicSectionsHost {
|
||||
settings: CodexPanelSettings;
|
||||
vaultPath: string;
|
||||
clientAccess: AppServerClientAccess;
|
||||
appServerQueries: SettingsAppServerQueries;
|
||||
threadCatalog: ThreadCatalogArchivedReader & ThreadCatalogEventSink;
|
||||
dynamicData: SettingsDynamicDataAccess;
|
||||
}
|
||||
|
||||
export interface CodexPanelSettingTabHost extends SettingsDynamicSectionsHost {
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ export class CodexPanelSettingTab extends PluginSettingTab {
|
|||
this.plugin.settings.codexPath = codexPath;
|
||||
await this.plugin.saveSettings();
|
||||
this.dynamicSections.resetDynamicSectionContext();
|
||||
this.plugin.appServerQueries.notifyContextChanged();
|
||||
this.plugin.dynamicData.notifyContextChanged();
|
||||
this.plugin.refreshOpenViews();
|
||||
this.renderSettingsShell();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ const APP_SERVER_ROOT_MODULE_IMPORT_MESSAGE =
|
|||
"Import app-server boundary modules from responsibility subfolders, not src/app-server root modules.";
|
||||
const APP_SERVER_SUBFOLDER_ROOT_IMPORT_MESSAGE =
|
||||
"App-server subfolders must not import sibling root modules; move the dependency into a responsibility subfolder.";
|
||||
const SETTINGS_APP_SERVER_BOUNDARY_MESSAGE =
|
||||
"Settings modules must depend on settings-owned dynamic data ports instead of importing app-server modules or settings app-server adapters.";
|
||||
const CHAT_APPLICATION_OUTER_LAYER_MESSAGE =
|
||||
"Chat application modules must not import app-server, host, panel, presentation, or UI layers; expose state and workflow contracts instead.";
|
||||
const CHAT_APP_SERVER_OUTER_LAYER_MESSAGE = "Chat app-server adapters must not import chat host, panel, presentation, or UI layers.";
|
||||
|
|
@ -826,6 +828,14 @@ export const value = statusText;
|
|||
]);
|
||||
});
|
||||
|
||||
it("keeps settings app-server access behind settings app-server adapters", async () => {
|
||||
const report = await appServerBoundaryPolicyReport();
|
||||
|
||||
expect(pluginMessages(report, "src/settings/dynamic-sections-controller.ts")).toEqual([SETTINGS_APP_SERVER_BOUNDARY_MESSAGE]);
|
||||
expect(pluginMessages(report, "src/settings/adapter-leak.ts")).toEqual([SETTINGS_APP_SERVER_BOUNDARY_MESSAGE]);
|
||||
expect(pluginDiagnostics(report, "src/settings/app-server/dynamic-data.ts")).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps app-server root from becoming a boundary escape hatch", async () => {
|
||||
const cwd = await tempBiomeWorkspace([
|
||||
"no-responsibility-root-module-files.grit",
|
||||
|
|
@ -1056,6 +1066,7 @@ async function createAppServerBoundaryPolicyReport() {
|
|||
"no-lower-level-feature-imports.grit",
|
||||
"no-app-server-connection-boundary-imports.grit",
|
||||
"no-app-server-projection-rpcs.grit",
|
||||
"no-settings-app-server-boundary-imports.grit",
|
||||
]);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/features/chat/domain/generated-thread.ts"),
|
||||
|
|
@ -1317,6 +1328,30 @@ export async function read(client: AppServerClient): Promise<void> {
|
|||
import type { AppServerClient } from "../../../app-server/connection/client";
|
||||
|
||||
export type AppServerThreadResumeClient = Pick<AppServerClient, "resumeThread">;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/settings/dynamic-sections-controller.ts"),
|
||||
`
|
||||
import { listHookCatalog } from "../app-server/services/catalog";
|
||||
|
||||
export const load = listHookCatalog;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/settings/adapter-leak.ts"),
|
||||
`
|
||||
import { createSettingsAppServerDynamicData } from "./app-server/dynamic-data";
|
||||
|
||||
export const create = createSettingsAppServerDynamicData;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/settings/app-server/dynamic-data.ts"),
|
||||
`
|
||||
import { listHookCatalog } from "../../app-server/services/catalog";
|
||||
|
||||
export const load = listHookCatalog;
|
||||
`.trimStart(),
|
||||
);
|
||||
|
||||
|
|
@ -1352,6 +1387,9 @@ export type AppServerThreadResumeClient = Pick<AppServerClient, "resumeThread">;
|
|||
"src/features/chat/application/threads/history.ts",
|
||||
"src/app-server/services/threads.ts",
|
||||
"src/features/chat/host/connection-bundle.ts",
|
||||
"src/settings/dynamic-sections-controller.ts",
|
||||
"src/settings/adapter-leak.ts",
|
||||
"src/settings/app-server/dynamic-data.ts",
|
||||
],
|
||||
cwd,
|
||||
);
|
||||
|
|
@ -1377,6 +1415,7 @@ async function tempBiomeWorkspace(plugins) {
|
|||
await mkdir(path.join(cwd, "src/features/threads-view"), { recursive: true });
|
||||
await mkdir(path.join(cwd, "src/features/turn-diff"), { recursive: true });
|
||||
await mkdir(path.join(cwd, "src/settings"), { recursive: true });
|
||||
await mkdir(path.join(cwd, "src/settings/app-server"), { recursive: true });
|
||||
await mkdir(path.join(cwd, "src/domain/threads"), { recursive: true });
|
||||
await mkdir(path.join(cwd, "src/app-server/connection"), { recursive: true });
|
||||
await mkdir(path.join(cwd, "src/app-server/protocol"), { recursive: true });
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AppServerClient } from "../../src/app-server/connection/client";
|
||||
import type { AppServerClientAccessOptions } from "../../src/app-server/connection/client-access";
|
||||
import type { CatalogHookMetadata, CatalogModel } from "../../src/app-server/protocol/catalog";
|
||||
import { modelMetadataFromCatalogModels } from "../../src/app-server/protocol/catalog";
|
||||
import type { ThreadRecord } from "../../src/app-server/protocol/thread";
|
||||
import type { ThreadCatalogEvent } from "../../src/app-server/query/thread-catalog";
|
||||
import type { ModelMetadata, ReasoningEffort } from "../../src/domain/catalog/metadata";
|
||||
import type { Thread } from "../../src/domain/threads/model";
|
||||
import { createSettingsAppServerDynamicData } from "../../src/settings/app-server/dynamic-data";
|
||||
import type { SettingsDynamicDataAccess } from "../../src/settings/dynamic-data";
|
||||
import { SettingsDynamicSectionsController, type SettingsDynamicSectionsSnapshot } from "../../src/settings/dynamic-sections-controller";
|
||||
import type { CodexPanelSettingTabHost } from "../../src/settings/host";
|
||||
import { CodexPanelSettingTab } from "../../src/settings/tab.obsidian";
|
||||
|
|
@ -952,14 +956,15 @@ function newSettingsTab(
|
|||
modelsSnapshot?: ModelMetadata[];
|
||||
fetchModels?: () => Promise<readonly ModelMetadata[]>;
|
||||
refreshModels?: () => Promise<readonly ModelMetadata[]>;
|
||||
observeModels?: CodexPanelSettingTabHost["appServerQueries"]["observeModelsResult"];
|
||||
observeModels?: SettingsDynamicDataAccess["observeModelsResult"];
|
||||
notifyContextChanged?: () => void;
|
||||
refreshOpenViews?: () => void;
|
||||
archivedThreads?: Thread[];
|
||||
archivedSnapshot?: Thread[] | null;
|
||||
refreshArchived?: () => Promise<readonly Thread[]>;
|
||||
observeArchived?: CodexPanelSettingTabHost["threadCatalog"]["observeArchived"];
|
||||
applyThreadCatalogEvent?: CodexPanelSettingTabHost["threadCatalog"]["apply"];
|
||||
observeArchived?: SettingsDynamicDataAccess["observeArchivedThreadsResult"];
|
||||
applyThreadCatalogEvent?: (event: ThreadCatalogEvent) => void;
|
||||
dynamicData?: SettingsDynamicDataAccess;
|
||||
settings?: Partial<{
|
||||
threadNamingModel: string | null;
|
||||
threadNamingEffort: string | null;
|
||||
|
|
@ -978,14 +983,15 @@ function settingsTabHost(
|
|||
modelsSnapshot?: ModelMetadata[];
|
||||
fetchModels?: () => Promise<readonly ModelMetadata[]>;
|
||||
refreshModels?: () => Promise<readonly ModelMetadata[]>;
|
||||
observeModels?: CodexPanelSettingTabHost["appServerQueries"]["observeModelsResult"];
|
||||
observeModels?: SettingsDynamicDataAccess["observeModelsResult"];
|
||||
notifyContextChanged?: () => void;
|
||||
refreshOpenViews?: () => void;
|
||||
archivedThreads?: Thread[];
|
||||
archivedSnapshot?: Thread[] | null;
|
||||
refreshArchived?: () => Promise<readonly Thread[]>;
|
||||
observeArchived?: CodexPanelSettingTabHost["threadCatalog"]["observeArchived"];
|
||||
applyThreadCatalogEvent?: CodexPanelSettingTabHost["threadCatalog"]["apply"];
|
||||
observeArchived?: SettingsDynamicDataAccess["observeArchivedThreadsResult"];
|
||||
applyThreadCatalogEvent?: (event: ThreadCatalogEvent) => void;
|
||||
dynamicData?: SettingsDynamicDataAccess;
|
||||
settings?: Partial<{
|
||||
threadNamingModel: string | null;
|
||||
threadNamingEffort: string | null;
|
||||
|
|
@ -1009,29 +1015,35 @@ function settingsTabHost(
|
|||
archiveExportFilenameTemplate: "{{date}} {{time}} {{title}} {{shortId}}.md",
|
||||
archiveExportTags: "",
|
||||
};
|
||||
const appServerQueries = {
|
||||
modelsSnapshot: vi.fn(() => options.modelsSnapshot ?? []),
|
||||
fetchModels: options.fetchModels ?? vi.fn().mockResolvedValue(options.modelsSnapshot ?? []),
|
||||
refreshModels: options.refreshModels ?? vi.fn().mockResolvedValue(options.modelsSnapshot ?? []),
|
||||
observeModelsResult: options.observeModels ?? vi.fn(() => () => undefined),
|
||||
notifyContextChanged: options.notifyContextChanged ?? vi.fn(),
|
||||
};
|
||||
const threadCatalog = {
|
||||
archivedSnapshot: vi.fn(() => options.archivedSnapshot ?? null),
|
||||
loadArchived: vi.fn().mockResolvedValue(options.archivedThreads ?? defaultArchivedThreads),
|
||||
refreshArchived: options.refreshArchived ?? vi.fn().mockResolvedValue(options.archivedThreads ?? defaultArchivedThreads),
|
||||
observeArchived: options.observeArchived ?? vi.fn(() => () => undefined),
|
||||
apply: options.applyThreadCatalogEvent ?? vi.fn(),
|
||||
};
|
||||
return {
|
||||
settings,
|
||||
vaultPath: "/vault",
|
||||
clientAccess: {
|
||||
withClient: <T>(operation: (client: never) => Promise<T>, clientOptions?: AppServerClientAccessOptions) =>
|
||||
withShortLivedAppServerClientMock(settings.codexPath, "/vault", operation, clientOptions) as Promise<T>,
|
||||
},
|
||||
dynamicData:
|
||||
options.dynamicData ??
|
||||
createSettingsAppServerDynamicData({
|
||||
vaultPath: "/vault",
|
||||
clientAccess: {
|
||||
withClient: <T>(operation: (client: AppServerClient) => Promise<T>, clientOptions?: AppServerClientAccessOptions) =>
|
||||
withShortLivedAppServerClientMock(settings.codexPath, "/vault", operation, clientOptions) as Promise<T>,
|
||||
},
|
||||
appServerQueries,
|
||||
threadCatalog,
|
||||
}),
|
||||
saveSettings: options.saveSettings ?? vi.fn().mockResolvedValue(undefined),
|
||||
refreshOpenViews: options.refreshOpenViews ?? vi.fn(),
|
||||
appServerQueries: {
|
||||
modelsSnapshot: vi.fn(() => options.modelsSnapshot ?? []),
|
||||
fetchModels: options.fetchModels ?? vi.fn().mockResolvedValue(options.modelsSnapshot ?? []),
|
||||
refreshModels: options.refreshModels ?? vi.fn().mockResolvedValue(options.modelsSnapshot ?? []),
|
||||
observeModelsResult: options.observeModels ?? vi.fn(() => () => undefined),
|
||||
notifyContextChanged: options.notifyContextChanged ?? vi.fn(),
|
||||
},
|
||||
threadCatalog: {
|
||||
archivedSnapshot: vi.fn(() => options.archivedSnapshot ?? null),
|
||||
loadArchived: vi.fn().mockResolvedValue(options.archivedThreads ?? defaultArchivedThreads),
|
||||
refreshArchived: options.refreshArchived ?? vi.fn().mockResolvedValue(options.archivedThreads ?? defaultArchivedThreads),
|
||||
observeArchived: options.observeArchived ?? vi.fn(() => () => undefined),
|
||||
apply: options.applyThreadCatalogEvent ?? vi.fn(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue