mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
327 lines
13 KiB
TypeScript
327 lines
13 KiB
TypeScript
import { type App, Notice, type Plugin, PluginSettingTab } from "obsidian";
|
|
|
|
import { DEFAULT_CODEX_PATH } from "../constants";
|
|
import type { ReasoningEffort } from "../domain/catalog/metadata";
|
|
import { listenDomEvent } from "../shared/dom/events.dom";
|
|
import { renderUiRoot, unmountUiRoot } from "../shared/dom/preact-root.dom";
|
|
import { SettingsDynamicSectionsController } from "./dynamic-sections-controller";
|
|
import type { CodexPanelSettingTabHost } from "./host";
|
|
import type { CodexPanelSettings } from "./model";
|
|
import {
|
|
DEFAULT_ARCHIVE_EXPORT_FILENAME_TEMPLATE,
|
|
DEFAULT_ARCHIVE_EXPORT_FOLDER_TEMPLATE,
|
|
DEFAULT_ATTACHMENT_FOLDER,
|
|
DEFAULT_CLIP_FILENAME_TEMPLATE,
|
|
DEFAULT_CLIP_FOLDER,
|
|
} from "./model";
|
|
import type { SettingsSectionsState } from "./section-state";
|
|
import { SettingsTabShell } from "./tab-shell";
|
|
|
|
const SETTINGS_INTRO_TEXT = "Codex Panel stores panel preferences only. Runtime settings still come from Codex.";
|
|
|
|
export class CodexPanelSettingTab extends PluginSettingTab {
|
|
private readonly dynamicSections: SettingsDynamicSectionsController;
|
|
private lastSavedSettings: CodexPanelSettings;
|
|
private archivedDeleteConfirmThreadId: string | null = null;
|
|
private disposeOutsidePointer: (() => void) | null = null;
|
|
private readonly cancelArchivedDeleteConfirmOnOutsidePointer = (event: PointerEvent): void => {
|
|
if (!this.archivedDeleteConfirmThreadId) return;
|
|
const target = event.target;
|
|
const viewWindow = this.containerEl.ownerDocument.defaultView;
|
|
if (viewWindow && target instanceof viewWindow.Element) {
|
|
const deleteConfirm = target.closest(".codex-panel-settings__archived-row--delete-confirming");
|
|
if (deleteConfirm && this.containerEl.contains(deleteConfirm)) return;
|
|
}
|
|
this.archivedDeleteConfirmThreadId = null;
|
|
this.renderSettingsShell();
|
|
};
|
|
|
|
constructor(
|
|
app: App,
|
|
owner: Plugin,
|
|
private readonly plugin: CodexPanelSettingTabHost,
|
|
) {
|
|
super(app, owner);
|
|
this.lastSavedSettings = { ...plugin.settings };
|
|
this.dynamicSections = new SettingsDynamicSectionsController(plugin, {
|
|
display: () => {
|
|
this.renderSettingsShell();
|
|
},
|
|
notify: (message) => {
|
|
new Notice(message);
|
|
},
|
|
});
|
|
}
|
|
|
|
display(): void {
|
|
this.dynamicSections.activate();
|
|
this.renderSettingsTab({ autoLoadDynamicSections: true });
|
|
}
|
|
|
|
override hide(): void {
|
|
this.disposeOutsidePointer?.();
|
|
this.disposeOutsidePointer = null;
|
|
this.archivedDeleteConfirmThreadId = null;
|
|
this.dynamicSections.dispose();
|
|
unmountUiRoot(this.containerEl);
|
|
super.hide();
|
|
}
|
|
|
|
private renderSettingsTab(options: { autoLoadDynamicSections: boolean }): void {
|
|
const { containerEl } = this;
|
|
containerEl.addClass("codex-panel-settings");
|
|
this.disposeOutsidePointer?.();
|
|
this.disposeOutsidePointer = listenDomEvent(containerEl, "pointerdown", this.cancelArchivedDeleteConfirmOnOutsidePointer);
|
|
|
|
this.renderSettingsShell();
|
|
|
|
if (options.autoLoadDynamicSections) this.maybeAutoLoadDynamicSections();
|
|
}
|
|
|
|
private renderSettingsShell(): void {
|
|
renderUiRoot(
|
|
this.containerEl,
|
|
<SettingsTabShell
|
|
introText={SETTINGS_INTRO_TEXT}
|
|
dynamicSectionsLoading={this.dynamicSections.isLoading()}
|
|
panel={{
|
|
codexPath: this.plugin.settings.codexPath,
|
|
showToolbar: this.plugin.settings.showToolbar,
|
|
sendShortcut: this.plugin.settings.sendShortcut,
|
|
scrollThreadFromComposerEdges: this.plugin.settings.scrollThreadFromComposerEdges,
|
|
referenceActiveNoteOnSend: this.plugin.settings.referenceActiveNoteOnSend,
|
|
attachmentFolder: this.plugin.settings.attachmentFolder,
|
|
clipFolder: this.plugin.settings.clipFolder,
|
|
clipFilenameTemplate: this.plugin.settings.clipFilenameTemplate,
|
|
clipTags: this.plugin.settings.clipTags,
|
|
}}
|
|
sections={this.settingsSectionsState()}
|
|
actions={{
|
|
refreshDynamicSections: () => {
|
|
void this.dynamicSections.refreshDynamicSections();
|
|
},
|
|
setCodexPath: (value) => {
|
|
void this.setCodexPath(value);
|
|
},
|
|
setShowToolbar: (value) => {
|
|
void this.setShowToolbar(value);
|
|
},
|
|
setSendShortcut: (value) => {
|
|
void this.setSendShortcut(value);
|
|
},
|
|
setScrollThreadFromComposerEdges: (value) => {
|
|
void this.setScrollThreadFromComposerEdges(value);
|
|
},
|
|
setReferenceActiveNoteOnSend: (value) => {
|
|
void this.setReferenceActiveNoteOnSend(value);
|
|
},
|
|
setAttachmentFolder: (value) => {
|
|
void this.setAttachmentFolder(value);
|
|
},
|
|
setClipFolder: (value) => {
|
|
void this.setClipFolder(value);
|
|
},
|
|
setClipFilenameTemplate: (value) => {
|
|
void this.setClipFilenameTemplate(value);
|
|
},
|
|
setClipTags: (value) => {
|
|
void this.setClipTags(value);
|
|
},
|
|
}}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
private settingsSectionsState(): SettingsSectionsState {
|
|
const dynamicSections = this.dynamicSections.snapshot();
|
|
return {
|
|
helper: {
|
|
threadNamingModel: this.plugin.settings.threadNamingModel,
|
|
threadNamingEffort: this.plugin.settings.threadNamingEffort,
|
|
rewriteSelectionModel: this.plugin.settings.rewriteSelectionModel,
|
|
rewriteSelectionEffort: this.plugin.settings.rewriteSelectionEffort,
|
|
models: this.dynamicSections.modelMetadata(),
|
|
modelLoadFailed: dynamicSections.modelsLifecycle.kind === "failed",
|
|
modelStatus: dynamicSections.modelsLifecycle.status,
|
|
onThreadNamingModelChange: (value) => void this.setThreadNamingModel(value),
|
|
onThreadNamingEffortChange: (value) => void this.setThreadNamingEffort(value),
|
|
onRewriteSelectionModelChange: (value) => void this.setRewriteSelectionModel(value),
|
|
onRewriteSelectionEffortChange: (value) => void this.setRewriteSelectionEffort(value),
|
|
},
|
|
archived: {
|
|
exportEnabled: this.plugin.settings.archiveExportEnabled,
|
|
exportFolderTemplate: this.plugin.settings.archiveExportFolderTemplate,
|
|
exportFilenameTemplate: this.plugin.settings.archiveExportFilenameTemplate,
|
|
exportTags: this.plugin.settings.archiveExportTags,
|
|
threads: dynamicSections.archivedThreads,
|
|
contentAvailable:
|
|
dynamicSections.archivedThreadsLifecycle.kind === "loaded" ||
|
|
(dynamicSections.archivedThreadsLifecycle.kind === "loading" && dynamicSections.archivedThreadsLoaded),
|
|
loaded: dynamicSections.archivedThreadsLifecycle.kind === "loaded",
|
|
loading: dynamicSections.archivedThreadsLifecycle.kind === "loading",
|
|
status: dynamicSections.archivedThreadsLifecycle.status,
|
|
deleteConfirmThreadId: this.archivedDeleteConfirmThreadId,
|
|
onExportEnabledChange: (enabled) => void this.setArchiveExportEnabled(enabled),
|
|
onExportFolderTemplateChange: (value) => void this.setArchiveExportFolderTemplate(value),
|
|
onExportFilenameTemplateChange: (value) => void this.setArchiveExportFilenameTemplate(value),
|
|
onExportTagsChange: (value) => void this.setArchiveExportTags(value),
|
|
onRestore: (threadId) => {
|
|
this.archivedDeleteConfirmThreadId = null;
|
|
this.renderSettingsShell();
|
|
void this.dynamicSections.restoreArchivedThread(threadId);
|
|
},
|
|
onStartDelete: (threadId) => {
|
|
this.archivedDeleteConfirmThreadId = threadId;
|
|
this.renderSettingsShell();
|
|
},
|
|
onDelete: (threadId) => {
|
|
this.archivedDeleteConfirmThreadId = null;
|
|
this.renderSettingsShell();
|
|
void this.dynamicSections.deleteArchivedThread(threadId);
|
|
},
|
|
},
|
|
hooks: {
|
|
hooks: dynamicSections.hooks,
|
|
warnings: dynamicSections.hookWarnings,
|
|
errors: dynamicSections.hookErrors,
|
|
contentAvailable:
|
|
dynamicSections.hooksLifecycle.kind === "loaded" ||
|
|
(dynamicSections.hooksLifecycle.kind === "loading" && dynamicSections.hooksLoaded),
|
|
loaded: dynamicSections.hooksLifecycle.kind === "loaded",
|
|
loading: dynamicSections.hooksLifecycle.kind === "loading",
|
|
status: dynamicSections.hooksLifecycle.status,
|
|
onTrust: (hook) => void this.dynamicSections.trustHook(hook),
|
|
onToggleEnabled: (hook, enabled) => void this.dynamicSections.setHookEnabled(hook, enabled),
|
|
},
|
|
};
|
|
}
|
|
|
|
private maybeAutoLoadDynamicSections(): void {
|
|
this.dynamicSections.maybeAutoLoadDynamicSections();
|
|
}
|
|
|
|
private async setCodexPath(value: string): Promise<void> {
|
|
const codexPath = value.trim() || DEFAULT_CODEX_PATH;
|
|
if (codexPath === this.plugin.settings.codexPath) return;
|
|
this.plugin.settings.codexPath = codexPath;
|
|
if (!(await this.persistSettings())) return;
|
|
this.dynamicSections.resetDynamicSectionContext();
|
|
this.plugin.dynamicData.notifyContextChanged();
|
|
this.plugin.refreshOpenViews();
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setShowToolbar(value: boolean): Promise<void> {
|
|
this.plugin.settings.showToolbar = value;
|
|
if (!(await this.persistSettings())) return;
|
|
this.plugin.refreshOpenViews();
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setSendShortcut(value: "enter" | "mod-enter"): Promise<void> {
|
|
this.plugin.settings.sendShortcut = value;
|
|
if (!(await this.persistSettings())) return;
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setScrollThreadFromComposerEdges(value: boolean): Promise<void> {
|
|
this.plugin.settings.scrollThreadFromComposerEdges = value;
|
|
if (!(await this.persistSettings())) return;
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setReferenceActiveNoteOnSend(value: boolean): Promise<void> {
|
|
this.plugin.settings.referenceActiveNoteOnSend = value;
|
|
if (!(await this.persistSettings())) return;
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setAttachmentFolder(value: string): Promise<void> {
|
|
this.plugin.settings.attachmentFolder = value.trim() || DEFAULT_ATTACHMENT_FOLDER;
|
|
if (!(await this.persistSettings())) return;
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setClipFolder(value: string): Promise<void> {
|
|
this.plugin.settings.clipFolder = value.trim() || DEFAULT_CLIP_FOLDER;
|
|
if (!(await this.persistSettings())) return;
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setClipFilenameTemplate(value: string): Promise<void> {
|
|
this.plugin.settings.clipFilenameTemplate = value.trim() || DEFAULT_CLIP_FILENAME_TEMPLATE;
|
|
if (!(await this.persistSettings())) return;
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setClipTags(value: string): Promise<void> {
|
|
this.plugin.settings.clipTags = value.trim();
|
|
if (!(await this.persistSettings())) return;
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setArchiveExportEnabled(enabled: boolean): Promise<void> {
|
|
this.plugin.settings.archiveExportEnabled = enabled;
|
|
if (!(await this.persistSettings())) return;
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setArchiveExportFolderTemplate(value: string): Promise<void> {
|
|
this.plugin.settings.archiveExportFolderTemplate = value.trim() || DEFAULT_ARCHIVE_EXPORT_FOLDER_TEMPLATE;
|
|
if (!(await this.persistSettings())) return;
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setArchiveExportFilenameTemplate(value: string): Promise<void> {
|
|
this.plugin.settings.archiveExportFilenameTemplate = value.trim() || DEFAULT_ARCHIVE_EXPORT_FILENAME_TEMPLATE;
|
|
if (!(await this.persistSettings())) return;
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setArchiveExportTags(value: string): Promise<void> {
|
|
this.plugin.settings.archiveExportTags = value.trim();
|
|
if (!(await this.persistSettings())) return;
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setThreadNamingModel(value: string | null): Promise<void> {
|
|
this.plugin.settings.threadNamingModel = value;
|
|
if (!this.dynamicSections.namingEffortSupported(this.plugin.settings.threadNamingEffort)) {
|
|
this.plugin.settings.threadNamingEffort = null;
|
|
}
|
|
if (!(await this.persistSettings())) return;
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setThreadNamingEffort(value: ReasoningEffort | null): Promise<void> {
|
|
this.plugin.settings.threadNamingEffort = value;
|
|
if (!(await this.persistSettings())) return;
|
|
}
|
|
|
|
private async setRewriteSelectionModel(value: string | null): Promise<void> {
|
|
this.plugin.settings.rewriteSelectionModel = value;
|
|
if (!this.dynamicSections.rewriteSelectionEffortSupported(this.plugin.settings.rewriteSelectionEffort)) {
|
|
this.plugin.settings.rewriteSelectionEffort = null;
|
|
}
|
|
if (!(await this.persistSettings())) return;
|
|
this.renderSettingsShell();
|
|
}
|
|
|
|
private async setRewriteSelectionEffort(value: ReasoningEffort | null): Promise<void> {
|
|
this.plugin.settings.rewriteSelectionEffort = value;
|
|
await this.persistSettings();
|
|
}
|
|
|
|
private async persistSettings(): Promise<boolean> {
|
|
try {
|
|
await this.plugin.saveSettings();
|
|
this.lastSavedSettings = { ...this.plugin.settings };
|
|
return true;
|
|
} catch (error) {
|
|
Object.assign(this.plugin.settings, this.lastSavedSettings);
|
|
new Notice(`Failed to save Codex Panel settings: ${error instanceof Error ? error.message : String(error)}`);
|
|
this.renderSettingsShell();
|
|
return false;
|
|
}
|
|
}
|
|
}
|