v0.2.2: Copy/save buttons, system context file, max tokens setting
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -337,6 +337,7 @@ ${content}`;
|
|||||||
const stream = this.plugin.claude.streamChat(claudeMessages, {
|
const stream = this.plugin.claude.streamChat(claudeMessages, {
|
||||||
apiKey: this.plugin.settings.apiKey,
|
apiKey: this.plugin.settings.apiKey,
|
||||||
model: this.plugin.settings.model,
|
model: this.plugin.settings.model,
|
||||||
|
maxTokens: this.plugin.settings.maxTokens,
|
||||||
systemPrompt
|
systemPrompt
|
||||||
});
|
});
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
@@ -556,6 +557,45 @@ ${content}`;
|
|||||||
link.onclick = () => this.app.workspace.openLinkText(notePath, "", "tab");
|
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 = `<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke-width="2"/></svg> Kopieren`;
|
||||||
|
copyBtn.onclick = async () => {
|
||||||
|
await navigator.clipboard.writeText(msg.content);
|
||||||
|
copyBtn.textContent = "\u2713 Kopiert";
|
||||||
|
setTimeout(() => {
|
||||||
|
copyBtn.innerHTML = `<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke-width="2"/></svg> Kopieren`;
|
||||||
|
}, 2e3);
|
||||||
|
};
|
||||||
|
const saveBtn = actions.createEl("button", { cls: "vc-msg-action-btn", title: "Als neue Notiz speichern" });
|
||||||
|
saveBtn.innerHTML = `<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" stroke-width="2"/><polyline points="17 21 17 13 7 13 7 21" stroke-width="2"/><polyline points="7 3 7 8 15 8" stroke-width="2"/></svg> 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) {
|
updateLastMessage(content) {
|
||||||
const messages = this.messagesEl.querySelectorAll(".vc-msg--assistant");
|
const messages = this.messagesEl.querySelectorAll(".vc-msg--assistant");
|
||||||
@@ -1140,7 +1180,7 @@ var ClaudeClient = class {
|
|||||||
headers: this.headers(options.apiKey),
|
headers: this.headers(options.apiKey),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: options.model,
|
model: options.model,
|
||||||
max_tokens: (_a = options.maxTokens) != null ? _a : 2048,
|
max_tokens: (_a = options.maxTokens) != null ? _a : 8192,
|
||||||
system: options.systemPrompt,
|
system: options.systemPrompt,
|
||||||
messages
|
messages
|
||||||
}),
|
}),
|
||||||
@@ -1163,7 +1203,7 @@ var ClaudeClient = class {
|
|||||||
headers: this.headers(options.apiKey),
|
headers: this.headers(options.apiKey),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: options.model,
|
model: options.model,
|
||||||
max_tokens: (_a = options.maxTokens) != null ? _a : 2048,
|
max_tokens: (_a = options.maxTokens) != null ? _a : 8192,
|
||||||
system: options.systemPrompt,
|
system: options.systemPrompt,
|
||||||
messages
|
messages
|
||||||
}),
|
}),
|
||||||
@@ -1181,6 +1221,7 @@ var import_obsidian3 = require("obsidian");
|
|||||||
var DEFAULT_SETTINGS = {
|
var DEFAULT_SETTINGS = {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
model: "claude-opus-4-5-20251101",
|
model: "claude-opus-4-5-20251101",
|
||||||
|
maxTokens: 8192,
|
||||||
maxContextNotes: 6,
|
maxContextNotes: 6,
|
||||||
maxCharsPerNote: 2500,
|
maxCharsPerNote: 2500,
|
||||||
systemPrompt: `Du bist ein hilfreicher Assistent mit Zugriff auf die pers\xF6nliche Wissensdatenbank des Nutzers (Obsidian Vault).
|
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();
|
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(
|
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) => {
|
(toggle) => toggle.setValue(this.plugin.settings.sendOnEnter).onChange(async (value) => {
|
||||||
this.plugin.settings.sendOnEnter = value;
|
this.plugin.settings.sendOnEnter = value;
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "memex-chat",
|
"id": "memex-chat",
|
||||||
"name": "Memex Chat",
|
"name": "Memex Chat",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"minAppVersion": "1.4.0",
|
"minAppVersion": "1.4.0",
|
||||||
"description": "Chat with your Obsidian vault using Claude AI — semantic context retrieval, @ mentions, thread history.",
|
"description": "Chat with your Obsidian vault using Claude AI — semantic context retrieval, @ mentions, thread history.",
|
||||||
"author": "Sven",
|
"author": "Sven",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "memex-chat",
|
"name": "memex-chat",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"description": "Obsidian plugin: Chat with your vault using Claude AI",
|
"description": "Obsidian plugin: Chat with your vault using Claude AI",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -420,6 +420,7 @@ export class ChatView extends ItemView {
|
|||||||
const stream = this.plugin.claude.streamChat(claudeMessages, {
|
const stream = this.plugin.claude.streamChat(claudeMessages, {
|
||||||
apiKey: this.plugin.settings.apiKey,
|
apiKey: this.plugin.settings.apiKey,
|
||||||
model: this.plugin.settings.model,
|
model: this.plugin.settings.model,
|
||||||
|
maxTokens: this.plugin.settings.maxTokens,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -661,6 +662,50 @@ export class ChatView extends ItemView {
|
|||||||
link.onclick = () => this.app.workspace.openLinkText(notePath, "", "tab");
|
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 = `<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke-width="2"/></svg> Kopieren`;
|
||||||
|
copyBtn.onclick = async () => {
|
||||||
|
await navigator.clipboard.writeText(msg.content);
|
||||||
|
copyBtn.textContent = "✓ Kopiert";
|
||||||
|
setTimeout(() => {
|
||||||
|
copyBtn.innerHTML = `<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke-width="2"/></svg> Kopieren`;
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save as note button
|
||||||
|
const saveBtn = actions.createEl("button", { cls: "vc-msg-action-btn", title: "Als neue Notiz speichern" });
|
||||||
|
saveBtn.innerHTML = `<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" stroke-width="2"/><polyline points="17 21 17 13 7 13 7 21" stroke-width="2"/><polyline points="7 3 7 8 15 8" stroke-width="2"/></svg> Als Notiz`;
|
||||||
|
saveBtn.onclick = async () => {
|
||||||
|
await this.saveResponseAsNote(msg.content);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveResponseAsNote(content: string): Promise<void> {
|
||||||
|
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 {
|
private updateLastMessage(content: string): void {
|
||||||
|
|||||||
+2
-2
@@ -45,7 +45,7 @@ export class ClaudeClient {
|
|||||||
headers: this.headers(options.apiKey),
|
headers: this.headers(options.apiKey),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: options.model,
|
model: options.model,
|
||||||
max_tokens: options.maxTokens ?? 2048,
|
max_tokens: options.maxTokens ?? 8192,
|
||||||
system: options.systemPrompt,
|
system: options.systemPrompt,
|
||||||
messages,
|
messages,
|
||||||
}),
|
}),
|
||||||
@@ -70,7 +70,7 @@ export class ClaudeClient {
|
|||||||
headers: this.headers(options.apiKey),
|
headers: this.headers(options.apiKey),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: options.model,
|
model: options.model,
|
||||||
max_tokens: options.maxTokens ?? 2048,
|
max_tokens: options.maxTokens ?? 8192,
|
||||||
system: options.systemPrompt,
|
system: options.systemPrompt,
|
||||||
messages,
|
messages,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface PromptButton {
|
|||||||
export interface MemexChatSettings {
|
export interface MemexChatSettings {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
maxTokens: number;
|
||||||
maxContextNotes: number;
|
maxContextNotes: number;
|
||||||
maxCharsPerNote: number;
|
maxCharsPerNote: number;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
@@ -28,6 +29,7 @@ export interface MemexChatSettings {
|
|||||||
export const DEFAULT_SETTINGS: MemexChatSettings = {
|
export const DEFAULT_SETTINGS: MemexChatSettings = {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
model: "claude-opus-4-5-20251101",
|
model: "claude-opus-4-5-20251101",
|
||||||
|
maxTokens: 8192,
|
||||||
maxContextNotes: 6,
|
maxContextNotes: 6,
|
||||||
maxCharsPerNote: 2500,
|
maxCharsPerNote: 2500,
|
||||||
systemPrompt: `Du bist ein hilfreicher Assistent mit Zugriff auf die persönliche Wissensdatenbank des Nutzers (Obsidian Vault).
|
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)
|
new Setting(containerEl)
|
||||||
.setName("Senden mit Enter")
|
.setName("Senden mit Enter")
|
||||||
.setDesc("Ein: Enter sendet. Aus: Cmd+Enter sendet (Enter = neue Zeile)")
|
.setDesc("Ein: Enter sendet. Aus: Cmd+Enter sendet (Enter = neue Zeile)")
|
||||||
|
|||||||
+32
@@ -498,6 +498,38 @@
|
|||||||
background: var(--background-modifier-hover);
|
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 */
|
/* Context Preview */
|
||||||
.vc-context-preview {
|
.vc-context-preview {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user