refactor: improve settings UI with icon grids and standardized helpers

- Add Exclusions heading section in settings
- Replace activeDocument calls with Obsidian helper functions (createEl, createDiv, createSpan, createFragment)
- Standardize file monitoring disclaimers with icon grid layout
- Add template warning for local models with external documentation link
- Consolidate file disclaimer rendering logic with clickable help links
- Remove redundant tooltip enum entry
- Add new CSS classes for icon grid layouts
This commit is contained in:
Andrew Beal 2026-07-04 13:27:22 +01:00
parent fc435caae2
commit 13a09cdb89
9 changed files with 91 additions and 27 deletions

View file

@ -40,6 +40,7 @@ export enum Copy {
SettingLocalUrl = "Local URL",
SettingLocalUrlDesc = "Enter the URL for the local server",
SettingApiKeyLocalDesc = "Enter your API key if authentication is required - optional.",
SettingExclusionsHeading = "Exclusions",
SettingFileExclusions = "File exclusions",
SettingContext = "Context",
SettingSearchResultsLimit = "Search results limit",
@ -64,14 +65,17 @@ export enum Copy {
SettingPlanningModelTip = "Tip: You can reduce cost by using a more powerful model for planning and a cheaper model for the regular agent.",
SettingQuickActionModelDesc = "Select the AI model to use for quick actions. A fast, lightweight model is recommended.",
SettingLocalQuickActionModelDesc = "For best results a smaller local model with a minimum of an 8K context window is recommended.",
SettingLocalModelTemplateWarning = "Note that some models may have very strict templates that require exact 'user' -> 'agent' turns. VaultKeeper AI may chain multiple tool calls and results, so you may find that some models need their Jinja templates adjusting, see: ",
SettingLocalModelTemplateWarningLinkText = "LM Studio prompt template docs (external link)",
SettingApiKeyDesc = "Enter your API key here.",
SettingFileExclusionsDesc = "Set which directories and files the AI should ignore. Enter one path per line - supports glob patterns like folder/**, *.md",
SettingSearchResultsLimitDesc = "Set the maximum number of results provided to the AI when it searches through files in your vault. Higher values provide more context but increase search time.",
SettingSnippetSizeLimitDesc = "Set the character limit of search previews provided to the AI when it searches through files in your vault. Higher values provide more context per result.",
SettingFileMonitoringGemini = "Files uploaded to Gemini are automatically deleted after 48 hours and will be re-uploaded during conversations as needed. No manual cleanup is typically required.",
SettingFileMonitoringClaude = "Files uploaded to Claude remain stored indefinitely. Periodically check the Anthropic Console (https://console.anthropic.com/) to review and remove old files that are no longer needed.",
SettingFileMonitoringOpenAI = "Files uploaded to OpenAI remain stored indefinitely. Periodically check the OpenAI Platform (https://platform.openai.com/) to review and remove old files that are no longer needed.",
SettingFileMonitoringMistral = "Documents uploaded to Mistral are stored on their platform. Images are sent inline and not stored. Periodically check the Mistral Console (https://console.mistral.ai/) to review and remove old files that are no longer needed.",
SettingFileMonitoringGemini = "Files uploaded to Gemini are automatically deleted after 48 hours and will be re-uploaded during conversations as needed. No manual cleanup is typically required. ",
SettingFileMonitoringClaude = "Files uploaded to Claude remain stored indefinitely. Periodically check the Anthropic Console to review and remove old files that are no longer needed. ",
SettingFileMonitoringOpenAI = "Files uploaded to OpenAI remain stored indefinitely. Periodically check the OpenAI Platform to review and remove old files that are no longer needed. ",
SettingFileMonitoringMistral = "Documents uploaded to Mistral are stored on their platform. Images are sent inline and not stored. Periodically check the Mistral Console to review and remove old files that are no longer needed. ",
SettingFileMonitoringLinkText = "See the plugin guide for more information.",
SettingAccessMemories = "Memories let the AI retain preferences and context across conversations. You can view and edit them at any time.",
// Settings Placeholders
@ -84,7 +88,6 @@ export enum Copy {
// Settings Tooltips
TooltipShowApiKey = "Show API Key",
TooltipHideApiKey = "Hide API Key",
TooltipLearnMoreFileMonitoring = "Learn more in Plugin Guide",
TooltipAccessMemories = "View Memories",
SettingAdvancedSettings = "Advanced Settings",

View file

@ -55,7 +55,7 @@ export function toNode(trigger: SearchTrigger, content: string): Node {
break;
}
const node = createEl("span", {
const node = createSpan({
text: text,
cls: "search-trigger",
attr: {

View file

@ -7,6 +7,10 @@ export enum Selector {
ConversationHistoryModal = "conversation-history-modal",
HelpModal = "help-modal",
ContextSettingItemDescription = "context-setting-item-description",
SettingDescIconGrid = "setting-desc-icon-grid",
TemplateWarningIcon = "template-warning-icon",
FileDisclaimerIcon = "file-disclaimer-icon",
FileDisclaimerLink = "file-disclaimer-link",
ErrorSelector = "error-selector"
}

View file

@ -86,7 +86,7 @@ export async function pdfToImages(arrayBuffer: ArrayBuffer,
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale });
const canvas = activeDocument.createElement('canvas');
const canvas = createEl('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');

View file

@ -117,7 +117,7 @@ export abstract class StringTools {
}
}
const canvas = activeDocument.createElement("canvas");
const canvas = createEl("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");

View file

@ -12,7 +12,7 @@ export class HTMLService {
public parseHTMLString(htmlString: string): DocumentFragment {
const parser = new DOMParser();
const fragment = activeDocument.createDocumentFragment();
const fragment = createFragment();
const doc = parser.parseFromString(htmlString, "text/html");
// Transfer all nodes from the parsed body to the fragment
@ -26,7 +26,7 @@ export class HTMLService {
// Creates a temporary container, parses HTML, and returns the container.
// Useful for parsing HTML when you need to traverse the resulting DOM structure.
public parseHTMLToContainer(htmlString: string): HTMLDivElement {
const container = activeDocument.createElement("div");
const container = createDiv();
const fragment = this.parseHTMLString(htmlString);
container.appendChild(fragment);
return container;

View file

@ -37,7 +37,7 @@ export class StreamingMarkdownService {
if (frozenCandidate.length > state.frozenUpTo) {
const newSlice = frozenCandidate.slice(state.frozenUpTo);
const tempDiv = activeDocument.createElement("div");
const tempDiv = createDiv();
await MarkdownRenderer.render(this.plugin.app, newSlice, tempDiv, "", state.component);
while (tempDiv.firstChild) {
state.frozenContainer.appendChild(tempDiv.firstChild);
@ -58,8 +58,8 @@ export class StreamingMarkdownService {
}
container.empty();
const frozenContainer = activeDocument.createElement("div");
const liveContainer = activeDocument.createElement("div");
const frozenContainer = createDiv();
const liveContainer = createDiv();
container.appendChild(frozenContainer);
container.appendChild(liveContainer);

View file

@ -212,6 +212,36 @@ body:not(.is-tablet) .workspace-drawer.mod-right {
color: var(--text-muted);
}
.setting-desc-icon-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--size-4-2);
align-items: start;
}
.template-warning-icon {
display: flex;
align-self: anchor-center;
margin: var(--size-4-1);
color: var(--text-warning);
}
.file-disclaimer-icon {
display: flex;
align-self: anchor-center;
margin: var(--size-4-1);
color: var(--text-muted);
}
.file-disclaimer-link {
color: var(--text-accent);
cursor: pointer;
}
.file-disclaimer-link:hover {
text-decoration: underline;
}
.top-bar-button {
margin: var(--size-4-2) 0px var(--size-4-2) 0px;
padding: var(--size-4-1) var(--size-4-2) var(--size-4-1) var(--size-4-2);

View file

@ -79,6 +79,11 @@ export class VaultkeeperAISettingTab extends PluginSettingTab {
this.providerSectionEl = containerEl.createDiv();
this.renderProviderSection();
/* Exclusions Header */
new Setting(containerEl)
.setHeading()
.setName(Copy.SettingExclusionsHeading);
/* Exclusions Setting */
new Setting(containerEl)
.setName(Copy.SettingFileExclusions)
@ -346,6 +351,20 @@ export class VaultkeeperAISettingTab extends PluginSettingTab {
});
});
});
const templateWarningDescFragment = createFragment();
const templateWarningGridEl = templateWarningDescFragment.createDiv({ cls: Selector.SettingDescIconGrid });
setIcon(templateWarningGridEl.createDiv({ cls: Selector.TemplateWarningIcon }), "circle-alert");
const templateWarningTextEl = templateWarningGridEl.createDiv();
templateWarningTextEl.appendText(Copy.SettingLocalModelTemplateWarning);
templateWarningTextEl.createEl("a", {
text: Copy.SettingLocalModelTemplateWarningLinkText,
href: "https://lmstudio.ai/docs/app/advanced/prompt-template",
cls: Selector.FileDisclaimerLink
});
new Setting(containerEl)
.setDesc(templateWarningDescFragment);
} else {
/* API Key Setting */
this.apiKeySetting = new Setting(containerEl)
@ -446,17 +465,7 @@ export class VaultkeeperAISettingTab extends PluginSettingTab {
});
/* Model files API disclaimer */
this.fileDisclaimerSetting = new Setting(containerEl)
.setDesc(Copy.SettingFileMonitoringClaude)
.addExtraButton(button => {
button
.setTooltip(Copy.TooltipLearnMoreFileMonitoring)
.onClick(() => {
const modal = Resolve<HelpModal>(Services.HelpModal);
modal.open(7); // Opens HelpModal to "Uploaded Files" (topic 7)
});
setIcon(button.extraSettingsEl, "help-circle");
});
this.fileDisclaimerSetting = new Setting(containerEl);
this.updateFileDisclaimer();
}
}
@ -578,7 +587,7 @@ export class VaultkeeperAISettingTab extends PluginSettingTab {
private updateFileDisclaimer() {
if (this.fileDisclaimerSetting) {
const provider = this.settingsService.settings.provider;
let disclaimerText;
let disclaimerText: string | null;
switch(provider) {
case AIProvider.Gemini:
@ -594,10 +603,28 @@ export class VaultkeeperAISettingTab extends PluginSettingTab {
disclaimerText = Copy.SettingFileMonitoringMistral;
break;
case AIProvider.Local:
disclaimerText = ""; // Not shown when a local model is being used
disclaimerText = null; // Not shown when a local model is being used
}
this.fileDisclaimerSetting.setDesc(disclaimerText);
if (disclaimerText === null) {
this.fileDisclaimerSetting.setDesc("");
return;
}
const disclaimerFragment = createFragment();
const disclaimerGridEl = disclaimerFragment.createDiv({ cls: Selector.SettingDescIconGrid });
setIcon(disclaimerGridEl.createDiv({ cls: Selector.FileDisclaimerIcon }), "help-circle");
const disclaimerTextEl = disclaimerGridEl.createDiv();
disclaimerTextEl.appendText(disclaimerText);
disclaimerTextEl.createEl("a", {
text: Copy.SettingFileMonitoringLinkText,
cls: Selector.FileDisclaimerLink
}).addEventListener("click", () => {
const modal = Resolve<HelpModal>(Services.HelpModal);
modal.open(7); // Opens HelpModal to "Uploaded Files" (topic 7)
});
this.fileDisclaimerSetting.setDesc(disclaimerFragment);
}
}
}