mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
570 lines
24 KiB
TypeScript
570 lines
24 KiB
TypeScript
import { type App, Notice, type Plugin, PluginSettingTab, Setting, setIcon } from "obsidian";
|
|
|
|
import type { AppServerClient } from "../app-server/connection/client";
|
|
import { restoreArchivedThread, setHookItemEnabled, trustHookItem } from "../app-server/services/resource-operations";
|
|
import { withShortLivedAppServerClient } from "../app-server/connection/short-lived-client";
|
|
import { DEFAULT_CODEX_PATH } from "../constants";
|
|
import type { ReasoningEffort } from "../domain/catalog/metadata";
|
|
import type { HookItem, ModelMetadata } from "../domain/catalog/metadata";
|
|
import type { Thread } from "../domain/threads/model";
|
|
import { supportedEffortsForModelMetadata } from "../domain/catalog/metadata";
|
|
import { findModelMetadataByIdOrName, sortedModelMetadata } from "../domain/catalog/metadata";
|
|
import { archivedThreadDisplayTitle } from "../domain/threads/model";
|
|
import { errorMessage } from "../utils";
|
|
import {
|
|
createSettingsDynamicSectionLifecycle,
|
|
transitionSettingsDynamicSectionLifecycle,
|
|
transitionSettingsDataRefreshLifecycle,
|
|
type SettingsDataRefreshLifecycleState,
|
|
} from "./lifecycle";
|
|
import { loadHookData, loadSettingsData } from "./app-server-data";
|
|
import { renderArchivedThreadSection, renderHookSection } from "./dynamic-sections";
|
|
import type { CodexPanelSettings } from "./model";
|
|
|
|
const CODEX_DEFAULT_VALUE = "__codex-default__";
|
|
const SETTINGS_INTRO_TEXT =
|
|
"Codex Panel stores only panel preferences. Models, sandboxing, approvals, MCP servers, hooks, and network access still come from Codex config.";
|
|
const SEND_SHORTCUT_LABELS = {
|
|
enter: "Enter",
|
|
"mod-enter": "Cmd/Ctrl+Enter",
|
|
} as const;
|
|
|
|
function renderSettingsHeading(containerEl: HTMLElement, name: string): void {
|
|
new Setting(containerEl).setClass("codex-panel-settings__section-heading").setHeading().setName(name);
|
|
}
|
|
|
|
export class CodexPanelSettingTab extends PluginSettingTab {
|
|
private settingsDataAutoLoadStarted = false;
|
|
private settingsDynamicOperationId = 0;
|
|
private settingsDataRefreshLifecycle: SettingsDataRefreshLifecycleState = { kind: "idle" };
|
|
private archivedThreads: Thread[] = [];
|
|
private archivedThreadsLifecycle = createSettingsDynamicSectionLifecycle();
|
|
private hooks: HookItem[] = [];
|
|
private hookWarnings: string[] = [];
|
|
private hookErrors: string[] = [];
|
|
private hooksLifecycle = createSettingsDynamicSectionLifecycle();
|
|
private models: ModelMetadata[] = [];
|
|
private modelsLifecycle = createSettingsDynamicSectionLifecycle();
|
|
|
|
constructor(
|
|
app: App,
|
|
owner: Plugin,
|
|
private readonly plugin: CodexPanelSettingTabHost,
|
|
) {
|
|
super(app, owner);
|
|
this.models = plugin.cachedModels() ?? [];
|
|
}
|
|
|
|
display(): void {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
containerEl.addClass("codex-panel-settings");
|
|
|
|
this.renderHeaderActions(containerEl, SETTINGS_INTRO_TEXT);
|
|
this.renderPanelPreferenceSections(containerEl);
|
|
this.renderCodexDynamicSections(containerEl);
|
|
|
|
this.maybeAutoLoadSettingsData();
|
|
}
|
|
|
|
private renderPanelPreferenceSections(containerEl: HTMLElement): void {
|
|
const configSection = containerEl.createDiv({ cls: "codex-panel-settings__section codex-panel-settings__general-section" });
|
|
|
|
new Setting(configSection)
|
|
.setName("Codex executable")
|
|
.setDesc("Path used to start `codex app-server`. Use an absolute path if Obsidian cannot find `codex`.")
|
|
.addText((text) => {
|
|
text.inputEl.ariaLabel = "Codex executable";
|
|
text
|
|
.setPlaceholder(DEFAULT_CODEX_PATH)
|
|
.setValue(this.plugin.settings.codexPath)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.codexPath = value.trim() || DEFAULT_CODEX_PATH;
|
|
await this.plugin.saveSettings();
|
|
});
|
|
});
|
|
new Setting(configSection)
|
|
.setName("Show chat toolbar")
|
|
.setDesc("Show the chat panel toolbar. Slash commands, composer status controls, and the threads view remain available when hidden.")
|
|
.addToggle((toggle) => {
|
|
toggle.setValue(this.plugin.settings.showToolbar).onChange(async (value) => {
|
|
this.plugin.settings.showToolbar = value;
|
|
await this.plugin.saveSettings();
|
|
this.plugin.refreshOpenViews();
|
|
});
|
|
});
|
|
|
|
const composerSection = containerEl.createDiv({ cls: "codex-panel-settings__section codex-panel-settings__composer-section" });
|
|
renderSettingsHeading(composerSection, "Composer");
|
|
new Setting(composerSection)
|
|
.setName("Send shortcut")
|
|
.setDesc(
|
|
"Choose how the composer sends messages. Shift+Enter inserts a newline when Enter sends. Obsidian hotkeys may intercept Cmd/Ctrl+Enter.",
|
|
)
|
|
.addDropdown((dropdown) => {
|
|
dropdown.selectEl.ariaLabel = "Send shortcut";
|
|
dropdown.addOption("enter", SEND_SHORTCUT_LABELS.enter);
|
|
dropdown.addOption("mod-enter", SEND_SHORTCUT_LABELS["mod-enter"]);
|
|
dropdown.setValue(this.plugin.settings.sendShortcut).onChange(async (value) => {
|
|
this.plugin.settings.sendShortcut = value === "mod-enter" ? "mod-enter" : "enter";
|
|
await this.plugin.saveSettings();
|
|
this.display();
|
|
});
|
|
});
|
|
new Setting(composerSection)
|
|
.setName("Scroll thread from composer edges")
|
|
.setDesc("When enabled, Up/Ctrl+P on the first composer line and Down/Ctrl+N on the last line scroll the thread.")
|
|
.addToggle((toggle) => {
|
|
toggle.setValue(this.plugin.settings.scrollThreadFromComposerEdges).onChange(async (value) => {
|
|
this.plugin.settings.scrollThreadFromComposerEdges = value;
|
|
await this.plugin.saveSettings();
|
|
});
|
|
});
|
|
|
|
const helperSection = containerEl.createDiv({ cls: "codex-panel-settings__section codex-panel-settings__helper-section" });
|
|
renderSettingsHeading(helperSection, "Codex helpers");
|
|
new Setting(helperSection)
|
|
.setName("Automatic thread naming")
|
|
.setDesc("Choose the model and reasoning effort used to suggest thread names.")
|
|
.addDropdown((dropdown) => {
|
|
const current = this.plugin.settings.threadNamingModel;
|
|
const options = this.modelMetadata();
|
|
dropdown.selectEl.ariaLabel = "Automatic thread naming model";
|
|
dropdown.addOption(CODEX_DEFAULT_VALUE, "Codex default");
|
|
if (current && !options.some((model) => model.model === current || model.id === current)) {
|
|
dropdown.addOption(current, `${current} (saved)`);
|
|
}
|
|
for (const model of options) {
|
|
dropdown.addOption(model.model, model.model);
|
|
}
|
|
dropdown.setValue(current ?? CODEX_DEFAULT_VALUE).onChange(async (value) => {
|
|
this.plugin.settings.threadNamingModel = value === CODEX_DEFAULT_VALUE ? null : value;
|
|
if (!this.namingEffortSupported(this.plugin.settings.threadNamingEffort)) {
|
|
this.plugin.settings.threadNamingEffort = null;
|
|
}
|
|
await this.plugin.saveSettings();
|
|
this.display();
|
|
});
|
|
})
|
|
.addDropdown((dropdown) => {
|
|
const current = this.plugin.settings.threadNamingEffort;
|
|
const options = this.effortOptions(this.plugin.settings.threadNamingModel);
|
|
dropdown.selectEl.ariaLabel = "Automatic thread naming effort";
|
|
dropdown.addOption(CODEX_DEFAULT_VALUE, "Codex default");
|
|
if (current && !options.includes(current)) {
|
|
dropdown.addOption(current, `${current} (saved)`);
|
|
}
|
|
for (const effort of options) {
|
|
dropdown.addOption(effort, effort);
|
|
}
|
|
dropdown.setValue(current ?? CODEX_DEFAULT_VALUE).onChange(async (value) => {
|
|
this.plugin.settings.threadNamingEffort = value === CODEX_DEFAULT_VALUE ? null : value;
|
|
await this.plugin.saveSettings();
|
|
});
|
|
});
|
|
|
|
new Setting(helperSection)
|
|
.setName("Selection rewrite")
|
|
.setDesc("Choose the model and reasoning effort used by rewrite selection.")
|
|
.addDropdown((dropdown) => {
|
|
const current = this.plugin.settings.rewriteSelectionModel;
|
|
const options = this.modelMetadata();
|
|
dropdown.selectEl.ariaLabel = "Selection rewrite model";
|
|
dropdown.addOption(CODEX_DEFAULT_VALUE, "Codex default");
|
|
if (current && !options.some((model) => model.model === current || model.id === current)) {
|
|
dropdown.addOption(current, `${current} (saved)`);
|
|
}
|
|
for (const model of options) {
|
|
dropdown.addOption(model.model, model.model);
|
|
}
|
|
dropdown.setValue(current ?? CODEX_DEFAULT_VALUE).onChange(async (value) => {
|
|
this.plugin.settings.rewriteSelectionModel = value === CODEX_DEFAULT_VALUE ? null : value;
|
|
if (!this.rewriteSelectionEffortSupported(this.plugin.settings.rewriteSelectionEffort)) {
|
|
this.plugin.settings.rewriteSelectionEffort = null;
|
|
}
|
|
await this.plugin.saveSettings();
|
|
this.display();
|
|
});
|
|
})
|
|
.addDropdown((dropdown) => {
|
|
const current = this.plugin.settings.rewriteSelectionEffort;
|
|
const options = this.effortOptions(this.plugin.settings.rewriteSelectionModel);
|
|
dropdown.selectEl.ariaLabel = "Selection rewrite effort";
|
|
dropdown.addOption(CODEX_DEFAULT_VALUE, "Codex default");
|
|
if (current && !options.includes(current)) {
|
|
dropdown.addOption(current, `${current} (saved)`);
|
|
}
|
|
for (const effort of options) {
|
|
dropdown.addOption(effort, effort);
|
|
}
|
|
dropdown.setValue(current ?? CODEX_DEFAULT_VALUE).onChange(async (value) => {
|
|
this.plugin.settings.rewriteSelectionEffort = value === CODEX_DEFAULT_VALUE ? null : value;
|
|
await this.plugin.saveSettings();
|
|
});
|
|
});
|
|
if (this.modelsLifecycle.kind === "failed") {
|
|
configSection.createEl("p", {
|
|
cls: "setting-item-description codex-panel-settings__section-status",
|
|
text: this.modelsLifecycle.status,
|
|
});
|
|
}
|
|
}
|
|
|
|
private renderCodexDynamicSections(containerEl: HTMLElement): void {
|
|
renderArchivedThreadSection(containerEl, {
|
|
exportEnabled: this.plugin.settings.archiveExportEnabled,
|
|
exportFolderTemplate: this.plugin.settings.archiveExportFolderTemplate,
|
|
exportFilenameTemplate: this.plugin.settings.archiveExportFilenameTemplate,
|
|
exportTags: this.plugin.settings.archiveExportTags,
|
|
threads: this.archivedThreads,
|
|
loaded: this.archivedThreadsLifecycle.kind === "loaded",
|
|
loading: this.archivedThreadsLifecycle.kind === "loading",
|
|
status: this.archivedThreadsLifecycle.status,
|
|
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) => void this.restoreArchivedThread(threadId),
|
|
});
|
|
|
|
renderHookSection(containerEl, {
|
|
hooks: this.hooks,
|
|
warnings: this.hookWarnings,
|
|
errors: this.hookErrors,
|
|
loaded: this.hooksLifecycle.kind === "loaded",
|
|
loading: this.hooksLifecycle.kind === "loading",
|
|
status: this.hooksLifecycle.status,
|
|
onTrust: (hook) => void this.trustHook(hook),
|
|
onToggleEnabled: (hook, enabled) => void this.setHookEnabled(hook, enabled),
|
|
});
|
|
}
|
|
|
|
private renderHeaderActions(containerEl: HTMLElement, introText: string): void {
|
|
const header = containerEl.createDiv({ cls: "codex-panel-settings__header" });
|
|
header.createEl("span", {
|
|
cls: "setting-item-description codex-panel-settings__section-intro",
|
|
text: introText,
|
|
});
|
|
const button = header.createEl("button", {
|
|
cls: "clickable-icon codex-panel-settings__refresh-button",
|
|
});
|
|
button.type = "button";
|
|
button.disabled = this.settingsDataLoading();
|
|
button.ariaLabel = this.settingsDataLoading() ? "Refreshing Codex data" : "Refresh Codex data";
|
|
setIcon(button, "refresh-cw");
|
|
button.addEventListener("click", () => {
|
|
void this.refreshSettingsData();
|
|
});
|
|
}
|
|
|
|
private maybeAutoLoadSettingsData(): void {
|
|
if (this.settingsDataAutoLoadStarted || this.settingsDataLoading()) return;
|
|
this.settingsDataAutoLoadStarted = true;
|
|
void this.refreshSettingsData();
|
|
}
|
|
|
|
private async refreshSettingsData(): Promise<void> {
|
|
const operationId = this.nextSettingsDynamicOperationId();
|
|
this.settingsDataRefreshLifecycle = transitionSettingsDataRefreshLifecycle(this.settingsDataRefreshLifecycle, {
|
|
type: "started",
|
|
operationId,
|
|
});
|
|
this.modelsLifecycle = transitionSettingsDynamicSectionLifecycle(this.modelsLifecycle, {
|
|
type: "started",
|
|
status: "Loading models...",
|
|
operationId,
|
|
});
|
|
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
|
type: "started",
|
|
status: "Loading archived threads...",
|
|
operationId,
|
|
});
|
|
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
|
type: "started",
|
|
status: "Loading hooks...",
|
|
operationId,
|
|
});
|
|
this.display();
|
|
|
|
let failedCount = 0;
|
|
try {
|
|
const result = await this.withSettingsConnection((client) => loadSettingsData(client, this.plugin.vaultPath));
|
|
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
|
|
|
if (result.models.ok) {
|
|
this.models = result.models.data;
|
|
this.plugin.publishModels(result.models.data);
|
|
this.modelsLifecycle = transitionSettingsDynamicSectionLifecycle(this.modelsLifecycle, {
|
|
type: "loaded",
|
|
status: result.models.status,
|
|
operationId,
|
|
});
|
|
} else {
|
|
failedCount += 1;
|
|
this.modelsLifecycle = transitionSettingsDynamicSectionLifecycle(this.modelsLifecycle, {
|
|
type: "failed",
|
|
status: result.models.status,
|
|
operationId,
|
|
});
|
|
}
|
|
|
|
if (result.hooks.ok) {
|
|
this.hooks = result.hooks.data.hooks;
|
|
this.hookWarnings = result.hooks.data.warnings;
|
|
this.hookErrors = result.hooks.data.errors;
|
|
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
|
type: "loaded",
|
|
status: result.hooks.status,
|
|
operationId,
|
|
});
|
|
} else {
|
|
failedCount += 1;
|
|
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
|
type: "failed",
|
|
status: result.hooks.status,
|
|
operationId,
|
|
});
|
|
}
|
|
|
|
if (result.archivedThreads.ok) {
|
|
this.archivedThreads = result.archivedThreads.data;
|
|
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
|
type: "loaded",
|
|
status: result.archivedThreads.status,
|
|
operationId,
|
|
});
|
|
} else {
|
|
failedCount += 1;
|
|
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
|
type: "failed",
|
|
status: result.archivedThreads.status,
|
|
operationId,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
|
failedCount = 3;
|
|
const message = errorMessage(error);
|
|
this.modelsLifecycle = transitionSettingsDynamicSectionLifecycle(this.modelsLifecycle, {
|
|
type: "failed",
|
|
status: `Could not load models: ${message}`,
|
|
operationId,
|
|
});
|
|
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
|
type: "failed",
|
|
status: `Could not load hooks: ${message}`,
|
|
operationId,
|
|
});
|
|
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
|
type: "failed",
|
|
status: `Could not load archived threads: ${message}`,
|
|
operationId,
|
|
});
|
|
} finally {
|
|
const staleOperation = this.isStaleSettingsDynamicOperation(operationId);
|
|
this.settingsDataRefreshLifecycle = transitionSettingsDataRefreshLifecycle(this.settingsDataRefreshLifecycle, {
|
|
type: "completed",
|
|
failedCount,
|
|
operationId,
|
|
});
|
|
if (!staleOperation) {
|
|
if (failedCount > 0) {
|
|
new Notice("Could not refresh all Codex data.");
|
|
}
|
|
this.display();
|
|
}
|
|
}
|
|
}
|
|
|
|
private settingsDataLoading(): boolean {
|
|
return this.settingsDataRefreshLifecycle.kind === "loading";
|
|
}
|
|
|
|
private async loadHooks(): Promise<void> {
|
|
const operationId = this.nextSettingsDynamicOperationId();
|
|
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
|
type: "started",
|
|
status: "Loading hooks...",
|
|
operationId,
|
|
});
|
|
this.display();
|
|
try {
|
|
const hooks = await this.withSettingsConnection((client) => loadHookData(client, this.plugin.vaultPath));
|
|
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
|
this.hooks = hooks.hooks;
|
|
this.hookWarnings = hooks.warnings;
|
|
this.hookErrors = hooks.errors;
|
|
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
|
type: "loaded",
|
|
status: hooks.status,
|
|
operationId,
|
|
});
|
|
} catch (error) {
|
|
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
|
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
|
type: "failed",
|
|
status: `Could not load hooks: ${errorMessage(error)}`,
|
|
operationId,
|
|
});
|
|
new Notice("Could not load Codex hooks.");
|
|
} finally {
|
|
if (!this.isStaleSettingsDynamicOperation(operationId)) this.display();
|
|
}
|
|
}
|
|
|
|
private async trustHook(hook: HookItem): Promise<void> {
|
|
const operationId = this.nextSettingsDynamicOperationId();
|
|
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
|
type: "started",
|
|
status: "Loading hooks...",
|
|
operationId,
|
|
});
|
|
this.display();
|
|
try {
|
|
await this.withSettingsConnection((client) => trustHookItem(client, hook));
|
|
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
|
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
|
type: "loaded",
|
|
status: "Trusted hook definition.",
|
|
operationId,
|
|
});
|
|
await this.loadHooks();
|
|
} catch (error) {
|
|
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
|
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
|
type: "failed",
|
|
status: `Could not trust hook: ${errorMessage(error)}`,
|
|
operationId,
|
|
});
|
|
new Notice("Could not trust Codex hook.");
|
|
this.display();
|
|
}
|
|
}
|
|
|
|
private async setHookEnabled(hook: HookItem, enabled: boolean): Promise<void> {
|
|
const operationId = this.nextSettingsDynamicOperationId();
|
|
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
|
type: "started",
|
|
status: "Loading hooks...",
|
|
operationId,
|
|
});
|
|
this.display();
|
|
try {
|
|
await this.withSettingsConnection((client) => setHookItemEnabled(client, hook, enabled));
|
|
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
|
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
|
type: "loaded",
|
|
status: enabled ? "Enabled hook." : "Disabled hook.",
|
|
operationId,
|
|
});
|
|
await this.loadHooks();
|
|
} catch (error) {
|
|
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
|
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
|
type: "failed",
|
|
status: `Could not update hook: ${errorMessage(error)}`,
|
|
operationId,
|
|
});
|
|
new Notice("Could not update Codex hook.");
|
|
this.display();
|
|
}
|
|
}
|
|
|
|
private async setArchiveExportEnabled(enabled: boolean): Promise<void> {
|
|
this.plugin.settings.archiveExportEnabled = enabled;
|
|
await this.plugin.saveSettings();
|
|
this.display();
|
|
}
|
|
|
|
private async setArchiveExportFolderTemplate(value: string): Promise<void> {
|
|
this.plugin.settings.archiveExportFolderTemplate = value.trim();
|
|
await this.plugin.saveSettings();
|
|
}
|
|
|
|
private async setArchiveExportFilenameTemplate(value: string): Promise<void> {
|
|
this.plugin.settings.archiveExportFilenameTemplate = value.trim();
|
|
await this.plugin.saveSettings();
|
|
}
|
|
|
|
private async setArchiveExportTags(value: string): Promise<void> {
|
|
this.plugin.settings.archiveExportTags = value.trim();
|
|
await this.plugin.saveSettings();
|
|
}
|
|
|
|
private async restoreArchivedThread(threadId: string): Promise<void> {
|
|
const operationId = this.nextSettingsDynamicOperationId();
|
|
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
|
type: "started",
|
|
status: "Loading archived threads...",
|
|
operationId,
|
|
});
|
|
this.display();
|
|
try {
|
|
const restoredThread = await this.withSettingsConnection((client) => restoreArchivedThread(client, threadId));
|
|
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
|
this.archivedThreads = this.archivedThreads.filter((thread) => thread.id !== threadId);
|
|
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
|
type: "loaded",
|
|
status: `Restored "${archivedThreadDisplayTitle(restoredThread)}".`,
|
|
operationId,
|
|
});
|
|
this.plugin.refreshSharedThreadListFromOpenSurface();
|
|
} catch (error) {
|
|
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
|
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
|
type: "failed",
|
|
status: `Could not restore archived thread: ${errorMessage(error)}`,
|
|
operationId,
|
|
});
|
|
new Notice("Could not restore archived Codex thread.");
|
|
} finally {
|
|
if (!this.isStaleSettingsDynamicOperation(operationId)) this.display();
|
|
}
|
|
}
|
|
|
|
private async withSettingsConnection<T>(operation: (client: AppServerClient) => Promise<T>): Promise<T> {
|
|
return withShortLivedAppServerClient(this.plugin.settings.codexPath, this.plugin.vaultPath, operation, {
|
|
unhandledServerRequestMessage: "Codex Panel settings does not handle server requests.",
|
|
});
|
|
}
|
|
|
|
private nextSettingsDynamicOperationId(): number {
|
|
this.settingsDynamicOperationId += 1;
|
|
return this.settingsDynamicOperationId;
|
|
}
|
|
|
|
private isStaleSettingsDynamicOperation(operationId: number): boolean {
|
|
return operationId !== this.settingsDynamicOperationId;
|
|
}
|
|
|
|
private modelMetadata(): ModelMetadata[] {
|
|
return sortedModelMetadata(this.models);
|
|
}
|
|
|
|
private effortOptions(modelIdOrName: string | null): ReasoningEffort[] {
|
|
const model = this.selectedModel(modelIdOrName);
|
|
return model ? supportedEffortsForModelMetadata(model) : [];
|
|
}
|
|
|
|
private namingEffortSupported(effort: ReasoningEffort | null): boolean {
|
|
return !effort || this.effortOptions(this.plugin.settings.threadNamingModel).includes(effort);
|
|
}
|
|
|
|
private rewriteSelectionEffortSupported(effort: ReasoningEffort | null): boolean {
|
|
return !effort || this.effortOptions(this.plugin.settings.rewriteSelectionModel).includes(effort);
|
|
}
|
|
|
|
private selectedModel(modelIdOrName: string | null): ModelMetadata | null {
|
|
return findModelMetadataByIdOrName(this.models, modelIdOrName);
|
|
}
|
|
}
|
|
|
|
export interface CodexPanelSettingTabHost {
|
|
settings: CodexPanelSettings;
|
|
vaultPath: string;
|
|
saveSettings(): Promise<void>;
|
|
refreshOpenViews(): void;
|
|
refreshSharedThreadListFromOpenSurface(): void;
|
|
cachedModels(): ModelMetadata[] | null;
|
|
publishModels(models: ModelMetadata[]): void;
|
|
}
|