From 64fac6bde9a731fa5113a52e791cf0b113a071c9 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:32:08 +0100 Subject: [PATCH] v0.2.2: Copy/save buttons, system context file, max tokens setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Copy and Save as Note buttons on assistant messages (hover to reveal) - Save as Note uses Obsidian's configured default new-note folder - Add System Context File setting: vault note appended to every system prompt - Add configurable Max. Antwort-Tokens setting (slider 1024–16000, default 8192) fixes Monthly Check responses being cut off - helpText textarea auto-expands with content, collapses to 1 row when empty Co-Authored-By: Claude Sonnet 4.6 --- main.js | 51 +++++++++++++++++++++++++++++++++++++++++++-- manifest.json | 2 +- package.json | 2 +- src/ChatView.ts | 45 +++++++++++++++++++++++++++++++++++++++ src/ClaudeClient.ts | 4 ++-- src/SettingsTab.ts | 16 ++++++++++++++ styles.css | 32 ++++++++++++++++++++++++++++ 7 files changed, 146 insertions(+), 6 deletions(-) diff --git a/main.js b/main.js index 756da3c..839de40 100644 --- a/main.js +++ b/main.js @@ -337,6 +337,7 @@ ${content}`; const stream = this.plugin.claude.streamChat(claudeMessages, { apiKey: this.plugin.settings.apiKey, model: this.plugin.settings.model, + maxTokens: this.plugin.settings.maxTokens, systemPrompt }); for await (const chunk of stream) { @@ -556,6 +557,45 @@ ${content}`; link.onclick = () => this.app.workspace.openLinkText(notePath, "", "tab"); } } + if (!msg.isStreaming && msg.role === "assistant") { + const actions = msgEl.createDiv("vc-msg-actions"); + const copyBtn = actions.createEl("button", { cls: "vc-msg-action-btn", title: "Antwort kopieren" }); + copyBtn.innerHTML = ` Kopieren`; + copyBtn.onclick = async () => { + await navigator.clipboard.writeText(msg.content); + copyBtn.textContent = "\u2713 Kopiert"; + setTimeout(() => { + copyBtn.innerHTML = ` Kopieren`; + }, 2e3); + }; + const saveBtn = actions.createEl("button", { cls: "vc-msg-action-btn", title: "Als neue Notiz speichern" }); + saveBtn.innerHTML = ` Als Notiz`; + saveBtn.onclick = async () => { + await this.saveResponseAsNote(msg.content); + }; + } + } + async saveResponseAsNote(content) { + var _a; + const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10); + const firstLine = (_a = content.split("\n").find((l) => l.trim())) != null ? _a : "Claude Antwort"; + const title = firstLine.replace(/^#+\s*/, "").replace(/[\\/:*?"<>|]/g, " ").slice(0, 60).trim(); + const noteContent = `--- +created: ${date} +tags: [chat, claude] +--- + +${content}`; + try { + const folder = this.app.fileManager.getNewFileParent(""); + const folderPath = folder.path === "/" ? "" : folder.path + "/"; + const fileName = `${folderPath}${date} ${title}.md`; + const existing = this.app.vault.getAbstractFileByPath(fileName); + const file = existing instanceof import_obsidian.TFile ? await this.app.vault.modify(existing, noteContent).then(() => existing) : await this.app.vault.create(fileName, noteContent); + this.app.workspace.openLinkText(file.path, "", "tab"); + } catch (e) { + this.setStatus("\u26A0 Fehler beim Speichern: " + e.message); + } } updateLastMessage(content) { const messages = this.messagesEl.querySelectorAll(".vc-msg--assistant"); @@ -1140,7 +1180,7 @@ var ClaudeClient = class { headers: this.headers(options.apiKey), body: JSON.stringify({ model: options.model, - max_tokens: (_a = options.maxTokens) != null ? _a : 2048, + max_tokens: (_a = options.maxTokens) != null ? _a : 8192, system: options.systemPrompt, messages }), @@ -1163,7 +1203,7 @@ var ClaudeClient = class { headers: this.headers(options.apiKey), body: JSON.stringify({ model: options.model, - max_tokens: (_a = options.maxTokens) != null ? _a : 2048, + max_tokens: (_a = options.maxTokens) != null ? _a : 8192, system: options.systemPrompt, messages }), @@ -1181,6 +1221,7 @@ var import_obsidian3 = require("obsidian"); var DEFAULT_SETTINGS = { apiKey: "", model: "claude-opus-4-5-20251101", + maxTokens: 8192, maxContextNotes: 6, maxCharsPerNote: 2500, systemPrompt: `Du bist ein hilfreicher Assistent mit Zugriff auf die pers\xF6nliche Wissensdatenbank des Nutzers (Obsidian Vault). @@ -1241,6 +1282,12 @@ var MemexChatSettingsTab = class extends import_obsidian3.PluginSettingTab { await this.plugin.saveSettings(); }); }); + new import_obsidian3.Setting(containerEl).setName("Max. Antwort-Tokens").setDesc("Maximale L\xE4nge der Claude-Antwort. F\xFCr lange Analysen (z.B. Monthly Check) h\xF6her einstellen. (1024\u201316000)").addSlider( + (slider) => slider.setLimits(1024, 16e3, 512).setValue(this.plugin.settings.maxTokens).setDynamicTooltip().onChange(async (value) => { + this.plugin.settings.maxTokens = value; + await this.plugin.saveSettings(); + }) + ); new import_obsidian3.Setting(containerEl).setName("Senden mit Enter").setDesc("Ein: Enter sendet. Aus: Cmd+Enter sendet (Enter = neue Zeile)").addToggle( (toggle) => toggle.setValue(this.plugin.settings.sendOnEnter).onChange(async (value) => { this.plugin.settings.sendOnEnter = value; diff --git a/manifest.json b/manifest.json index c9f33af..4066a72 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "memex-chat", "name": "Memex Chat", - "version": "0.2.1", + "version": "0.2.2", "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 245093b..88ceb22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "memex-chat", - "version": "0.2.1", + "version": "0.2.2", "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 b955dee..4df19f4 100644 --- a/src/ChatView.ts +++ b/src/ChatView.ts @@ -420,6 +420,7 @@ export class ChatView extends ItemView { const stream = this.plugin.claude.streamChat(claudeMessages, { apiKey: this.plugin.settings.apiKey, model: this.plugin.settings.model, + maxTokens: this.plugin.settings.maxTokens, systemPrompt, }); @@ -661,6 +662,50 @@ export class ChatView extends ItemView { link.onclick = () => this.app.workspace.openLinkText(notePath, "", "tab"); } } + + // Action buttons for finished assistant messages + if (!msg.isStreaming && msg.role === "assistant") { + const actions = msgEl.createDiv("vc-msg-actions"); + + // Copy button + const copyBtn = actions.createEl("button", { cls: "vc-msg-action-btn", title: "Antwort kopieren" }); + copyBtn.innerHTML = ` Kopieren`; + copyBtn.onclick = async () => { + await navigator.clipboard.writeText(msg.content); + copyBtn.textContent = "✓ Kopiert"; + setTimeout(() => { + copyBtn.innerHTML = ` Kopieren`; + }, 2000); + }; + + // Save as note button + const saveBtn = actions.createEl("button", { cls: "vc-msg-action-btn", title: "Als neue Notiz speichern" }); + saveBtn.innerHTML = ` Als Notiz`; + saveBtn.onclick = async () => { + await this.saveResponseAsNote(msg.content); + }; + } + } + + private async saveResponseAsNote(content: string): Promise { + const date = new Date().toISOString().slice(0, 10); + // Use first non-empty line as title (max 60 chars, strip markdown headers) + const firstLine = content.split("\n").find((l) => l.trim()) ?? "Claude Antwort"; + const title = firstLine.replace(/^#+\s*/, "").replace(/[\\/:*?"<>|]/g, " ").slice(0, 60).trim(); + const noteContent = `---\ncreated: ${date}\ntags: [chat, claude]\n---\n\n${content}`; + try { + // Use Obsidian's configured default new-note folder + const folder = this.app.fileManager.getNewFileParent(""); + const folderPath = folder.path === "/" ? "" : folder.path + "/"; + const fileName = `${folderPath}${date} ${title}.md`; + const existing = this.app.vault.getAbstractFileByPath(fileName); + const file = existing instanceof TFile + ? await this.app.vault.modify(existing, noteContent).then(() => existing) + : await this.app.vault.create(fileName, noteContent); + this.app.workspace.openLinkText(file.path, "", "tab"); + } catch (e) { + this.setStatus("⚠ Fehler beim Speichern: " + e.message); + } } private updateLastMessage(content: string): void { diff --git a/src/ClaudeClient.ts b/src/ClaudeClient.ts index 0c857e9..b7c0587 100644 --- a/src/ClaudeClient.ts +++ b/src/ClaudeClient.ts @@ -45,7 +45,7 @@ export class ClaudeClient { headers: this.headers(options.apiKey), body: JSON.stringify({ model: options.model, - max_tokens: options.maxTokens ?? 2048, + max_tokens: options.maxTokens ?? 8192, system: options.systemPrompt, messages, }), @@ -70,7 +70,7 @@ export class ClaudeClient { headers: this.headers(options.apiKey), body: JSON.stringify({ model: options.model, - max_tokens: options.maxTokens ?? 2048, + max_tokens: options.maxTokens ?? 8192, system: options.systemPrompt, messages, }), diff --git a/src/SettingsTab.ts b/src/SettingsTab.ts index be5f27a..c09886b 100644 --- a/src/SettingsTab.ts +++ b/src/SettingsTab.ts @@ -12,6 +12,7 @@ export interface PromptButton { export interface MemexChatSettings { apiKey: string; model: string; + maxTokens: number; maxContextNotes: number; maxCharsPerNote: number; systemPrompt: string; @@ -28,6 +29,7 @@ export interface MemexChatSettings { export const DEFAULT_SETTINGS: MemexChatSettings = { apiKey: "", model: "claude-opus-4-5-20251101", + maxTokens: 8192, maxContextNotes: 6, maxCharsPerNote: 2500, systemPrompt: `Du bist ein hilfreicher Assistent mit Zugriff auf die persönliche Wissensdatenbank des Nutzers (Obsidian Vault). @@ -107,6 +109,20 @@ export class MemexChatSettingsTab extends PluginSettingTab { }); }); + new Setting(containerEl) + .setName("Max. Antwort-Tokens") + .setDesc("Maximale Länge der Claude-Antwort. Für lange Analysen (z.B. Monthly Check) höher einstellen. (1024–16000)") + .addSlider((slider) => + slider + .setLimits(1024, 16000, 512) + .setValue(this.plugin.settings.maxTokens) + .setDynamicTooltip() + .onChange(async (value) => { + this.plugin.settings.maxTokens = value; + await this.plugin.saveSettings(); + }) + ); + new Setting(containerEl) .setName("Senden mit Enter") .setDesc("Ein: Enter sendet. Aus: Cmd+Enter sendet (Enter = neue Zeile)") diff --git a/styles.css b/styles.css index 2da7537..46ae342 100644 --- a/styles.css +++ b/styles.css @@ -498,6 +498,38 @@ background: var(--background-modifier-hover); } +/* Message action buttons (Copy, Save as Note) */ +.vc-msg-actions { + display: flex; + gap: 6px; + margin-top: 6px; + opacity: 0; + transition: opacity 0.15s; +} + +.vc-msg--assistant:hover .vc-msg-actions { + opacity: 1; +} + +.vc-msg-action-btn { + display: inline-flex; + align-items: center; + gap: 4px; + background: none; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + padding: 2px 8px; + font-size: 11px; + color: var(--text-muted); + cursor: pointer; + transition: background 0.1s, color 0.1s; +} + +.vc-msg-action-btn:hover { + background: var(--background-modifier-hover); + color: var(--text-normal); +} + /* Context Preview */ .vc-context-preview { padding: 8px 12px;