Address Obsidian plugin review feedback

Fix issues raised in obsidianmd/obsidian-releases#6690:
- Update copyright year to 2025-2026
- Namespace generic CSS classes (sr-only, model-selector, etc.) with coi- prefix
- Add FolderSuggest (AbstractInputSuggest) for folder setting inputs
- Fix MarkdownRenderChild memory leak by tracking and unloading instances

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mike Thicke 2026-02-06 13:02:32 -05:00
parent 638d33dc7f
commit 4da7fee481
8 changed files with 91 additions and 23 deletions

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Epistemic Technology
Copyright (c) 2025-2026 Epistemic Technology
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -0,0 +1,31 @@
import { AbstractInputSuggest, App, TFolder } from "obsidian";
export class FolderSuggest extends AbstractInputSuggest<TFolder> {
constructor(app: App, inputEl: HTMLInputElement) {
super(app, inputEl);
}
getSuggestions(query: string): TFolder[] {
const lowerQuery = query.toLowerCase();
const folders: TFolder[] = [];
for (const abstractFile of this.app.vault.getAllLoadedFiles()) {
if (
abstractFile instanceof TFolder &&
abstractFile.path.toLowerCase().contains(lowerQuery)
) {
folders.push(abstractFile);
}
}
return folders;
}
renderSuggestion(folder: TFolder, el: HTMLElement): void {
el.setText(folder.path);
}
selectSuggestion(folder: TFolder): void {
(this as any).inputEl.value = folder.path;
(this as any).inputEl.trigger("input");
this.close();
}
}

View file

@ -1,5 +1,5 @@
import { MarkdownRenderer, MarkdownRenderChild, Notice } from "obsidian";
import { createEffect, onMount, useContext } from "solid-js";
import { createEffect, onCleanup, onMount, useContext } from "solid-js";
import { AppContext, PluginContext } from "@/CoiChatApp";
interface MarkdownViewProps {
@ -12,6 +12,7 @@ export const MarkdownView = ({
sourcePath = "",
}: MarkdownViewProps) => {
let containerRef: HTMLDivElement | undefined;
let currentRenderChild: MarkdownRenderChild | undefined;
const app = useContext(AppContext);
const plugin = useContext(PluginContext);
if (!app) {
@ -31,18 +32,22 @@ export const MarkdownView = ({
const renderMarkdown = async () => {
if (containerRef && app && plugin) {
if (currentRenderChild) {
currentRenderChild.unload();
}
while (containerRef.firstChild) {
containerRef.removeChild(containerRef.firstChild);
}
try {
const renderChild = new MarkdownRenderChild(containerRef);
currentRenderChild = new MarkdownRenderChild(containerRef);
await MarkdownRenderer.render(
app,
markdown,
containerRef,
sourcePath,
renderChild,
currentRenderChild,
);
} catch (error) {
new Notice("Error: failed to render markdown");
@ -62,5 +67,11 @@ export const MarkdownView = ({
renderMarkdown();
});
onCleanup(() => {
if (currentRenderChild) {
currentRenderChild.unload();
}
});
return <div class="markdown-rendered" ref={(el) => (containerRef = el)} />;
};

View file

@ -24,16 +24,16 @@ export const ModelSelector = ({
const hasModels = registry.availableModels.length > 0;
return (
<div class="model-selector">
<div class="coi-model-selector">
{showLabel ? (
<label for={id} class="model-selector-label">
<label for={id} class="coi-model-selector-label">
<LucideIcon name="bot-message-square" aria-hidden="true" />
{label}
</label>
) : (
<>
<LucideIcon name="bot-message-square" aria-hidden="true" />
<label for={id} class="sr-only">
<label for={id} class="coi-sr-only">
{label}
</label>
</>
@ -62,7 +62,7 @@ export const ModelSelector = ({
)}
</select>
{!hasModels && (
<div id={`${id}-description`} class="sr-only">
<div id={`${id}-description`} class="coi-sr-only">
No AI models are currently available. Please configure model providers
in settings.
</div>

View file

@ -94,16 +94,16 @@ export const SystemPromptSelector: Component<SystemPromptSelectorProps> = ({
};
return (
<div class="system-prompt-selector">
<div class="coi-system-prompt-selector">
{showLabel ? (
<label for={id} class="system-prompt-selector-label">
<label for={id} class="coi-system-prompt-selector-label">
<LucideIcon name="message-circle-code" aria-hidden="true" />
{label}
</label>
) : (
<>
<LucideIcon name="message-circle-code" aria-hidden="true" />
<label for={id} class="sr-only">
<label for={id} class="coi-sr-only">
{label}
</label>
</>
@ -140,7 +140,7 @@ export const SystemPromptSelector: Component<SystemPromptSelectorProps> = ({
</For>
</select>
{prompts().length === 0 && (
<div id={`${id}-description`} class="sr-only">
<div id={`${id}-description`} class="coi-sr-only">
No system prompts are available. Configure a system prompts folder in
settings to add custom prompts.
</div>

View file

@ -1,6 +1,7 @@
import type CoIntelligencePlugin from "@/CoIntelligencePlugin";
import { App, normalizePath, PluginSettingTab, Setting } from "obsidian";
import { ModelId } from "@/types";
import { FolderSuggest } from "@/components/FolderSuggest";
import kofiLogo from "@assets/images/kofi.png";
@ -201,12 +202,13 @@ export class CoIntelligenceSettingsTab extends PluginSettingTab {
new Setting(containerEl)
.setName("Default folder")
.setDesc("Enter the default folder for CoIntelligence")
.addText((text) =>
.addText((text) => {
text
.setPlaceholder("Enter the default folder for CoIntelligence")
.setValue(this.plugin.settings.defaultFolder)
.onChange(this.createDebouncedChangeHandler("defaultFolder")),
);
.onChange(this.createDebouncedChangeHandler("defaultFolder"));
new FolderSuggest(this.app, text.inputEl);
});
new Setting(containerEl)
.setName("Default model")
@ -256,6 +258,7 @@ export class CoIntelligenceSettingsTab extends PluginSettingTab {
textArea.onChange(
this.createDebouncedChangeHandler("systemPromptFolder", false, true),
);
new FolderSuggest(this.app, textArea.inputEl);
});
new Setting(containerEl)
@ -293,7 +296,7 @@ export class CoIntelligenceSettingsTab extends PluginSettingTab {
});
const srOnlySpan = kofiLink.createEl("span", {
text: "Support me on Ko-fi",
cls: "sr-only",
cls: "coi-sr-only",
});
kofiLink.createEl("img", {
attr: {

View file

@ -434,13 +434,13 @@ li button.coi-add-context-menu-option:focus-visible {
}
/* ModelSelector accessibility styles */
.model-selector {
.coi-model-selector {
display: flex;
align-items: center;
gap: 0.5rem;
}
.model-selector-label {
.coi-model-selector-label {
display: flex;
align-items: center;
gap: 0.25rem;
@ -450,13 +450,13 @@ li button.coi-add-context-menu-option:focus-visible {
}
/* SystemPromptSelector accessibility styles */
.system-prompt-selector {
.coi-system-prompt-selector {
display: flex;
align-items: center;
gap: 0.5rem;
}
.system-prompt-selector-label {
.coi-system-prompt-selector-label {
display: flex;
align-items: center;
gap: 0.25rem;
@ -466,7 +466,7 @@ li button.coi-add-context-menu-option:focus-visible {
}
/* Screen reader only text */
.sr-only {
.coi-sr-only {
position: absolute;
width: 1px;
height: 1px;
@ -481,8 +481,8 @@ li button.coi-add-context-menu-option:focus-visible {
/* Compact view for narrow containers */
@container (max-width: 545px) {
.model-selector .coi-lucide-icon,
.system-prompt-selector .coi-lucide-icon {
.coi-model-selector .coi-lucide-icon,
.coi-system-prompt-selector .coi-lucide-icon {
display: none;
}
}

View file

@ -38,6 +38,7 @@ export class Vault {
rename = vi.fn().mockResolvedValue(undefined);
getAbstractFileByPath = vi.fn().mockReturnValue(null);
getMarkdownFiles = vi.fn().mockReturnValue([]);
getAllLoadedFiles = vi.fn().mockReturnValue([]);
createFolder = vi.fn().mockImplementation(async () => new TFolder());
}
@ -347,6 +348,28 @@ export class MarkdownRenderChild {
constructor(containerEl: HTMLElement) {
this.containerEl = containerEl;
}
load(): void {}
unload(): void {}
onload(): void {}
onunload(): void {}
}
export class AbstractInputSuggest<T> {
app: App;
inputEl: HTMLInputElement;
constructor(app: App, inputEl: HTMLInputElement) {
this.app = app;
this.inputEl = inputEl;
}
getSuggestions(_query: string): T[] {
return [];
}
renderSuggestion(_item: T, _el: HTMLElement): void {}
selectSuggestion(_item: T): void {}
close(): void {}
}