From c2d3f12dd5fca41d9f545d1b7d257dbbc3f460c2 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:21:20 +0100 Subject: [PATCH] v0.2.1: Security fixes, @ mentions, system context file, UX improvements - Cap thread history sent to API at last 10 messages (privacy/payload size) - Fix @ mention parsing for filenames with spaces & special chars (e.g. "Freiheit & Technologie") - Add System Context File setting: optional vault note appended to system prompt on every request - helpText textarea in prompt button settings: 1 row when empty, auto-expands with content - Hide Draft Check hint panel after sending - registerDomEvent for input listeners (proper Obsidian API cleanup) - Public setStatus/setInputValue/setExplicitContext methods on ChatView (remove @ts-ignore) - Remove duplicate model entry from MODELS array - Remove empty authorUrl from manifest.json Co-Authored-By: Claude Sonnet 4.6 --- .claude/settings.local.json | 3 ++- main.js | 40 +++++++++++++++++++++++++++---------- manifest.json | 2 +- package.json | 2 +- src/ChatView.ts | 30 +++++++++++++++++++--------- src/SettingsTab.ts | 24 +++++++++++++++++++++- 6 files changed, 78 insertions(+), 23 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 357d29b..852aca2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,7 +5,8 @@ "Bash(/opt/homebrew/bin/gh repo create memex-chat --public --source=. --remote=origin --push)", "Bash(npm run build)", "Bash(npm install)", - "WebFetch(domain:docs.obsidian.md)" + "WebFetch(domain:docs.obsidian.md)", + "Bash(grep -n \"data\\\\.json\\\\|apiKey\\\\|saveData\\\\|loadData\" src/*.ts)" ] } } diff --git a/main.js b/main.js index 935f523..756da3c 100644 --- a/main.js +++ b/main.js @@ -211,14 +211,13 @@ var ChatView = class extends import_obsidian.ItemView { this.setStatus("\u26A0 Bitte API Key in den Einstellungen eingeben"); return; } - const mentionPattern = /@([\w\däöüÄÖÜß][^@\n]{1,}?)(?=\s|$)/g; const mentions = []; - let match; - while ((match = mentionPattern.exec(query)) !== null) { - const name = match[1].trim(); - const file = this.app.metadataCache.getFirstLinkpathDest(name, ""); - if (file) + const seenPaths = /* @__PURE__ */ new Set(); + for (const file of this.app.vault.getMarkdownFiles()) { + if (query.includes(`@${file.basename}`) && !seenPaths.has(file.path)) { mentions.push(file); + seenPaths.add(file.path); + } } if (this.activeExtensions.size === 0) { if (this.plugin.settings.autoRetrieveContext && this.plugin.settings.showContextPreview) { @@ -260,7 +259,7 @@ var ChatView = class extends import_obsidian.ItemView { this.isLoading = false; } async sendMessage(query, additionalFiles = []) { - var _a, _b; + var _a, _b, _c, _d; this.isLoading = true; this.sendBtn.disabled = true; const thread = this.activeThread; @@ -299,6 +298,7 @@ ${content}`; this.pendingContext = []; this.explicitContext = []; this.clearContextPreview(); + this.modeHintEl.style.display = "none"; this.renderMessages(); this.renderThreadList(); const assistantMsg = { @@ -309,7 +309,7 @@ ${content}`; }; thread.messages.push(assistantMsg); this.renderMessages(); - const claudeMessages = thread.messages.slice(0, -1).map((m) => ({ + const claudeMessages = thread.messages.slice(0, -1).slice(-10).map((m) => ({ role: m.role, content: m.content })); @@ -319,8 +319,15 @@ ${content}`; }); this.setStatus("Claude denkt\u2026"); let systemPrompt = this.plugin.settings.systemPrompt; + const ctxPath = this.plugin.settings.systemContextFile; + if (ctxPath) { + const ctxFile = (_b = (_a = this.app.metadataCache.getFirstLinkpathDest(ctxPath, "")) != null ? _a : this.app.vault.getAbstractFileByPath(ctxPath + ".md")) != null ? _b : this.app.vault.getAbstractFileByPath(ctxPath); + if (ctxFile instanceof import_obsidian.TFile) { + systemPrompt += "\n\n---\n" + await this.app.vault.cachedRead(ctxFile); + } + } for (const filePath of this.activeExtensions) { - const file = (_b = (_a = this.app.metadataCache.getFirstLinkpathDest(filePath, "")) != null ? _a : this.app.vault.getAbstractFileByPath(filePath + ".md")) != null ? _b : this.app.vault.getAbstractFileByPath(filePath); + const file = (_d = (_c = this.app.metadataCache.getFirstLinkpathDest(filePath, "")) != null ? _c : this.app.vault.getAbstractFileByPath(filePath + ".md")) != null ? _d : this.app.vault.getAbstractFileByPath(filePath); if (file instanceof import_obsidian.TFile) { const ext = await this.app.vault.cachedRead(file); systemPrompt += "\n\n---\n" + ext; @@ -1190,6 +1197,7 @@ Wenn du Fragen beantwortest: threadsFolder: "Calendar/Chat", sendOnEnter: false, contextProperties: ["collection", "related", "up", "tags"], + systemContextFile: "", promptButtons: [ { label: "Draft Check", @@ -1401,9 +1409,15 @@ var MemexChatSettingsTab = class extends import_obsidian3.PluginSettingTab { const helpLabel = card.createEl("label", { cls: "vc-pbtn-folder-label", text: "Hilfetext (optional, erscheint im Chat wenn Button aktiv):" }); const helpTextArea = card.createEl("textarea", { cls: "vc-pbtn-help-textarea", - attr: { rows: "3", placeholder: "z.B. DRAFT \u2014 Fr\xFChphase\u2026\nPRE-PUBLISH \u2014 Fast fertig\u2026" } + attr: { placeholder: "z.B. DRAFT \u2014 Fr\xFChphase\u2026\nPRE-PUBLISH \u2014 Fast fertig\u2026" } }); helpTextArea.value = (_a = pb.helpText) != null ? _a : ""; + const updateHelpRows = () => { + const lines = helpTextArea.value.split("\n").length; + helpTextArea.rows = helpTextArea.value.trim() ? Math.max(2, lines) : 1; + }; + updateHelpRows(); + helpTextArea.addEventListener("input", updateHelpRows); helpTextArea.addEventListener("change", async () => { pb.helpText = helpTextArea.value.trim() || void 0; await this.plugin.saveSettings(); @@ -1454,6 +1468,12 @@ var MemexChatSettingsTab = class extends import_obsidian3.PluginSettingTab { textarea.inputEl.style.fontFamily = "monospace"; textarea.inputEl.style.fontSize = "12px"; }); + new import_obsidian3.Setting(containerEl).setName("System Context (Datei)").setDesc("Optionale Vault-Notiz, deren Inhalt an den System Prompt angeh\xE4ngt wird (Pfad ohne .md)").addText( + (text) => text.setPlaceholder("z.B. Prompts/Mein System Context").setValue(this.plugin.settings.systemContextFile).onChange(async (value) => { + this.plugin.settings.systemContextFile = value.trim(); + await this.plugin.saveSettings(); + }) + ); containerEl.createEl("h3", { text: "Aktionen" }); new import_obsidian3.Setting(containerEl).setName("Index neu aufbauen").setDesc("Vault-Index f\xFCr die Suche neu aufbauen (dauert je nach Vault-Gr\xF6\xDFe einige Sekunden)").addButton( (btn) => btn.setButtonText("Index neu aufbauen").setCta().onClick(async () => { diff --git a/manifest.json b/manifest.json index 28b8aa0..c9f33af 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "memex-chat", "name": "Memex Chat", - "version": "0.2.0", + "version": "0.2.1", "minAppVersion": "1.4.0", "description": "Chat with your Obsidian vault using Claude AI — semantic context retrieval, @ mentions, thread history.", "author": "Sven", diff --git a/package.json b/package.json index f3dba93..245093b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "memex-chat", - "version": "0.2.0", + "version": "0.2.1", "description": "Obsidian plugin: Chat with your vault using Claude AI", "main": "main.js", "scripts": { diff --git a/src/ChatView.ts b/src/ChatView.ts index d105be3..b955dee 100644 --- a/src/ChatView.ts +++ b/src/ChatView.ts @@ -260,14 +260,14 @@ export class ChatView extends ItemView { return; } - // Parse @Notizname mentions from input - const mentionPattern = /@([\w\däöüÄÖÜß][^@\n]{1,}?)(?=\s|$)/g; + // Parse @Notizname mentions — match against actual vault filenames to handle spaces & special chars const mentions: TFile[] = []; - let match; - while ((match = mentionPattern.exec(query)) !== null) { - const name = match[1].trim(); - const file = this.app.metadataCache.getFirstLinkpathDest(name, ""); - if (file) mentions.push(file); + const seenPaths = new Set(); + for (const file of this.app.vault.getMarkdownFiles()) { + if (query.includes(`@${file.basename}`) && !seenPaths.has(file.path)) { + mentions.push(file); + seenPaths.add(file.path); + } } // With active prompt extensions, skip auto-retrieve and clear any leftover context @@ -362,6 +362,7 @@ export class ChatView extends ItemView { this.pendingContext = []; this.explicitContext = []; this.clearContextPreview(); + this.modeHintEl.style.display = "none"; this.renderMessages(); this.renderThreadList(); @@ -375,9 +376,10 @@ export class ChatView extends ItemView { thread.messages.push(assistantMsg); this.renderMessages(); - // Build Claude messages (history) + // Build Claude messages (history) — cap at last 10 messages to limit API payload const claudeMessages: ClaudeMessage[] = thread.messages .slice(0, -1) // exclude the empty assistant msg we just added + .slice(-10) // keep only the last 10 messages .map((m) => ({ role: m.role, content: m.content, @@ -391,8 +393,18 @@ export class ChatView extends ItemView { this.setStatus("Claude denkt…"); - // Build effective system prompt (base + any active extensions) + // Build effective system prompt (base + optional system context file + active extensions) let systemPrompt = this.plugin.settings.systemPrompt; + const ctxPath = this.plugin.settings.systemContextFile; + if (ctxPath) { + const ctxFile = + this.app.metadataCache.getFirstLinkpathDest(ctxPath, "") ?? + (this.app.vault.getAbstractFileByPath(ctxPath + ".md") as TFile | null) ?? + (this.app.vault.getAbstractFileByPath(ctxPath) as TFile | null); + if (ctxFile instanceof TFile) { + systemPrompt += "\n\n---\n" + await this.app.vault.cachedRead(ctxFile); + } + } for (const filePath of this.activeExtensions) { const file = this.app.metadataCache.getFirstLinkpathDest(filePath, "") ?? diff --git a/src/SettingsTab.ts b/src/SettingsTab.ts index 9abb369..be5f27a 100644 --- a/src/SettingsTab.ts +++ b/src/SettingsTab.ts @@ -22,6 +22,7 @@ export interface MemexChatSettings { sendOnEnter: boolean; contextProperties: string[]; promptButtons: PromptButton[]; + systemContextFile: string; // optional vault path for extended system context } export const DEFAULT_SETTINGS: MemexChatSettings = { @@ -43,6 +44,7 @@ Wenn du Fragen beantwortest: threadsFolder: "Calendar/Chat", sendOnEnter: false, contextProperties: ["collection", "related", "up", "tags"], + systemContextFile: "", promptButtons: [ { label: "Draft Check", @@ -309,9 +311,16 @@ export class MemexChatSettingsTab extends PluginSettingTab { const helpLabel = card.createEl("label", { cls: "vc-pbtn-folder-label", text: "Hilfetext (optional, erscheint im Chat wenn Button aktiv):" }); const helpTextArea = card.createEl("textarea", { cls: "vc-pbtn-help-textarea", - attr: { rows: "3", placeholder: "z.B. DRAFT — Frühphase…\nPRE-PUBLISH — Fast fertig…" }, + attr: { placeholder: "z.B. DRAFT — Frühphase…\nPRE-PUBLISH — Fast fertig…" }, }) as HTMLTextAreaElement; helpTextArea.value = pb.helpText ?? ""; + // 1 row when empty, auto-fit to content when filled + const updateHelpRows = () => { + const lines = helpTextArea.value.split("\n").length; + helpTextArea.rows = helpTextArea.value.trim() ? Math.max(2, lines) : 1; + }; + updateHelpRows(); + helpTextArea.addEventListener("input", updateHelpRows); helpTextArea.addEventListener("change", async () => { pb.helpText = helpTextArea.value.trim() || undefined; await this.plugin.saveSettings(); @@ -385,6 +394,19 @@ export class MemexChatSettingsTab extends PluginSettingTab { textarea.inputEl.style.fontSize = "12px"; }); + new Setting(containerEl) + .setName("System Context (Datei)") + .setDesc("Optionale Vault-Notiz, deren Inhalt an den System Prompt angehängt wird (Pfad ohne .md)") + .addText((text) => + text + .setPlaceholder("z.B. Prompts/Mein System Context") + .setValue(this.plugin.settings.systemContextFile) + .onChange(async (value) => { + this.plugin.settings.systemContextFile = value.trim(); + await this.plugin.saveSettings(); + }) + ); + // --- Actions --- containerEl.createEl("h3", { text: "Aktionen" });