From 2884853c981cca1cf87077a5084ef7ce259bdf2f Mon Sep 17 00:00:00 2001 From: Debanjum Date: Wed, 20 Aug 2025 12:44:14 -0700 Subject: [PATCH 1/8] Make plugin object accessible to chat, find similar panes in obsidian Allows ability to access, save settings in a cleaner way --- src/interface/obsidian/src/chat_view.ts | 13 ++++++------- src/interface/obsidian/src/main.ts | 8 ++++---- src/interface/obsidian/src/pane_view.ts | 7 +++++-- src/interface/obsidian/src/similar_view.ts | 8 +++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/interface/obsidian/src/chat_view.ts b/src/interface/obsidian/src/chat_view.ts index b76d2808..e6ef59eb 100644 --- a/src/interface/obsidian/src/chat_view.ts +++ b/src/interface/obsidian/src/chat_view.ts @@ -1,9 +1,9 @@ import { ItemView, MarkdownRenderer, Scope, WorkspaceLeaf, request, requestUrl, setIcon, Platform, TFile } from 'obsidian'; import * as DOMPurify from 'isomorphic-dompurify'; -import { KhojSetting } from 'src/settings'; import { KhojPaneView } from 'src/pane_view'; import { KhojView, createCopyParentText, getLinkToEntry, pasteTextAtCursor } from 'src/utils'; import { KhojSearchModal } from 'src/search_modal'; +import Khoj from 'src/main'; import { FileInteractions, EditBlock } from 'src/interact_with_files'; export interface ChatJsonResult { @@ -67,7 +67,6 @@ interface Agent { export class KhojChatView extends KhojPaneView { result: string; - setting: KhojSetting; waitingForLocation: boolean; location: Location = { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone }; keyPressTimeout: NodeJS.Timeout | null = null; @@ -101,8 +100,8 @@ export class KhojChatView extends KhojPaneView { // 2. Higher invalid edit blocks than tolerable private maxEditRetries: number = 1; // Maximum retries for edit blocks - constructor(leaf: WorkspaceLeaf, setting: KhojSetting) { - super(leaf, setting); + constructor(leaf: WorkspaceLeaf, plugin: Khoj) { + super(leaf, plugin); this.fileInteractions = new FileInteractions(this.app); this.waitingForLocation = true; @@ -1190,7 +1189,7 @@ export class KhojChatView extends KhojPaneView { chatUrl += `&conversation_id=${chatBodyEl.dataset.conversationId}`; } - console.log("Fetching chat history from:", chatUrl); + console.debug("Fetching chat history from:", chatUrl); try { let response = await fetch(chatUrl, { @@ -1199,7 +1198,7 @@ export class KhojChatView extends KhojPaneView { }); let responseJson: any = await response.json(); - console.log("Chat history response:", responseJson); + console.debug("Chat history response:", responseJson); chatBodyEl.dataset.conversationId = responseJson.conversation_id; @@ -1221,7 +1220,7 @@ export class KhojChatView extends KhojPaneView { // Update current agent from conversation history if (responseJson.response.agent?.slug) { - console.log("Found agent in conversation history:", responseJson.response.agent); + console.debug("Found agent in conversation history:", responseJson.response.agent); this.currentAgent = responseJson.response.agent.slug; // Update the agent selector if it exists const agentSelect = this.contentEl.querySelector('.khoj-header-agent-select') as HTMLSelectElement; diff --git a/src/interface/obsidian/src/main.ts b/src/interface/obsidian/src/main.ts index 57eab878..86342f64 100644 --- a/src/interface/obsidian/src/main.ts +++ b/src/interface/obsidian/src/main.ts @@ -3,8 +3,8 @@ import { KhojSetting, KhojSettingTab, DEFAULT_SETTINGS } from 'src/settings' import { KhojSearchModal } from 'src/search_modal' import { KhojChatView } from 'src/chat_view' import { KhojSimilarView } from 'src/similar_view' -import { updateContentIndex, canConnectToBackend, KhojView, jumpToPreviousView } from './utils'; -import { KhojPaneView } from './pane_view'; +import { updateContentIndex, canConnectToBackend, KhojView } from 'src/utils'; +import { KhojPaneView } from 'src/pane_view'; export default class Khoj extends Plugin { @@ -136,8 +136,8 @@ export default class Khoj extends Plugin { }); // Register views - this.registerView(KhojView.CHAT, (leaf) => new KhojChatView(leaf, this.settings)); - this.registerView(KhojView.SIMILAR, (leaf) => new KhojSimilarView(leaf, this.settings)); + this.registerView(KhojView.CHAT, (leaf) => new KhojChatView(leaf, this)); + this.registerView(KhojView.SIMILAR, (leaf) => new KhojSimilarView(leaf, this)); // Create an icon in the left ribbon. this.addRibbonIcon('message-circle', 'Khoj', (_: MouseEvent) => { diff --git a/src/interface/obsidian/src/pane_view.ts b/src/interface/obsidian/src/pane_view.ts index 095dfd62..d63a5edd 100644 --- a/src/interface/obsidian/src/pane_view.ts +++ b/src/interface/obsidian/src/pane_view.ts @@ -1,14 +1,17 @@ import { ItemView, WorkspaceLeaf } from 'obsidian'; import { KhojSetting } from 'src/settings'; import { KhojView, populateHeaderPane } from './utils'; +import Khoj from 'src/main'; export abstract class KhojPaneView extends ItemView { setting: KhojSetting; + plugin: Khoj; - constructor(leaf: WorkspaceLeaf, setting: KhojSetting) { + constructor(leaf: WorkspaceLeaf, plugin: Khoj) { super(leaf); - this.setting = setting; + this.setting = plugin.settings; + this.plugin = plugin; // Register Modal Keybindings to send user message // this.scope.register([], 'Enter', async () => { await this.chat() }); diff --git a/src/interface/obsidian/src/similar_view.ts b/src/interface/obsidian/src/similar_view.ts index 2f5306d2..bb1621d3 100644 --- a/src/interface/obsidian/src/similar_view.ts +++ b/src/interface/obsidian/src/similar_view.ts @@ -1,7 +1,7 @@ import { WorkspaceLeaf, TFile, MarkdownRenderer, Notice, setIcon } from 'obsidian'; -import { KhojSetting } from 'src/settings'; import { KhojPaneView } from 'src/pane_view'; import { KhojView, getLinkToEntry, supportedBinaryFileTypes } from 'src/utils'; +import Khoj from 'src/main'; export interface SimilarResult { entry: string; @@ -11,7 +11,6 @@ export interface SimilarResult { export class KhojSimilarView extends KhojPaneView { static iconName: string = "search"; - setting: KhojSetting; currentController: AbortController | null = null; isLoading: boolean = false; loadingEl: HTMLElement; @@ -21,9 +20,8 @@ export class KhojSimilarView extends KhojPaneView { fileWatcher: any; component: any; - constructor(leaf: WorkspaceLeaf, setting: KhojSetting) { - super(leaf, setting); - this.setting = setting; + constructor(leaf: WorkspaceLeaf, plugin: Khoj) { + super(leaf, plugin); this.component = this; } From eb2f0ec6bc6ad349b00b0f9704d3eb9c4ed8f462 Mon Sep 17 00:00:00 2001 From: Debanjum Date: Wed, 20 Aug 2025 12:46:05 -0700 Subject: [PATCH 2/8] Persist open file access mode setting across restarts in obsidian Allows a lightweight mechanism to persist this user preference. Improve hover text a bit for readability. Resolves #1209 --- src/interface/obsidian/src/chat_view.ts | 34 ++++++++++++++++++++----- src/interface/obsidian/src/settings.ts | 2 ++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/interface/obsidian/src/chat_view.ts b/src/interface/obsidian/src/chat_view.ts index e6ef59eb..af88b08c 100644 --- a/src/interface/obsidian/src/chat_view.ts +++ b/src/interface/obsidian/src/chat_view.ts @@ -104,6 +104,9 @@ export class KhojChatView extends KhojPaneView { super(leaf, plugin); this.fileInteractions = new FileInteractions(this.app); + // Initialize file access mode from persisted settings + this.fileAccessMode = this.setting.fileAccessMode ?? 'read'; + this.waitingForLocation = true; fetch("https://ipapi.co/json") @@ -271,29 +274,48 @@ export class KhojChatView extends KhojPaneView { text: "File Access", attr: { class: "khoj-input-row-button clickable-icon", - title: "Toggle file access mode (Read Only)", + title: "Toggle open file access", }, }); - setIcon(fileAccessButton, "file-search"); - fileAccessButton.addEventListener('click', () => { + // Set initial icon based on persisted setting + switch (this.fileAccessMode) { + case 'none': + setIcon(fileAccessButton, "file-x"); + fileAccessButton.title = "Toggle open file access (No Access)"; + break; + case 'write': + setIcon(fileAccessButton, "file-edit"); + fileAccessButton.title = "Toggle open file access (Read & Write)"; + break; + case 'read': + default: + setIcon(fileAccessButton, "file-search"); + fileAccessButton.title = "Toggle open file access (Read Only)"; + break; + } + fileAccessButton.addEventListener('click', async () => { // Cycle through modes: none -> read -> write -> none switch (this.fileAccessMode) { case 'none': this.fileAccessMode = 'read'; setIcon(fileAccessButton, "file-search"); - fileAccessButton.title = "Toggle file access mode (Read Only)"; + fileAccessButton.title = "Toggle open file access (Read Only)"; break; case 'read': this.fileAccessMode = 'write'; setIcon(fileAccessButton, "file-edit"); - fileAccessButton.title = "Toggle file access mode (Read & Write)"; + fileAccessButton.title = "Toggle open file access (Read & Write)"; break; case 'write': this.fileAccessMode = 'none'; setIcon(fileAccessButton, "file-x"); - fileAccessButton.title = "Toggle file access mode (No Access)"; + fileAccessButton.title = "Toggle open file access (No Access)"; break; } + + // Persist the updated mode to settings + this.setting.fileAccessMode = this.fileAccessMode; + await this.plugin.saveSettings(); }); let chatInput = inputRow.createEl("textarea", { diff --git a/src/interface/obsidian/src/settings.ts b/src/interface/obsidian/src/settings.ts index e8cf4f51..4a07ca0c 100644 --- a/src/interface/obsidian/src/settings.ts +++ b/src/interface/obsidian/src/settings.ts @@ -38,6 +38,7 @@ export interface KhojSetting { syncFolders: string[]; syncInterval: number; autoVoiceResponse: boolean; + fileAccessMode: 'none' | 'read' | 'write'; selectedChatModelId: string | null; // Mirrors server's selected_chat_model_config availableChatModels: ModelOption[]; } @@ -58,6 +59,7 @@ export const DEFAULT_SETTINGS: KhojSetting = { syncFolders: [], syncInterval: 60, autoVoiceResponse: true, + fileAccessMode: 'read', selectedChatModelId: null, // Will be populated from server availableChatModels: [], } From d8b2df4107b3bae13c311f72714a1938cb8238ad Mon Sep 17 00:00:00 2001 From: Debanjum Date: Wed, 20 Aug 2025 15:09:01 -0700 Subject: [PATCH 3/8] Only show 3 recent files as context in obsidian file read, write mode Related #1209 --- .../obsidian/src/interact_with_files.ts | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/interface/obsidian/src/interact_with_files.ts b/src/interface/obsidian/src/interact_with_files.ts index ebd2b7a7..90627774 100644 --- a/src/interface/obsidian/src/interact_with_files.ts +++ b/src/interface/obsidian/src/interact_with_files.ts @@ -1,4 +1,4 @@ -import { App, TFile } from 'obsidian'; +import { App, MarkdownView, TFile } from 'obsidian'; import { diffWords } from 'diff'; /** @@ -55,6 +55,7 @@ export class FileInteractions { private app: App; private readonly EDIT_BLOCK_START = ''; private readonly EDIT_BLOCK_END = ''; + private readonly CONTEXT_FILES_LIMIT = 3; /** * Constructor for FileInteractions @@ -65,6 +66,26 @@ export class FileInteractions { this.app = app; } + /** + * Get N open, recently viewed markdown files. + */ + private getRecentActiveMarkdownFiles(N: number): TFile[] { + const seen = new Set(); + const recentActiveFiles = this.app.workspace.getLeavesOfType('markdown') + .sort((a, b) => (b as any).activeTime - (a as any).activeTime) // Sort by leaf activeTime (note: undocumented prop) + .map(leaf => (leaf.view as MarkdownView)?.file) + // Dedupe by file path + .filter((file): file is TFile => { + if (!file || seen.has(file.path)) return false; + seen.add(file.path); + return true; + }) + .slice(0, N); + + console.log(`Using ${recentActiveFiles.length} recently viewed md files for context: ${recentActiveFiles.map(file => file.path).join(', ')}`); + return recentActiveFiles; + } + /** * Gets the content of all open files * @@ -75,9 +96,9 @@ export class FileInteractions { // Only proceed if we have read or write access if (fileAccessMode === 'none') return ''; - // Get all open markdown leaves - const leaves = this.app.workspace.getLeavesOfType('markdown'); - if (leaves.length === 0) return ''; + // Get recently viewed markdown files + const recentFiles = this.getRecentActiveMarkdownFiles(this.CONTEXT_FILES_LIMIT); + if (recentFiles.length === 0) return ''; // Instructions in write access mode let editInstructions: string = ''; @@ -274,11 +295,7 @@ For context, the user is currently working on the following files: `; - for (const leaf of leaves) { - const view = leaf.view as any; - const file = view?.file; - if (!file || file.extension !== 'md') continue; - + for (const file of recentFiles) { // Read file content let fileContent: string; try { @@ -684,10 +701,8 @@ For context, the user is currently working on the following files: // Track current content for each file as we apply edits const currentFileContents = new Map(); - // Get all open markdown files - const files = this.app.workspace.getLeavesOfType('markdown') - .map(leaf => (leaf.view as any)?.file) - .filter(file => file && file.extension === 'md'); + // Get recently viewed markdown file(s) to edit + const files = this.getRecentActiveMarkdownFiles(this.CONTEXT_FILES_LIMIT); // Track success/failure for each edit const editResults: { block: EditBlock, success: boolean, error?: string }[] = []; From c5e2373d731b4ac1509be0a034b2fb125b817b45 Mon Sep 17 00:00:00 2001 From: Debanjum Date: Wed, 20 Aug 2025 16:14:43 -0700 Subject: [PATCH 4/8] Make khoj obsidian keyboard shortcuts toggle voice chat, chat history Previously hitting voice chat keybinding would just start voice chat, not end it and just open chat history and not close it. This is unintuitive and different from the equivalent button click behaviors. Fix toggles voice chat on/off and shows/hides chat history when hit Ctrl+Alt+V, Ctrl+Alt+O keybindings in khoj obsidian chat view --- src/interface/obsidian/src/chat_view.ts | 7 +++++-- src/interface/obsidian/src/main.ts | 7 ++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/interface/obsidian/src/chat_view.ts b/src/interface/obsidian/src/chat_view.ts index af88b08c..ebfb36bd 100644 --- a/src/interface/obsidian/src/chat_view.ts +++ b/src/interface/obsidian/src/chat_view.ts @@ -72,6 +72,7 @@ export class KhojChatView extends KhojPaneView { keyPressTimeout: NodeJS.Timeout | null = null; userMessages: string[] = []; // Store user sent messages for input history cycling currentMessageIndex: number = -1; // Track current message index in userMessages array + voiceChatActive: boolean = false; // Flag to track if voice chat is active private currentUserInput: string = ""; // Stores the current user input that is being typed in chat private startingMessage: string = this.getLearningMoment(); chatMessageState: ChatMessageState; @@ -131,7 +132,7 @@ export class KhojChatView extends KhojPaneView { this.scope = new Scope(this.app.scope); this.scope.register(["Ctrl", "Alt"], 'n', (_) => this.createNewConversation(this.currentAgent)); this.scope.register(["Ctrl", "Alt"], 'o', async (_) => await this.toggleChatSessions()); - this.scope.register(["Ctrl", "Alt"], 'v', (_) => this.speechToText(new KeyboardEvent('keydown'))); + this.scope.register(["Ctrl", "Alt"], 'v', (_) => this.speechToText(this.voiceChatActive ? new KeyboardEvent('keyup') : new KeyboardEvent('keydown'))); this.scope.register(["Ctrl"], 'f', (_) => new KhojSearchModal(this.app, this.setting).open()); this.scope.register(["Ctrl"], 'r', (_) => { this.activateView(KhojView.SIMILAR); }); } @@ -340,7 +341,7 @@ export class KhojChatView extends KhojPaneView { attr: { id: "khoj-transcribe", class: "khoj-transcribe khoj-input-row-button clickable-icon ", - title: "Start Voice Chat (Ctrl+Alt+V)", + title: "Hold to Voice Chat (Ctrl+Alt+V)", }, }) transcribe.addEventListener('mousedown', (event) => { this.startSpeechToText(event) }); @@ -1741,6 +1742,7 @@ export class KhojChatView extends KhojPaneView { // Toggle recording if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive' || event.type === 'touchstart' || event.type === 'mousedown' || event.type === 'keydown') { + this.voiceChatActive = true; navigator.mediaDevices .getUserMedia({ audio: true }) ?.then(handleRecording) @@ -1748,6 +1750,7 @@ export class KhojChatView extends KhojPaneView { this.flashStatusInChatInput("⛔️ Failed to access microphone"); }); } else if (this.mediaRecorder?.state === 'recording' || event.type === 'touchend' || event.type === 'touchcancel' || event.type === 'mouseup' || event.type === 'keyup') { + this.voiceChatActive = false; this.mediaRecorder.stop(); this.mediaRecorder.stream.getTracks().forEach(track => track.stop()); this.mediaRecorder = undefined; diff --git a/src/interface/obsidian/src/main.ts b/src/interface/obsidian/src/main.ts index 86342f64..b0c8d7d0 100644 --- a/src/interface/obsidian/src/main.ts +++ b/src/interface/obsidian/src/main.ts @@ -73,7 +73,7 @@ export default class Khoj extends Plugin { this.activateView(KhojView.CHAT).then(() => { const chatView = this.app.workspace.getActiveViewOfType(KhojChatView); if (chatView) { - chatView.toggleChatSessions(true); + chatView.toggleChatSessions(); } }); } @@ -88,8 +88,9 @@ export default class Khoj extends Plugin { this.activateView(KhojView.CHAT).then(() => { const chatView = this.app.workspace.getActiveViewOfType(KhojChatView); if (chatView) { - // Trigger speech to text functionality - chatView.speechToText(new KeyboardEvent('keydown')); + // Toggle speech to text functionality + const toggleEvent = chatView.voiceChatActive ? 'keyup' : 'keydown'; + chatView.speechToText(new KeyboardEvent(toggleEvent)); } }); } From 2e6928c58273c8d37a46e2cfbc6d29c95f617343 Mon Sep 17 00:00:00 2001 From: Debanjum Date: Wed, 20 Aug 2025 17:25:09 -0700 Subject: [PATCH 5/8] Limit retries to fix invalid edit blocks in obsidian write mode --- src/interface/obsidian/src/chat_view.ts | 33 +++++++++++++++---------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/interface/obsidian/src/chat_view.ts b/src/interface/obsidian/src/chat_view.ts index ebfb36bd..1b0983ef 100644 --- a/src/interface/obsidian/src/chat_view.ts +++ b/src/interface/obsidian/src/chat_view.ts @@ -1374,18 +1374,25 @@ export class KhojChatView extends KhojPaneView { if (this.fileAccessMode === 'write') { const editBlocks = this.parseEditBlocks(this.chatMessageState.rawResponse); - // Check for errors and retry if needed - if (editBlocks.length > 0 && editBlocks[0].hasError && this.editRetryCount < this.maxEditRetries) { - await this.handleEditRetry(editBlocks[0]); - return; - } - - // Reset retry count on success - this.editRetryCount = 0; - - // Apply edits if there are any if (editBlocks.length > 0) { - await this.applyEditBlocks(editBlocks); + const firstBlock = editBlocks[0]; + if (firstBlock.hasError) { + // Only retry if we have remaining attempts; do NOT reset counter on failure + if (this.editRetryCount < this.maxEditRetries) { + await this.handleEditRetry(firstBlock); + return; // Wait for retry response + } else { + // Exhausted retries; surface error and do not attempt further automatic retries + console.warn('[Khoj] Max edit retries reached. Aborting further retries.'); + } + } else { + // Successful parse => reset counter and apply edits + this.editRetryCount = 0; + await this.applyEditBlocks(editBlocks); + } + } else { + // No edit blocks => reset counter just in case + this.editRetryCount = 0; } } @@ -2427,7 +2434,7 @@ export class KhojChatView extends KhojPaneView { // Add retry count retryBadge.createSpan({ cls: "retry-count", - text: `Attempt ${this.editRetryCount}/3` + text: `Attempt ${this.editRetryCount}/${this.maxEditRetries}` }); // Add error details as a tooltip @@ -2442,7 +2449,7 @@ export class KhojChatView extends KhojPaneView { retryBadge.scrollIntoView({ behavior: "smooth", block: "center" }); // Create a retry prompt for the LLM - const retryPrompt = `/general I noticed some issues with the edit block. Please fix the following and provide a corrected version (retry ${this.editRetryCount}/3):\n\n${errorDetails}\n\nPlease provide a new edit block that fixes these issues. Make sure to follow the exact format required.`; + const retryPrompt = `/general I noticed some issues with the edit block. Please fix the following and provide a corrected version (retry ${this.editRetryCount}/${this.maxEditRetries}):\n\n${errorDetails}\n\nPlease provide a new edit block that fixes these issues. Make sure to follow the exact format required.`; // Send retry request without displaying the user message await this.getChatResponse(retryPrompt, "", false, false); From 7645cbea3b205266a61d291fca65f0e6f9ef8fc1 Mon Sep 17 00:00:00 2001 From: Debanjum Date: Wed, 20 Aug 2025 16:18:38 -0700 Subject: [PATCH 6/8] Do not throw error when no edit blocks in write mode on obsidian Editing is an option, not a requirement in file write/edit mode. --- .../obsidian/src/interact_with_files.ts | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/interface/obsidian/src/interact_with_files.ts b/src/interface/obsidian/src/interact_with_files.ts index 90627774..98452ebe 100644 --- a/src/interface/obsidian/src/interact_with_files.ts +++ b/src/interface/obsidian/src/interact_with_files.ts @@ -447,28 +447,21 @@ For context, the user is currently working on the following files: // Validate required fields let error: { type: 'missing_field' | 'invalid_format' | 'preprocessing' | 'unknown', message: string, details?: string } | null = null; - if (!editData) { - error = { - type: 'invalid_format', - message: 'Invalid edit block format', - details: 'The edit block does not match the expected format' - }; - } - else if (!editData.file) { + if (editData && !editData.file) { error = { type: 'missing_field', message: 'Missing "file" field in edit block', details: 'The "file" field is required and should contain the target file name' }; } - else if (editData.find === undefined || editData.find === null) { + else if (editData && (editData.find === undefined || editData.find === null)) { error = { type: 'missing_field', message: 'Missing "find" field markers', details: 'The "find" field is required and should contain the content to find in the file' }; } - else if (!editData.replace) { + else if (editData && !editData.replace) { error = { type: 'missing_field', message: 'Missing "replace" field in edit block', @@ -524,7 +517,7 @@ For context, the user is currently working on the following files: } if (!editData) { - console.error("No edit data parsed"); + console.debug("No edit data parsed"); continue; } @@ -898,6 +891,10 @@ For context, the user is currently working on the following files: // Parse the block content const { editData, cleanContent, error, inProgress } = this.parseEditBlock(content, isComplete); + if (!editData && !error) { + // If no edit data and no error, skip this block + continue; + } // Escape content for HTML display const diff = diffWords(editData?.find || '', editData?.replace || ''); @@ -913,7 +910,7 @@ For context, the user is currently working on the following files: ).join('').trim(); let htmlRender = ''; - if (error || !editData) { + if (error) { // Error block console.error("Error parsing khoj-edit block:", error); console.error("Content causing error:", content); @@ -928,7 +925,7 @@ For context, the user is currently working on the following files:
${diffContent}
`; - } else if (inProgress) { + } else if (editData && inProgress) { // In-progress block htmlRender = `
📄 ${editData.file} In Progress @@ -936,7 +933,7 @@ For context, the user is currently working on the following files:
${diffContent}
`; - } else { + } else if (editData) { // Success block // Find the actual file that will be modified const targetFile = this.findBestMatchingFile(editData.file, files); From 82dc7b115bd76f96726ec1a9514cd96eb8be53c4 Mon Sep 17 00:00:00 2001 From: Debanjum Date: Wed, 20 Aug 2025 16:40:05 -0700 Subject: [PATCH 7/8] Fix to allow khoj to delete content in obsidian write mode Previous regex and replacement logic did not allow replace block to be empty --- .../obsidian/src/interact_with_files.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/interface/obsidian/src/interact_with_files.ts b/src/interface/obsidian/src/interact_with_files.ts index 98452ebe..baa6ce8f 100644 --- a/src/interface/obsidian/src/interact_with_files.ts +++ b/src/interface/obsidian/src/interact_with_files.ts @@ -432,8 +432,16 @@ For context, the user is currently working on the following files: } // Try parse SEARCH/REPLACE format for complete edit blocks - // Regex: file_path\n<<<<<<< SEARCH\nsearch_content\n=======\nreplacement_content\n>>>>>>> REPLACE - const newFormatRegex = /^([^\n]+)\n<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE\s*$/; + // Supports empty SEARCH (new file / replace whole file) and empty REPLACE (deletion) + // Regex structure: + // file_path (group 1) + // <<<<<<< SEARCH literal marker + // search_content (group 2, can be empty) + // ======= divider + // replacement_content (group 3, can be empty => deletion) + // >>>>>>> REPLACE end marker + // Note: The trailing newline before the end marker is optional to allow zero-length replacement + const newFormatRegex = /^([^\n]+)\n<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n?>>>>>>> REPLACE\s*$/; const newFormatMatch = newFormatRegex.exec(cleanContent); let editData: EditBlock | null = null; @@ -458,14 +466,14 @@ For context, the user is currently working on the following files: error = { type: 'missing_field', message: 'Missing "find" field markers', - details: 'The "find" field is required and should contain the content to find in the file' + details: 'The "find" field is required. It should contain the content to find in the file or be empty for new files' }; } - else if (editData && !editData.replace) { + else if (editData && editData.replace === undefined) { error = { type: 'missing_field', message: 'Missing "replace" field in edit block', - details: 'The "replace" field is required and should contain the replacement text' + details: 'The "replace" field is required. It should contain the content to replace or be empty to indicate deletion' }; } From 48ed7afab85ad25333f77556c7c8d61d477f3bda Mon Sep 17 00:00:00 2001 From: Debanjum Date: Wed, 20 Aug 2025 19:17:27 -0700 Subject: [PATCH 8/8] Do not show instructions in chat session title on obsidian In obsidian we have a hacky system instruction being passed in read, write file access modes. This shouldn't be shown in chat sessions list during view or edit. It is an internal implementation detail. --- src/interface/obsidian/src/chat_view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface/obsidian/src/chat_view.ts b/src/interface/obsidian/src/chat_view.ts index 1b0983ef..1b4dc480 100644 --- a/src/interface/obsidian/src/chat_view.ts +++ b/src/interface/obsidian/src/chat_view.ts @@ -1059,7 +1059,7 @@ export class KhojChatView extends KhojPaneView { if (incomingConversationId == conversationId) { conversationSessionEl.classList.add("selected-conversation"); } - const conversationTitle = conversation["slug"] || `New conversation 🌱`; + const conversationTitle = conversation["slug"].split("")[0].trim() || `New conversation 🌱`; const conversationSessionTitleEl = conversationSessionEl.createDiv("conversation-session-title"); conversationSessionTitleEl.textContent = conversationTitle; conversationSessionTitleEl.addEventListener('click', () => {