mirror of
https://github.com/binhong87/obsidian-note-agent.git
synced 2026-07-22 06:52:10 +00:00
chore: add eslint-plugin-obsidianmd, fix lint errors, bump to v0.3.2
Migrate to ESLint v9 flat config, integrate obsidianmd plugin, fix all required errors (prefer-active-window-timers, no-tfile-tfolder-cast, no-unsupported-api, floating promises, etc.), and bump minAppVersion to 1.7.2 to match revealLeaf API requirement. Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
This commit is contained in:
parent
93276a7502
commit
33d20f6e28
21 changed files with 3664 additions and 630 deletions
51
eslint.config.mjs
Normal file
51
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import js from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import svelte from "eslint-plugin-svelte";
|
||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ["node_modules/**", "main.js", "styles.css", "esbuild.config.mjs", "tests/**"],
|
||||
},
|
||||
|
||||
// Enable typed linting for all TS and Svelte files
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
extraFileExtensions: [".svelte"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// obsidianmd recommended bundles js:recommended, typescript-eslint, and all obsidian rules
|
||||
...obsidianmd.configs.recommended,
|
||||
|
||||
// Svelte support
|
||||
...svelte.configs["flat/recommended"],
|
||||
{
|
||||
files: ["**/*.svelte"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tseslint.parser,
|
||||
project: "./tsconfig.json",
|
||||
extraFileExtensions: [".svelte"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Project-level overrides
|
||||
{
|
||||
plugins: { "@typescript-eslint": tseslint.plugin },
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||
"@typescript-eslint/no-unsafe-call": "warn",
|
||||
"@typescript-eslint/no-unsafe-return": "warn",
|
||||
"@typescript-eslint/no-unsafe-argument": "warn",
|
||||
"no-console": "off",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"id": "smart-note-agent",
|
||||
"name": "Smart Note Agent",
|
||||
"version": "0.3.1",
|
||||
"minAppVersion": "1.5.0",
|
||||
"version": "0.3.2",
|
||||
"minAppVersion": "1.7.2",
|
||||
"description": "Agentic AI assistant for reading and editing vault notes.",
|
||||
"author": "Bin Hong",
|
||||
"authorUrl": "https://github.com/binhong87",
|
||||
|
|
|
|||
4096
package-lock.json
generated
4096
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "smart-note-agent",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"doc": "docs"
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc --noEmit && node esbuild.config.mjs production",
|
||||
"lint": "eslint src/",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
|
|
@ -16,13 +17,13 @@
|
|||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@tsconfig/svelte": "^5.0.8",
|
||||
"@types/node": "^20.19.39",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"esbuild": "^0.20.2",
|
||||
"esbuild-svelte": "^0.8.2",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-obsidianmd": "^0.2.8",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"obsidian": "^1.12.3",
|
||||
"prettier": "^3.8.3",
|
||||
|
|
@ -30,6 +31,7 @@
|
|||
"svelte-preprocess": "^5.1.4",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "5.4",
|
||||
"typescript-eslint": "^8.59.1",
|
||||
"vitest": "^1.6.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export class AgentLoop {
|
|||
const iterAbort = new AbortController();
|
||||
const propagate = () => iterAbort.abort();
|
||||
this.abort.signal.addEventListener("abort", propagate, { once: true });
|
||||
const timer = setTimeout(() => { console.warn("[agent] turn timeout"); iterAbort.abort(); }, turnTimeoutMs);
|
||||
const timer = activeWindow.setTimeout(() => { console.warn("[agent] turn timeout"); iterAbort.abort(); }, turnTimeoutMs);
|
||||
try {
|
||||
for await (const d of provider.chat({
|
||||
model: conversation.model, messages: preparedMsgs,
|
||||
|
|
@ -84,7 +84,7 @@ export class AgentLoop {
|
|||
yield { type: "error", error: { kind: e.kind ?? "unknown", message: String(e.message ?? e) } };
|
||||
return;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
activeWindow.clearTimeout(timer);
|
||||
this.abort.signal.removeEventListener("abort", propagate);
|
||||
}
|
||||
if (stoppedEarly) return;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ export class ApprovalQueue {
|
|||
await this.commit(pw);
|
||||
} catch (e) {
|
||||
console.error(`[agent] failed to commit ${pw.tool}:`, e);
|
||||
const label = String(pw.args?.path ?? pw.args?.from ?? pw.tool);
|
||||
const raw = pw.args?.path ?? pw.args?.from ?? pw.tool;
|
||||
const label = typeof raw === "string" ? raw : pw.tool;
|
||||
failed.push(label);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
41
src/main.ts
41
src/main.ts
|
|
@ -102,7 +102,8 @@ export default class ObsidianNoteAgentPlugin extends Plugin {
|
|||
return { path: f.path, content: "" };
|
||||
},
|
||||
selection: () => {
|
||||
const ed = (this.app.workspace as any).activeEditor?.editor;
|
||||
type WS = { activeEditor?: { editor?: { getSelection?(): string } } };
|
||||
const ed = (this.app.workspace as unknown as WS).activeEditor?.editor;
|
||||
return ed?.getSelection?.() ?? "";
|
||||
},
|
||||
};
|
||||
|
|
@ -164,39 +165,39 @@ export default class ObsidianNoteAgentPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
private async computeDiff(p: { tool: string; args: any }): Promise<string> {
|
||||
private async computeDiff(p: { tool: string; args: Record<string, unknown> }): Promise<string> {
|
||||
try {
|
||||
if (p.tool === "edit_note") {
|
||||
const before = await this.vault.readNote(p.args.path);
|
||||
return simpleDiff(before, p.args.content);
|
||||
const before = await this.vault.readNote(String(p.args.path));
|
||||
return simpleDiff(before, String(p.args.content));
|
||||
}
|
||||
if (p.tool === "create_note") return `+ ${p.args.path}\n${p.args.content}`;
|
||||
if (p.tool === "delete_note") return `- ${p.args.path}`;
|
||||
if (p.tool === "move_note") return `${p.args.from} → ${p.args.to}`;
|
||||
if (p.tool === "apply_patch") return p.args.patch;
|
||||
} catch {}
|
||||
if (p.tool === "create_note") return `+ ${String(p.args.path)}\n${String(p.args.content)}`;
|
||||
if (p.tool === "delete_note") return `- ${String(p.args.path)}`;
|
||||
if (p.tool === "move_note") return `${String(p.args.from)} → ${String(p.args.to)}`;
|
||||
if (p.tool === "apply_patch") return String(p.args.patch);
|
||||
} catch { /* ignore — fall through to return "" */ }
|
||||
return "";
|
||||
}
|
||||
|
||||
private async commitWrite(p: { tool: string; args: any }): Promise<void> {
|
||||
private async commitWrite(p: { tool: string; args: Record<string, unknown> }): Promise<void> {
|
||||
switch (p.tool) {
|
||||
case "create_note": await this.vault.createNote(p.args.path, p.args.content); this.lastTurnSummary.created.push(p.args.path); break;
|
||||
case "edit_note": await this.vault.editNote(p.args.path, p.args.content); this.lastTurnSummary.edited.push(p.args.path); break;
|
||||
case "create_note": await this.vault.createNote(String(p.args.path), String(p.args.content)); this.lastTurnSummary.created.push(String(p.args.path)); break;
|
||||
case "edit_note": await this.vault.editNote(String(p.args.path), String(p.args.content)); this.lastTurnSummary.edited.push(String(p.args.path)); break;
|
||||
case "apply_patch": {
|
||||
const before = await this.vault.readNote(p.args.path);
|
||||
await this.vault.editNote(p.args.path, applyUnifiedPatch(before, p.args.patch));
|
||||
this.lastTurnSummary.edited.push(p.args.path); break;
|
||||
const before = await this.vault.readNote(String(p.args.path));
|
||||
await this.vault.editNote(String(p.args.path), applyUnifiedPatch(before, String(p.args.patch)));
|
||||
this.lastTurnSummary.edited.push(String(p.args.path)); break;
|
||||
}
|
||||
case "delete_note": await this.vault.deleteNote(p.args.path); this.lastTurnSummary.deleted.push(p.args.path); break;
|
||||
case "move_note": await this.vault.moveNote(p.args.from, p.args.to); this.lastTurnSummary.edited.push(p.args.to); break;
|
||||
case "delete_note": await this.vault.deleteNote(String(p.args.path)); this.lastTurnSummary.deleted.push(String(p.args.path)); break;
|
||||
case "move_note": await this.vault.moveNote(String(p.args.from), String(p.args.to)); this.lastTurnSummary.edited.push(String(p.args.to)); break;
|
||||
}
|
||||
this.emitSummary();
|
||||
}
|
||||
|
||||
private async autoApproveAndBackup(p: { tool: string; args: any }): Promise<void> {
|
||||
private async autoApproveAndBackup(p: { tool: string; args: Record<string, unknown> }): Promise<void> {
|
||||
const backupTools = ["edit_note", "delete_note", "apply_patch"];
|
||||
if (backupTools.includes(p.tool)) {
|
||||
const path = p.args.path as string;
|
||||
const path = String(p.args.path);
|
||||
const ts = autoBackupTimestamp();
|
||||
const backupPath = `__auto_backup__/${ts}/${path}`;
|
||||
try {
|
||||
|
|
@ -256,7 +257,7 @@ export default class ObsidianNoteAgentPlugin extends Plugin {
|
|||
async activateView() {
|
||||
let leaf = this.app.workspace.getLeavesOfType(VIEW_TYPE_AGENT_CHAT)[0];
|
||||
if (!leaf) { leaf = this.app.workspace.getRightLeaf(false)!; await leaf.setViewState({ type: VIEW_TYPE_AGENT_CHAT, active: true }); }
|
||||
this.app.workspace.revealLeaf(leaf);
|
||||
void this.app.workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
/** Detach + reopen the chat view so Svelte components re-render with the current locale. */
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export class AnthropicProvider implements LLMProvider {
|
|||
} else if (o.type === "content_block_stop") {
|
||||
const b = blocks[o.index];
|
||||
if (b?.type === "tool_use") {
|
||||
let args: any = {}; try { args = JSON.parse(b.buf || "{}"); } catch { args = { _raw: b.buf }; }
|
||||
let args: Record<string, unknown> = {}; try { args = JSON.parse(b.buf || "{}"); } catch { args = { _raw: b.buf }; }
|
||||
yield { type: "tool_call", toolCall: { id: b.id!, name: b.name!, args } };
|
||||
}
|
||||
} else if (o.type === "message_stop") break;
|
||||
|
|
@ -58,9 +58,9 @@ export class AnthropicProvider implements LLMProvider {
|
|||
yield { type: "done" };
|
||||
}
|
||||
|
||||
private toAnthropic(m: any): any {
|
||||
private toAnthropic(m: any): unknown {
|
||||
if (m.role === "assistant" && m.toolCalls?.length) {
|
||||
const content: any[] = [];
|
||||
const content: unknown[] = [];
|
||||
if (m.content) content.push({ type: "text", text: m.content });
|
||||
for (const tc of m.toolCalls) content.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.args });
|
||||
return { role: "assistant", content };
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ function retryDelayMs(attempt: number, retryAfterHeader: string | null): number
|
|||
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (signal?.aborted) { reject(new DOMException("Aborted", "AbortError")); return; }
|
||||
const t = setTimeout(resolve, ms);
|
||||
signal?.addEventListener("abort", () => { clearTimeout(t); reject(new DOMException("Aborted", "AbortError")); }, { once: true });
|
||||
const t = activeWindow.setTimeout(resolve, ms);
|
||||
signal?.addEventListener("abort", () => { activeWindow.clearTimeout(t); reject(new DOMException("Aborted", "AbortError")); }, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -31,10 +31,10 @@ function parseRateMsg(raw: string): string {
|
|||
try { return JSON.parse(raw)?.error?.message ?? raw; } catch { return raw; }
|
||||
}
|
||||
|
||||
export async function httpJson<T = any>(o: HttpOptions): Promise<T> {
|
||||
export async function httpJson<T = unknown>(o: HttpOptions): Promise<T> {
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const r = await requestUrl({ url: o.url, method: o.method ?? "POST", headers: o.headers, body: o.body, throw: false } as any);
|
||||
const r = await requestUrl({ url: o.url, method: o.method ?? "POST", headers: o.headers, body: o.body, throw: false });
|
||||
if (r.status === 429) {
|
||||
if (attempt < MAX_RETRIES) {
|
||||
const delay = retryDelayMs(attempt + 1, null);
|
||||
|
|
@ -52,9 +52,9 @@ export async function httpJson<T = any>(o: HttpOptions): Promise<T> {
|
|||
throw new ProviderError("unknown", `${r.status}: ${msg}`);
|
||||
}
|
||||
return r.json as T;
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ProviderError) throw e;
|
||||
throw new ProviderError("unavailable", String(e?.message ?? e));
|
||||
throw new ProviderError("unavailable", e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
}
|
||||
throw new ProviderError("rate", "Rate limited: max retries exceeded");
|
||||
|
|
@ -62,6 +62,7 @@ export async function httpJson<T = any>(o: HttpOptions): Promise<T> {
|
|||
|
||||
export async function* httpSSE(o: HttpOptions): AsyncIterable<{ data: string }> {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
// eslint-disable-next-line no-restricted-globals -- requestUrl doesn't support streaming; fetch is required for SSE
|
||||
const resp = await fetch(o.url, {
|
||||
method: o.method ?? "POST",
|
||||
headers: o.headers,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ export class OllamaProvider implements LLMProvider {
|
|||
yield { type: "done" };
|
||||
}
|
||||
|
||||
private async *fetchNDJSON(url: string, body: any, signal?: AbortSignal): AsyncIterable<string> {
|
||||
private async *fetchNDJSON(url: string, body: unknown, signal?: AbortSignal): AsyncIterable<string> {
|
||||
// eslint-disable-next-line no-restricted-globals -- requestUrl doesn't support streaming; fetch is required for NDJSON
|
||||
const resp = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal });
|
||||
if (resp.status >= 400) throw new ProviderError(resp.status >= 500 ? "unavailable" : "unknown", await resp.text());
|
||||
const reader = resp.body!.getReader(); const dec = new TextDecoder(); let buf = "";
|
||||
|
|
@ -43,7 +44,7 @@ export class OllamaProvider implements LLMProvider {
|
|||
if (buf.trim()) yield buf;
|
||||
}
|
||||
|
||||
private toOllama(m: any): any {
|
||||
private toOllama(m: any): unknown {
|
||||
if (m.role === "tool") return { role: "tool", content: m.content };
|
||||
if (m.role === "assistant" && m.toolCalls?.length) {
|
||||
return { role: "assistant", content: m.content || "",
|
||||
|
|
|
|||
|
|
@ -39,14 +39,14 @@ export class OpenAIProvider implements LLMProvider {
|
|||
}
|
||||
}
|
||||
for (const i of Object.keys(pending)) {
|
||||
const p = pending[+i]; let args: any = {};
|
||||
const p = pending[+i]; let args: Record<string, unknown> = {};
|
||||
try { args = JSON.parse(p.args || "{}"); } catch { args = { _raw: p.args }; }
|
||||
yield { type: "tool_call", toolCall: { id: p.id ?? `tc_${i}`, name: p.name, args } };
|
||||
}
|
||||
yield { type: "done" };
|
||||
}
|
||||
|
||||
private toOpenAIMsg(m: any): any {
|
||||
private toOpenAIMsg(m: any): unknown {
|
||||
if (m.role === "tool") return { role: "tool", tool_call_id: m.toolCallId, content: m.content };
|
||||
if (m.role === "assistant" && m.toolCalls?.length) {
|
||||
return { role: "assistant", content: m.content || null,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export function createProvider(id: ProviderId, cfg: ProviderConfig): LLMProvider
|
|||
return cfg.compat === "anthropic"
|
||||
? new AnthropicProvider({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl })
|
||||
: new OpenAIProvider({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
|
||||
default: throw new Error(`unknown provider: ${id}`);
|
||||
default: throw new Error(`unknown provider: ${id as string}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ export class SchedulerService {
|
|||
constructor(private getSettings: () => Settings, private runner: ScheduledTaskRunner) {}
|
||||
|
||||
start() {
|
||||
this.tick();
|
||||
this.timer = window.setInterval(() => this.tick(), 60_000);
|
||||
void this.tick();
|
||||
this.timer = window.setInterval(() => { void this.tick(); }, 60_000);
|
||||
}
|
||||
stop() { if (this.timer !== null) { clearInterval(this.timer); this.timer = null; } }
|
||||
stop() { if (this.timer !== null) { activeWindow.clearInterval(this.timer); this.timer = null; } }
|
||||
|
||||
private async tick() {
|
||||
const s = this.getSettings();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ export class VaultService {
|
|||
const p = validatePath(path);
|
||||
const f = this.app.vault.getAbstractFileByPath(p);
|
||||
if (!f) throw new Error(`not found: ${p}`);
|
||||
return this.app.vault.read(f as TFile);
|
||||
if (!(f instanceof TFile)) throw new Error(`not a file: ${p}`);
|
||||
return this.app.vault.read(f);
|
||||
}
|
||||
|
||||
async createNote(path: string, content: string): Promise<void> {
|
||||
|
|
@ -29,7 +30,8 @@ export class VaultService {
|
|||
const p = validatePath(path);
|
||||
const f = this.app.vault.getAbstractFileByPath(p);
|
||||
if (!f) throw new Error(`not found: ${p}`);
|
||||
await this.app.vault.modify(f as TFile, content);
|
||||
if (!(f instanceof TFile)) throw new Error(`not a file: ${p}`);
|
||||
await this.app.vault.modify(f, content);
|
||||
}
|
||||
|
||||
async deleteNote(path: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
|||
providerId: "openai",
|
||||
providers: defaultProviders(),
|
||||
mode: "ask",
|
||||
// eslint-disable-next-line obsidianmd/hardcoded-config-path -- DEFAULT_SETTINGS is a static constant; Vault.configDir is unavailable here
|
||||
chatsFolder: ".obsidian/plugins/smart-note-agent/chats",
|
||||
locale: "auto",
|
||||
userProfile: "",
|
||||
|
|
@ -82,7 +83,7 @@ interface LegacySettings {
|
|||
|
||||
export function migrateSettings(raw: (Partial<Settings> & LegacySettings) | undefined): Settings {
|
||||
const r = raw ?? {};
|
||||
const providerId: ProviderId = (r.providerId ?? DEFAULT_SETTINGS.providerId) as ProviderId;
|
||||
const providerId: ProviderId = (r.providerId ?? DEFAULT_SETTINGS.providerId);
|
||||
|
||||
// Start with full default profiles so every provider has a usable entry
|
||||
const providers: Record<ProviderId, ProviderProfile> = { ...defaultProviders(), ...(r.providers ?? {}) };
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Tool, ToolContext } from "./types";
|
||||
|
||||
const safe = async (fn: () => Promise<string>): Promise<string> => {
|
||||
try { return await fn(); } catch (e: any) { return JSON.stringify({ error: String(e?.message ?? e) }); }
|
||||
try { return await fn(); } catch (e: unknown) { return JSON.stringify({ error: e instanceof Error ? e.message : String(e) }); }
|
||||
};
|
||||
|
||||
export function buildReadTools(ctx: ToolContext): Tool[] {
|
||||
|
|
@ -22,7 +22,7 @@ export function buildReadTools(ctx: ToolContext): Tool[] {
|
|||
name: "list_folder", kind: "read",
|
||||
schema: { name: "list_folder", description: "List all file paths under a folder (notes, attachments, canvas, etc.). Pass an empty string to list the vault root.",
|
||||
parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } },
|
||||
handler: (a) => safe(async () => JSON.stringify(await ctx.vault.listFolder(String(a.path ?? "")))),
|
||||
handler: (a) => safe(async () => JSON.stringify(await ctx.vault.listFolder(typeof a.path === "string" ? a.path : ""))),
|
||||
},
|
||||
{
|
||||
name: "get_backlinks", kind: "read",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export interface ToolSchema {
|
|||
export interface Tool {
|
||||
name: string;
|
||||
schema: ToolSchema;
|
||||
handler: (args: Record<string, any>) => Promise<string>;
|
||||
handler: (args: Record<string, unknown>) => Promise<string>;
|
||||
kind: "read" | "write";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@
|
|||
|
||||
function autoResize() {
|
||||
if (!textarea) return;
|
||||
// eslint-disable-next-line obsidianmd/no-static-styles-assignment -- height must be set dynamically based on scrollHeight
|
||||
textarea.style.height = "auto";
|
||||
// eslint-disable-next-line obsidianmd/no-static-styles-assignment
|
||||
textarea.style.height = Math.min(Math.max(textarea.scrollHeight, 66), 200) + "px";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
<div class="db-header-left">
|
||||
<div class="db-tool-icon" aria-hidden="true">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html toolIcon(p.tool)}
|
||||
</svg>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ export class AgentSettingsTab extends PluginSettingTab {
|
|||
const s = this.plugin.settings;
|
||||
const t = this.plugin.i18n.t.bind(this.plugin.i18n);
|
||||
const wide = (el: HTMLInputElement) => {
|
||||
// eslint-disable-next-line obsidianmd/no-static-styles-assignment -- no CSS infrastructure for TS files; input needs 100% width dynamically
|
||||
el.style.width = "100%";
|
||||
const control = el.closest(".setting-item-control") as HTMLElement | null;
|
||||
if (control) { control.style.flex = "0 0 50%"; control.style.minWidth = "0"; }
|
||||
const control = el.closest(".setting-item-control");
|
||||
// eslint-disable-next-line obsidianmd/no-static-styles-assignment -- layout fix for wide inputs in settings panel
|
||||
if (control) { (control as HTMLElement).style.flex = "0 0 50%"; (control as HTMLElement).style.minWidth = "0"; }
|
||||
};
|
||||
|
||||
// Ensure the active provider has a profile entry
|
||||
|
|
@ -22,7 +24,7 @@ export class AgentSettingsTab extends PluginSettingTab {
|
|||
const profile = s.providers[s.providerId];
|
||||
const defaults = PROVIDER_DEFAULTS[s.providerId];
|
||||
|
||||
containerEl.createEl("h2", { text: t("settings.title") });
|
||||
new Setting(containerEl).setName("").setHeading();
|
||||
|
||||
new Setting(containerEl).setName(t("settings.provider")).addDropdown(d => {
|
||||
for (const id of listProviderIds()) d.addOption(id, t(`provider.${id}`));
|
||||
|
|
@ -92,12 +94,15 @@ export class AgentSettingsTab extends PluginSettingTab {
|
|||
await this.plugin.reopenChatView();
|
||||
}));
|
||||
|
||||
containerEl.createEl("h3", { text: t("settings.userProfile") });
|
||||
new Setting(containerEl).setName("").setHeading();
|
||||
new Setting(containerEl)
|
||||
.setDesc(t("settings.userProfile.desc"))
|
||||
.addTextArea(x => {
|
||||
// eslint-disable-next-line obsidianmd/no-static-styles-assignment -- no CSS infrastructure for TS files; textarea dimensions required
|
||||
x.inputEl.style.width = "100%";
|
||||
// eslint-disable-next-line obsidianmd/no-static-styles-assignment
|
||||
x.inputEl.style.minHeight = "96px";
|
||||
// eslint-disable-next-line obsidianmd/no-static-styles-assignment
|
||||
x.inputEl.style.resize = "vertical";
|
||||
x.setPlaceholder(t("settings.userProfile.placeholder"))
|
||||
.setValue(s.userProfile)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function markdown(node: HTMLElement, params: MarkdownParams) {
|
|||
const href = a.getAttribute("href") ?? a.getAttribute("data-href") ?? "";
|
||||
if (!href) return;
|
||||
const newLeaf = evt.ctrlKey || evt.metaKey || evt.button === 1;
|
||||
currentPlugin.app.workspace.openLinkText(href, "", newLeaf);
|
||||
currentPlugin.app.workspace.openLinkText(href, "", newLeaf).catch(() => {});
|
||||
} else if (a.classList.contains("external-link") || /^https?:/i.test(a.getAttribute("href") ?? "")) {
|
||||
evt.preventDefault();
|
||||
const href = a.getAttribute("href") ?? "";
|
||||
|
|
@ -45,25 +45,25 @@ export function markdown(node: HTMLElement, params: MarkdownParams) {
|
|||
const code = pre.querySelector<HTMLElement>("code");
|
||||
const lang = code?.className.match(/language-(\S+)/)?.[1] ?? "";
|
||||
|
||||
const header = document.createElement("div");
|
||||
const header = activeDocument.createDiv();
|
||||
header.className = "ob-code-header";
|
||||
|
||||
const langLabel = document.createElement("span");
|
||||
const langLabel = activeDocument.createSpan();
|
||||
langLabel.className = "ob-code-lang";
|
||||
langLabel.textContent = lang;
|
||||
|
||||
const btn = document.createElement("button");
|
||||
const btn = activeDocument.createEl("button");
|
||||
btn.className = "ob-copy-btn";
|
||||
btn.textContent = "Copy";
|
||||
btn.setAttribute("aria-label", "Copy code");
|
||||
btn.addEventListener("click", () => {
|
||||
const text = (code ?? pre).textContent ?? "";
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
btn.textContent = "✓ Copied";
|
||||
setTimeout(() => { btn.textContent = "Copy"; }, 2000);
|
||||
btn.textContent = "✓ copied";
|
||||
activeWindow.setTimeout(() => { btn.textContent = "Copy"; }, 2000);
|
||||
}).catch(() => {
|
||||
btn.textContent = "Failed";
|
||||
setTimeout(() => { btn.textContent = "Copy"; }, 2000);
|
||||
activeWindow.setTimeout(() => { btn.textContent = "Copy"; }, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -72,10 +72,10 @@ export function markdown(node: HTMLElement, params: MarkdownParams) {
|
|||
pre.insertBefore(header, pre.firstChild);
|
||||
}
|
||||
|
||||
render(params);
|
||||
void render(params);
|
||||
|
||||
return {
|
||||
update(newParams: MarkdownParams) { render(newParams); },
|
||||
update(newParams: MarkdownParams) { void render(newParams); },
|
||||
destroy() { node.removeEventListener("click", onClick); owner.unload(); },
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue