Merge pull request #12 from Epistemic-Technology/coding-style-fixes

Fix lint issues from Obsidian plugin review bot
This commit is contained in:
Mike Thicke 2026-02-14 20:35:06 -05:00 committed by GitHub
commit d35ddbe8f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 2688 additions and 118 deletions

3
.gitignore vendored
View file

@ -137,3 +137,6 @@ dist
#macos
.DS_Store
# Manual test vault
test-vault/

38
eslint.config.mjs Normal file
View file

@ -0,0 +1,38 @@
import obsidianmd from "eslint-plugin-obsidianmd";
import tseslint from "typescript-eslint";
export default tseslint.config(
{
ignores: ["node_modules/**", "dist/**", "test/**"],
},
{
files: ["src/**/*.{ts,tsx}"],
plugins: {
obsidianmd,
},
rules: {
...obsidianmd.configs.recommended,
},
},
...tseslint.configs.recommendedTypeChecked.map((config) => ({
...config,
files: ["src/**/*.{ts,tsx}"],
})),
{
files: ["src/**/*.{ts,tsx}"],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/require-await": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/restrict-template-expressions": "error",
"no-console": ["error", { allow: ["warn", "error", "debug"] }],
},
},
);

View file

@ -36,10 +36,13 @@
"@vitest/coverage-v8": "^4.0.18",
"@vitest/ui": "^4.0.18",
"esbuild": "^0.25.5",
"eslint": "^9.39.2",
"eslint-plugin-obsidianmd": "^0.1.9",
"jsdom": "^26.1.0",
"obsidian": "^1.8.7",
"postcss": "^8.5.4",
"typescript": "^5.8.3",
"typescript-eslint": "^8.55.0",
"vite": "^7.3.1",
"vite-plugin-solid": "^2.11.10",
"vite-plugin-static-copy": "^3.2.0",

File diff suppressed because it is too large Load diff

View file

@ -38,7 +38,7 @@ export class ChatView extends TextFileView {
public contextItems: ContextItems = { notes: [], tags: [], sources: [] };
public sources: Source[] = [];
public rootElement: Element | null = null;
public dispose: any;
public dispose: (() => void) | null = null;
private updating = false;
private debounceTimeout: number | null = null;
@ -60,13 +60,13 @@ export class ChatView extends TextFileView {
return this.file?.basename || "Co-Intelligence Chat";
}
async debounceUpdateViewData() {
debounceUpdateViewData() {
if (this.debounceTimeout) {
window.clearTimeout(this.debounceTimeout);
}
this.debounceTimeout = window.setTimeout(() => {
this.debounceTimeout = null;
this.updateViewData();
void this.updateViewData();
}, 500);
}
@ -79,7 +79,7 @@ export class ChatView extends TextFileView {
}
this.updating = true;
const currentNoteContent = await this.app.vault.cachedRead(this.file);
const content = await serializeCoiNoteContent(
const content = serializeCoiNoteContent(
currentNoteContent,
this.app,
this.messages,
@ -94,7 +94,7 @@ export class ChatView extends TextFileView {
return this.data;
}
async setViewData(data: string, clear: boolean) {
setViewData(data: string, clear: boolean): void {
this.data = data;
if (!this.file) {
new Notice("Error: file is null while trying to set view data");
@ -105,7 +105,7 @@ export class ChatView extends TextFileView {
this.clear();
}
const metadata = this.app.metadataCache.getFileCache(this.file);
const { messages, contextItems, sources } = await deserializeCoiNoteContent(
const { messages, contextItems, sources } = deserializeCoiNoteContent(
data,
metadata,
this.app,
@ -128,7 +128,7 @@ export class ChatView extends TextFileView {
}
}
async render() {
render() {
this.rootElement = this.rootElement || this.containerEl.children[1];
if (!this.rootElement) {
new Notice("Error: root element is null");
@ -146,7 +146,7 @@ export class ChatView extends TextFileView {
app={this.app}
plugin={this.plugin}
file={file}
onChange={this.handleChatChange}
onChange={(props) => void this.handleChatChange(props)}
initialMessages={this.messages}
initialContext={this.contextItems}
initialSources={this.sources}
@ -183,8 +183,8 @@ export class ChatView extends TextFileView {
await renameNote(this.file, newTitle, this.app, this.plugin);
}
} catch (error) {
new Notice(`Error serializing CoiNote: ${error}`);
console.error(`Error serializing CoiNote: ${error}`);
new Notice(`Error serializing CoiNote: ${String(error)}`);
console.error(`Error serializing CoiNote: ${String(error)}`);
} finally {
this.updating = false;
}
@ -199,10 +199,10 @@ export class ChatView extends TextFileView {
this.messages = messages;
this.contextItems = contextItems;
this.sources = sources;
await this.render();
this.render();
}
async onUnloadFile(file: TFile): Promise<void> {
onUnloadFile(_file: TFile): void {
this.clear();
}
@ -220,7 +220,7 @@ export class ChatView extends TextFileView {
this.messages = messages;
this.contextItems = contextItems;
this.sources = sources;
await this.render();
this.render();
}
async refresh(): Promise<void> {
@ -228,10 +228,10 @@ export class ChatView extends TextFileView {
await this.onOpen();
}
onPaneMenu(menu: Menu, source: "more-options" | "tab-header" | string): void {
onPaneMenu(menu: Menu, source: string): void {
menu.addItem((item) => {
item
.setTitle("View as markdown")
.setTitle("View as Markdown")
.setIcon("bot-message-square")
.onClick(() => {
this.app.commands.executeCommandById(

View file

@ -50,8 +50,8 @@ export class CoIntelligencePlugin extends Plugin {
(leaf: WorkspaceLeaf) => new ChatView(leaf, this, this.app),
);
this.addRibbonIcon("bot-message-square", "New COI chat", () => {
createCOINote(this.app, this);
this.addRibbonIcon("bot-message-square", "New chat", () => {
void createCOINote(this.app, this);
});
this.registerEvent(
this.app.workspace.on("file-open", this.handleFileOpen.bind(this)),
@ -66,7 +66,7 @@ export class CoIntelligencePlugin extends Plugin {
this.app.workspace.onLayoutReady(this.onloadOnLayoutReady.bind(this));
}
async onloadOnLayoutReady() {
onloadOnLayoutReady() {
this.registerMonkeyPatches();
}
@ -99,18 +99,18 @@ export class CoIntelligencePlugin extends Plugin {
}
}
private async onFileMenuHandler(
private onFileMenuHandler(
menu: Menu,
file: TFile,
source: string,
leaf: WorkspaceLeaf,
_source: string,
_leaf: WorkspaceLeaf,
) {
if (!isCoiNote(file, this.app) || isActiveCoiNote(file, this.app)) {
return;
}
menu.addItem((item) => {
item.setTitle("View as chat");
item.onClick(async () => {
item.onClick(() => {
this.app.commands.executeCommandById(
"co-intelligence:toggle-chat-view",
);
@ -141,13 +141,17 @@ export class CoIntelligencePlugin extends Plugin {
this.register(
around(WorkspaceLeaf.prototype, {
setViewState(next) {
return function (this: any, state: ViewState, eState?: any) {
return function (
this: WorkspaceLeaf,
state: ViewState,
eState?: unknown,
) {
const newState = {
...state,
};
if (state.type === "markdown") {
const path = (state.state?.file as string) ?? "";
if (isPathActiveCoiNote(path, this.app as App)) {
if (isPathActiveCoiNote(path, this.app)) {
newState.type = VIEW_TYPE_COI_CHAT;
}
}

View file

@ -1,4 +1,4 @@
import { Component, Context, createContext } from "solid-js";
import { Component, createContext } from "solid-js";
import { ModelChatMessage } from "@/types";
import { App, TFile } from "obsidian";

View file

@ -1,4 +1,4 @@
import { App, Plugin, Command, TFile, TFolder, normalizePath } from "obsidian";
import { App, Command } from "obsidian";
import CoIntelligencePlugin from "@/CoIntelligencePlugin";
import { createCOINote } from "@/utils/notes";

View file

@ -33,7 +33,7 @@ export class ToggleChatViewCommand implements Command {
return;
}
this.performToggle(currentFile);
void this.performToggle(currentFile);
return;
};

View file

@ -1,5 +1,5 @@
import { Component, onMount, createSignal } from "solid-js";
import { App, TFile, Menu, setIcon } from "obsidian";
import { Component } from "solid-js";
import { App, TFile, Menu } from "obsidian";
import { NoteLinkSuggestionModal } from "@/components/NoteLinkSuggestionModal";
import { TagSuggestionModal } from "@/components/TagSuggestionModal";
import { Tag } from "@/types";

View file

@ -3,7 +3,7 @@ import { ModelChatMessage } from "@/types";
import { ChatMessage } from "@/components/ChatMessage";
import { MarkdownView } from "@/components/MarkdownView";
import { AppContext, FileContext, PluginContext } from "@/CoiChatApp";
import { FileContext } from "@/CoiChatApp";
export interface BotMessageProps {
message: ModelChatMessage;
@ -14,7 +14,7 @@ export const BotMessage: Component<BotMessageProps> = ({
}: BotMessageProps) => {
const file = useContext(FileContext);
const filePath = file?.path || "";
const content = message.content as string;
const content = (message.content as string) || "";
// Perplexity models use <think> tags to indicate the reasoning section.
// OpenAI models use chunk types instead. These are translated into <think>
// tags by handleSendMessage in ChatInterface.tsx

View file

@ -49,7 +49,7 @@ export const ChatInterface = ({
const modelSetting = plugin.settings.defaultModel;
let currentModel: Model | null = null;
if (modelSetting) {
currentModel = registry.getModel(modelSetting as ModelId);
currentModel = registry.getModel(modelSetting);
} else {
currentModel = registry.getDefaultModel();
}
@ -164,7 +164,7 @@ export const ChatInterface = ({
try {
setIsProcessing(true);
const responseStream = await generateChatResponse(request, registry);
const responseStream = generateChatResponse(request, registry);
let accumulatedContent = "";
let isFirstChunk = true;
let chunk;
@ -173,31 +173,37 @@ export const ChatInterface = ({
try {
for await (chunk of responseStream.fullStream) {
if (chunk.type === "error") {
console.log("Error:", chunk.error);
console.error("Error:", chunk.error);
new Notice("Unknown error occurred. See console log for details.");
}
if (chunk.type !== "text-delta" && chunk.type !== "reasoning") {
if (
chunk.type !== "text-delta" &&
chunk.type !== "reasoning-delta"
) {
continue;
}
if (!chunk.text) {
continue;
}
if (isFirstChunk) {
const assistantMessage: ModelChatMessage = {
role: "assistant",
content: chunk.textDelta,
content: chunk.text,
};
setMessages([...messages(), assistantMessage]);
if (chunk.type === "reasoning") {
if (chunk.type === "reasoning-delta") {
accumulatedContent = "<think>";
doingReasoning = true;
}
accumulatedContent += chunk.textDelta;
accumulatedContent += chunk.text;
isFirstChunk = false;
setIsProcessing(false);
} else {
if (doingReasoning && chunk.type !== "reasoning") {
if (doingReasoning && chunk.type !== "reasoning-delta") {
accumulatedContent += "</think>";
doingReasoning = false;
}
accumulatedContent += chunk.textDelta;
accumulatedContent += chunk.text;
setMessages((prevMessages) => {
const updatedMessages = [...prevMessages];
updatedMessages[updatedMessages.length - 1] = {
@ -365,7 +371,7 @@ export const ChatInterface = ({
/>
<UserInput
triggerChange={triggerChange}
onSubmit={handleSendMessage}
onSubmit={(msg, ws, sp) => void handleSendMessage(msg, ws, sp)}
currentModel={model}
updateModel={setModel}
onLinkNote={handleLinkNote}

View file

@ -1,4 +1,4 @@
import { Component, useContext, Accessor } from "solid-js";
import { Component, useContext } from "solid-js";
import { ModelChatMessage } from "@/types";
import { MarkdownView } from "@/components/MarkdownView";
@ -32,7 +32,7 @@ export const ChatMessage: Component<ModelChatMessageProps> = ({
window.open(newHref);
} else if (anchor.classList.contains("internal-link")) {
const newLeaf = event.ctrlKey || event.metaKey;
app.workspace.openLinkText(href, "", newLeaf);
void app.workspace.openLinkText(href, "", newLeaf);
} else {
window.open(href, "_blank");
}

View file

@ -9,7 +9,7 @@ import {
} from "solid-js";
import { NoteLink } from "@/components/NoteLink";
import { AddContextMenu } from "@/components/AddContextMenu";
import { ContextItems, Source, Tag } from "@/types";
import { ContextItems, Tag } from "@/types";
import { contextTokenEstimate } from "@/utils/model-context";
export interface ContextListProps {
@ -82,7 +82,7 @@ export const ContextList = ({
console.warn("Item is not a TFile");
return;
}
app.workspace.openLinkText(item.basename, "");
void app.workspace.openLinkText(item.basename, "");
} else {
// Simulate clicking the tag link
window.open(

View file

@ -16,16 +16,12 @@ export const MarkdownView = ({
const app = useContext(AppContext);
const plugin = useContext(PluginContext);
if (!app) {
new Notice(
"Error: AppContext is not available while creating MarkdownView",
);
new Notice("Error: app context is not available");
console.error("AppContext is not available");
return null;
}
if (!plugin) {
new Notice(
"Error: PluginContext is not available while creating MarkdownView",
);
new Notice("Error: plugin context is not available");
console.error("PluginContext is not available");
return null;
}
@ -50,21 +46,21 @@ export const MarkdownView = ({
currentRenderChild,
);
} catch (error) {
new Notice("Error: failed to render markdown");
console.error("Failed to render markdown:", error);
containerRef.textContent = "Error rendering markdown";
new Notice("Error: failed to render Markdown");
console.error("Failed to render Markdown:", error);
containerRef.textContent = "Error rendering Markdown";
}
}
};
createEffect(() => {
if (markdown) {
renderMarkdown();
void renderMarkdown();
}
});
onMount(() => {
renderMarkdown();
void renderMarkdown();
});
onCleanup(() => {

View file

@ -14,7 +14,7 @@ export const NoteLink: Component<LinkProps> = (props) => {
const app = useContext(AppContext);
const file = useContext(FileContext);
if (!file) {
new Notice("Error: no file context for NoteLink");
new Notice("Error: no file context available");
console.error("NoteLink called with no file context");
return null;
}
@ -27,7 +27,7 @@ export const NoteLink: Component<LinkProps> = (props) => {
return;
}
const filePath = file?.path ?? "";
app?.workspace.openLinkText(local.href, filePath);
void app?.workspace.openLinkText(local.href, filePath);
};
return (

View file

@ -1,4 +1,4 @@
import { Component, createEffect } from "solid-js";
import { Component } from "solid-js";
export interface ProcessingIndicatorProps {
onCancel?: () => void;

View file

@ -8,7 +8,7 @@ import {
} from "solid-js";
import { TFile } from "obsidian";
import { ModelSelector, ModelSelectorProps } from "@/components/ModelSelector";
import { ModelSelector } from "@/components/ModelSelector";
import { SystemPromptSelector } from "@/components/SystemPromptSelector";
import { NoteLinkSuggestionModal } from "@/components/NoteLinkSuggestionModal";
import { TagSuggestionModal } from "@/components/TagSuggestionModal";

View file

@ -1,4 +1,4 @@
import { Component, Accessor } from "solid-js";
import { Component } from "solid-js";
import { ModelChatMessage } from "@/types";
import { ChatMessage } from "./ChatMessage";

View file

@ -3,7 +3,7 @@ import { createOpenAI } from "@ai-sdk/openai";
import { createAnthropic } from "@ai-sdk/anthropic";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { createPerplexity } from "@ai-sdk/perplexity";
import { Model, ModelId, Provider } from "@/types";
import { Model, ModelId } from "@/types";
import { ProviderRegistryInternal } from "@/types-extended";
import type { CoIntelligencePlugin } from "@/CoIntelligencePlugin";
import { Notice } from "obsidian";
@ -233,8 +233,8 @@ export class ModelRegistry {
}
throw new Error("OpenAI responses method not available");
} catch (error) {
new Notice("Error fetching OpenAI responses model");
console.error(`Error fetching OpenAI responses model: ${error}`);
new Notice("Error: could not fetch responses model");
console.error(`Error fetching OpenAI responses model: ${String(error)}`);
//fall through to default behavior
}
}

View file

@ -25,10 +25,10 @@ const abortControllers = new Map<string, AbortController>();
* @returns For stream=true: a Promise of StreamTextResult object with streaming capabilities
* For stream=false: a Promise of GenerateTextResult object with the complete response
*/
export async function generateChatResponse(
export function generateChatResponse(
request: ChatRequest,
registry: ModelRegistry,
): Promise<StreamTextResult<ToolSet, never>> {
): StreamTextResult<ToolSet, never> {
const model = registry.getLanguageModel(request.modelId);
const abortController = new AbortController();

View file

@ -144,11 +144,11 @@ export class CoIntelligenceSettingsTab extends PluginSettingTab {
});
new Setting(containerEl)
// eslint-disable-next-line obsidianmd/ui/sentence-case -- brand name
.setName("OpenAI API key")
.setDesc("Enter your OpenAI API key")
.addText((text) => {
text
.setPlaceholder("Enter your OpenAI API key")
.setPlaceholder("Enter API key")
.setValue(this.plugin.settings.openaiApiKey)
.onChange((value) => this.saveSetting("openaiApiKey", value));
text.inputEl.addEventListener("blur", () =>
@ -158,10 +158,9 @@ export class CoIntelligenceSettingsTab extends PluginSettingTab {
new Setting(containerEl)
.setName("Anthropic API key")
.setDesc("Enter your Anthropic API key")
.addText((text) => {
text
.setPlaceholder("Enter your Anthropic API key")
.setPlaceholder("Enter API key")
.setValue(this.plugin.settings.anthropicApiKey)
.onChange((value) => this.saveSetting("anthropicApiKey", value));
text.inputEl.addEventListener("blur", () =>
@ -171,10 +170,9 @@ export class CoIntelligenceSettingsTab extends PluginSettingTab {
new Setting(containerEl)
.setName("Google API key")
.setDesc("Enter your Google API key")
.addText((text) => {
text
.setPlaceholder("Enter your Google API key")
.setPlaceholder("Enter API key")
.setValue(this.plugin.settings.googleApiKey)
.onChange((value) => this.saveSetting("googleApiKey", value));
text.inputEl.addEventListener("blur", () =>
@ -184,10 +182,9 @@ export class CoIntelligenceSettingsTab extends PluginSettingTab {
new Setting(containerEl)
.setName("Perplexity API key")
.setDesc("Enter your Perplexity API key")
.addText((text) => {
text
.setPlaceholder("Enter your Perplexity API key")
.setPlaceholder("Enter API key")
.setValue(this.plugin.settings.perplexityApiKey)
.onChange((value) => this.saveSetting("perplexityApiKey", value));
text.inputEl.addEventListener("blur", () =>
@ -197,17 +194,17 @@ export class CoIntelligenceSettingsTab extends PluginSettingTab {
new Setting(containerEl)
.setName("Default folder")
.setDesc("Enter the default folder for CoIntelligence")
.setDesc("Enter the default folder")
.addText((text) =>
text
.setPlaceholder("Enter the default folder for CoIntelligence")
.setPlaceholder("Enter the default folder")
.setValue(this.plugin.settings.defaultFolder)
.onChange((value) => this.saveSetting("defaultFolder", value)),
);
new Setting(containerEl)
.setName("Default model")
.setDesc("Enter the default model for CoIntelligence")
.setDesc("Enter the default model")
.addDropdown((dropdown) => {
this.defaultModelSelect = dropdown.selectEl;
@ -295,8 +292,8 @@ export class CoIntelligenceSettingsTab extends PluginSettingTab {
href: "https://ko-fi.com/epistemictechnology",
cls: "coi-settings-donate-link",
});
const srOnlySpan = kofiLink.createEl("span", {
text: "Support me on Ko-fi",
kofiLink.createEl("span", {
text: "Support me on ko-fi",
cls: "coi-sr-only",
});
kofiLink.createEl("img", {

View file

@ -12,14 +12,7 @@ import { ModelChatMessage } from "@/types";
import { ModelRegistry } from "@/services/model-registry";
import { VIEW_TYPE_COI_CHAT } from "@/ChatView";
import type CoIntelligencePlugin from "@/CoIntelligencePlugin";
import {
Source,
CoiNoteFrontmatter,
ContextItems,
ContextItemContent,
ChatRequest,
Model,
} from "@/types";
import { Source, CoiNoteFrontmatter, ContextItems } from "@/types";
const CHAT_START = "<!-- CHAT-THREAD-START -->";
const CHAT_END = "<!-- CHAT-THREAD-END -->";
@ -180,16 +173,16 @@ export async function openCOINote(
});
}
export async function serializeCoiNoteContent(
export function serializeCoiNoteContent(
currentNoteContent: string,
app: App,
_app: App,
messages: ModelChatMessage[],
contextItems: ContextItems | null,
sources?: Source[],
): Promise<string> {
): string {
const serializedMessages = messages
.map(({ role, content }) => {
const contentStr = content as string;
const contentStr = (content as string) || "";
// Find the highest level header in the content
const headerMatches = contentStr.match(/^#+/gm);
@ -267,7 +260,7 @@ export async function serializeCoiNote(
return;
}
const newNoteContent = await serializeCoiNoteContent(
const newNoteContent = serializeCoiNoteContent(
currentNoteContent,
app,
messages,
@ -305,15 +298,15 @@ export async function deserializeCoiNote(note: TFile, app: App) {
);
}
export async function deserializeCoiNoteContent(
export function deserializeCoiNoteContent(
content: string,
metadata: CachedMetadata | null,
app: App,
): Promise<{
): {
messages: ModelChatMessage[];
contextItems: ContextItems;
sources: Source[];
}> {
} {
if (!pattern.test(content)) {
return {
messages: [],
@ -363,7 +356,7 @@ export async function deserializeCoiNoteContent(
flushContent();
const candidateMode = line
.replace(/^## /, "")
.replace(/\:/, "")
.replace(/:/, "")
.trim()
.toLowerCase();
if (

View file

@ -5,9 +5,9 @@ export function getDomainFromUrl(url: string): string {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch (error) {
} catch {
// If URL parsing fails, try to extract domain manually
const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/);
const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^/]+)/);
return match ? match[1] : url;
}
}