mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-02 21:19:12 +00:00
Major updates to Obsidian Khoj plugin chat interface and editing features (#1109)
## Description This PR introduces significant improvements to the Obsidian Khoj plugin's chat interface and editing capabilities, enhancing the overall user experience and content management functionality. ## Features ### 🔍 Enhanced Communication Mode I've implemented radio buttons below the chat window for easier communication mode selection. The modes are now displayed as emojis in the conversation for a cleaner interface, replacing the previous text-based system (e.g., /default, /research). I've also documented the search mode functionality in the help command. #### Screenshots - Radio buttons for mode selection - Emoji display in conversations  ### 💬 Revamped Message Interaction I've redesigned the message buttons with improved spacing and color coding for better visual differentiation. The new edit button allows quick message modifications - clicking it removes the conversation up to that point and copies the message to the input field for easy editing or retrying questions. #### Screenshots - New message styling and color scheme  - Edit button functionality  ### 🤖 Advanced Agent Selection System I've added a new chat creation button with agent selection capability. Users can now choose from their available agents when starting a new chat. While agents can't be switched mid-conversation to maintain context, users can easily start fresh conversations with different agents. #### Screenshots - Agent selection dropdown  ### 👁️ Real-Time Context Awareness I've added a button that gives Khoj access to read Obsidian opened tabs. This allows Khoj to read open notes and track changes in real-time, maintaining a history of previous versions to provide more contextual assistance. #### Screenshots - Window access toggle  ### ✏️ Smart Document Editing Inspired by Cursor IDE's intelligent editing and ChatGPT's Canvas functionality, I've implemented a first version of a content creation system we've been discussing. Using a JSON-based modification system, Khoj can now make precise changes to specific parts of files, with changes previewed in yellow highlighting before application. Modification code blocks are neatly organized in collapsible sections with clear action summaries. While this is just a first step, it's working remarkably well and I have several ideas for expanding this functionality to make Khoj an even more powerful content creation assistant. #### Screenshots - JSON modification preview - Change highlighting system - Collapsible code blocks - Accept/cancel controls  --------- Co-authored-by: Debanjum <debanjum@gmail.com>
This commit is contained in:
6
src/interface/obsidian/.gitignore
vendored
6
src/interface/obsidian/.gitignore
vendored
@@ -5,6 +5,12 @@
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# Cursor
|
||||
.cursor
|
||||
|
||||
# Copilot
|
||||
.github
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"second brain"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/dompurify": "3.2.0",
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "7.13.1",
|
||||
"@typescript-eslint/parser": "7.13.1",
|
||||
@@ -29,6 +29,7 @@
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"dompurify": "^3.1.4"
|
||||
"diff": "^8.0.2",
|
||||
"isomorphic-dompurify": "^2.25.0"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
976
src/interface/obsidian/src/interact_with_files.ts
Normal file
976
src/interface/obsidian/src/interact_with_files.ts
Normal file
@@ -0,0 +1,976 @@
|
||||
import { App, TFile } from 'obsidian';
|
||||
import { diffWords } from 'diff';
|
||||
|
||||
/**
|
||||
* Interface representing a block of edit instructions for modifying files
|
||||
*/
|
||||
export interface EditBlock {
|
||||
file: string; // Target file name [Required]
|
||||
find: string; // Content to find in file [Required]
|
||||
replace: string; // Content to replace with in file [Required]
|
||||
note?: string; // Brief explanation of edit [Optional]
|
||||
hasError?: boolean; // Flag to indicate parsing error [Optional]
|
||||
error?: {
|
||||
type: 'missing_field' | 'invalid_format' | 'preprocessing' | 'unknown';
|
||||
message: string;
|
||||
details?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface representing the result of parsing a Khoj edit block
|
||||
*/
|
||||
export interface ParsedEditBlock {
|
||||
editData: EditBlock | null;
|
||||
cleanContent: string;
|
||||
inProgress?: boolean;
|
||||
error?: {
|
||||
type: 'missing_field' | 'invalid_format' | 'preprocessing' | 'unknown';
|
||||
message: string;
|
||||
details?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface representing the result of processing an edit block
|
||||
*/
|
||||
interface ProcessedEditResult {
|
||||
preview: string; // The content with diff markers to be inserted
|
||||
newContent: string; // The new content after replacement
|
||||
error?: string; // Error message if processing failed (e.g., 'find' text not found)
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface representing the result of detecting a partial edit block
|
||||
*/
|
||||
interface PartialEditBlockResult {
|
||||
content: string;
|
||||
isComplete: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class that handles file operations for the Khoj plugin
|
||||
*/
|
||||
export class FileInteractions {
|
||||
private app: App;
|
||||
private readonly EDIT_BLOCK_START = '<khoj_edit>';
|
||||
private readonly EDIT_BLOCK_END = '</khoj_edit>';
|
||||
|
||||
/**
|
||||
* Constructor for FileInteractions
|
||||
*
|
||||
* @param app - The Obsidian App instance
|
||||
*/
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content of all open files
|
||||
*
|
||||
* @param fileAccessMode - The access mode ('none', 'read', or 'write')
|
||||
* @returns A string containing the content of all open files
|
||||
*/
|
||||
public async getOpenFilesContent(fileAccessMode: 'none' | 'read' | 'write'): Promise<string> {
|
||||
// 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 '';
|
||||
|
||||
// Instructions in write access mode
|
||||
let editInstructions: string = '';
|
||||
if (fileAccessMode === 'write') {
|
||||
editInstructions = `
|
||||
If the user requests, you can suggest edits to files provided in the WORKING_FILE_SET provided below.
|
||||
Once you understand the user request you MUST:
|
||||
|
||||
1. Decide if you need to propose *SEARCH/REPLACE* edits to any files that haven't been added to the chat.
|
||||
|
||||
If you need to propose edits to existing files not already added to the chat, you *MUST* tell the user their full path names and ask them to *add the files to the chat*.
|
||||
End your reply and wait for their approval.
|
||||
You can keep asking if you then decide you need to edit more files.
|
||||
|
||||
2. Think step-by-step and explain the needed changes in a few short sentences before each EDIT block.
|
||||
|
||||
3. Describe each change with a *SEARCH/REPLACE block* like the examples below.
|
||||
|
||||
All changes to files must use this *SEARCH/REPLACE block* format.
|
||||
ONLY EVER RETURN EDIT TEXT IN A *SEARCH/REPLACE BLOCK*!
|
||||
|
||||
# *SEARCH/REPLACE block* Rules:
|
||||
|
||||
Every *SEARCH/REPLACE block* must use this format:
|
||||
1. The opening fence: \`${this.EDIT_BLOCK_START}\`
|
||||
2. The *FULL* file path alone on a line, verbatim. No bold asterisks, no quotes around it, no escaping of characters, etc.
|
||||
3. The start of search block: <<<<<<< SEARCH
|
||||
4. A contiguous chunk of lines to search for in the source file
|
||||
5. The dividing line: =======
|
||||
6. The lines to replace into the source file
|
||||
7. The end of the replace block: >>>>>>> REPLACE
|
||||
8. The closing fence: \`${this.EDIT_BLOCK_END}\`
|
||||
|
||||
Use the *FULL* file path, as shown to you by the user.
|
||||
|
||||
Every *SEARCH* section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, etc.
|
||||
If the file contains code or other data wrapped/escaped in json/xml/quotes or other containers, you need to propose edits to the literal contents of the file, including the container markup.
|
||||
|
||||
*SEARCH/REPLACE* blocks will *only* replace the first match occurrence.
|
||||
Including multiple unique *SEARCH/REPLACE* blocks if needed.
|
||||
Include enough lines in each SEARCH section to uniquely match each set of lines that need to change.
|
||||
|
||||
Keep *SEARCH/REPLACE* blocks concise.
|
||||
Break large *SEARCH/REPLACE* blocks into a series of smaller blocks that each change a small portion of the file.
|
||||
Include just the changing lines, and a few surrounding lines if needed for uniqueness.
|
||||
Do not include long runs of unchanging lines in *SEARCH/REPLACE* blocks.
|
||||
|
||||
Only create *SEARCH/REPLACE* blocks for files that the user has added to the chat!
|
||||
|
||||
To move text within a file, use 2 *SEARCH/REPLACE* blocks: 1 to delete it from its current location, 1 to insert it in the new location.
|
||||
|
||||
Pay attention to which filenames the user wants you to edit, especially if they are asking you to create a new file.
|
||||
|
||||
If you want to put text in a new file, use a *SEARCH/REPLACE block* with:
|
||||
- A new file path, including dir name if needed
|
||||
- An empty \`SEARCH\` section
|
||||
- The new file's contents in the \`REPLACE\` section
|
||||
|
||||
ONLY EVER RETURN EDIT TEXT IN A *SEARCH/REPLACE BLOCK*!
|
||||
|
||||
<EDIT_INSTRUCTIONS>
|
||||
Suggest edits using targeted modifications. Use multiple edit blocks to make precise changes rather than rewriting entire sections.
|
||||
|
||||
Here's how to use the *SEARCH/REPLACE block* format:
|
||||
|
||||
${this.EDIT_BLOCK_START}
|
||||
target-filename
|
||||
<<<<<<< SEARCH
|
||||
from flask import Flask
|
||||
=======
|
||||
import math
|
||||
from flask import Flask
|
||||
>>>>>>> REPLACE
|
||||
${this.EDIT_BLOCK_END}
|
||||
|
||||
⚠️ Important:
|
||||
- The target-filename parameter is required and must match an open file name.
|
||||
- The XML format ${this.EDIT_BLOCK_START}...${this.EDIT_BLOCK_END} ensures reliable parsing.
|
||||
- The SEARCH block content must completely and uniquely identify the section to edit.
|
||||
- The REPLACE block content will replace the first SEARCH block match in the specified \`target-filename\`.
|
||||
|
||||
📝 Example note:
|
||||
|
||||
\`\`\`
|
||||
---
|
||||
date: 2024-01-20
|
||||
tags: meeting, planning
|
||||
status: active
|
||||
---
|
||||
# file: Meeting Notes.md
|
||||
|
||||
Action items from today:
|
||||
- Review Q4 metrics
|
||||
- Schedule follow-up with marketing team about new campaign launch
|
||||
- Update project timeline and milestones for Q1 2024
|
||||
|
||||
Next steps:
|
||||
- Send summary to team
|
||||
- Book conference room for next week
|
||||
\`\`\`
|
||||
|
||||
Examples of targeted edits:
|
||||
|
||||
1. Using just a few words to identify long text (notice how "campaign launch" is kept in content):
|
||||
|
||||
Add deadline and specificity to the marketing team follow-up.
|
||||
${this.EDIT_BLOCK_START}
|
||||
Meeting Notes.md
|
||||
<<<<<<< SEARCH
|
||||
- Schedule follow-up with marketing team about new campaign launch
|
||||
=======
|
||||
- Schedule follow-up with marketing team by Wednesday to discuss Q1 campaign launch
|
||||
>>>>>>> REPLACE
|
||||
${this.EDIT_BLOCK_END}
|
||||
|
||||
2. Multiple targeted changes with escaped characters:
|
||||
|
||||
Add HIGH priority flag with code reference to Q4 metrics review"
|
||||
${this.EDIT_BLOCK_START}
|
||||
Meeting Notes.md
|
||||
<<<<<<< SEARCH
|
||||
- Review Q4 metrics
|
||||
=======
|
||||
- [HIGH] Review Q4 metrics (see "metrics.ts" and \`calculateQ4Metrics()\`)
|
||||
>>>>>>> REPLACE
|
||||
</${this.EDIT_BLOCK_END}>
|
||||
|
||||
Add resource allocation to project timeline task
|
||||
<${this.EDIT_BLOCK_START}>
|
||||
Meeting Notes.md
|
||||
<<<<<<< SEARCH
|
||||
- Update project timeline and milestones for Q1 2024
|
||||
=======
|
||||
- Update project timeline and add resource allocation for Q1 2024
|
||||
>>>>>>> REPLACE
|
||||
</${this.EDIT_BLOCK_END}>
|
||||
|
||||
3. Adding new content between sections:
|
||||
Insert a new section for discussion points after the action items section:
|
||||
${this.EDIT_BLOCK_START}
|
||||
Meeting Notes.md
|
||||
<<<<<<< SEARCH
|
||||
Action items from today:
|
||||
- Review Q4 metrics
|
||||
- Schedule follow-up with marketing team about new campaign launch
|
||||
- Update project timeline and milestones for Q1 2024
|
||||
=======
|
||||
Action items from today:
|
||||
- Review Q4 metrics
|
||||
- Schedule follow-up
|
||||
- Update timeline
|
||||
|
||||
Discussion Points:
|
||||
- Budget review
|
||||
- Team feedback
|
||||
>>>>>>> REPLACE
|
||||
</${this.EDIT_BLOCK_END}>
|
||||
|
||||
4. Completely replacing a file content (preserving frontmatter):
|
||||
Replace entire file content while keeping frontmatter metadata
|
||||
${this.EDIT_BLOCK_START}
|
||||
Meeting Notes.md
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
# Project Overview
|
||||
|
||||
## Goals
|
||||
- Increase user engagement by 25%
|
||||
- Launch mobile app by Q3
|
||||
- Expand to 3 new markets
|
||||
|
||||
## Timeline
|
||||
1. Q1: Research & Planning
|
||||
2. Q2: Development
|
||||
3. Q3: Testing & Launch
|
||||
4. Q4: Market Expansion
|
||||
>>>>>>> REPLACE
|
||||
${this.EDIT_BLOCK_END}
|
||||
|
||||
- The SEARCH block must uniquely identify the section to edit
|
||||
- The REPLACE block content replaces the first SEARCH block match in the specified file
|
||||
- Frontmatter metadata (between --- markers at top of file) cannot be modified
|
||||
- Use an empty SEARCH block to replace entire file content with content in REPLACE block (while preserving frontmatter).
|
||||
- Remember to escape special characters: use \" for quotes in content
|
||||
- Each edit block must be fenced in ${this.EDIT_BLOCK_START}...${this.EDIT_BLOCK_END} XML tags
|
||||
|
||||
</EDIT_INSTRUCTIONS>
|
||||
`;
|
||||
}
|
||||
|
||||
let openFilesContent = `
|
||||
For context, the user is currently working on the following files:
|
||||
<WORKING_FILE_SET>
|
||||
|
||||
`;
|
||||
|
||||
for (const leaf of leaves) {
|
||||
const view = leaf.view as any;
|
||||
const file = view?.file;
|
||||
if (!file || file.extension !== 'md') continue;
|
||||
|
||||
// Read file content
|
||||
let fileContent: string;
|
||||
try {
|
||||
fileContent = await this.app.vault.read(file);
|
||||
} catch (error) {
|
||||
console.error(`Error reading file ${file.path}:`, error);
|
||||
continue;
|
||||
}
|
||||
|
||||
openFilesContent += `<OPEN_FILE>\n# file: ${file.basename}.md\n\n${fileContent}\n</OPEN_FILE>\n\n`;
|
||||
}
|
||||
|
||||
openFilesContent += "</WORKING_FILE_SET>\n";
|
||||
|
||||
// Collate open files content with instructions
|
||||
let context: string;
|
||||
if (fileAccessMode === 'write') {
|
||||
context = `\n\n<SYSTEM>${editInstructions + openFilesContent}</SYSTEM>`;
|
||||
} else {
|
||||
context = `\n\n<SYSTEM>${openFilesContent}</SYSTEM>`;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the Levenshtein distance between two strings
|
||||
*
|
||||
* @param a - First string
|
||||
* @param b - Second string
|
||||
* @returns The Levenshtein distance
|
||||
*/
|
||||
public levenshteinDistance(a: string, b: string): number {
|
||||
if (a.length === 0) return b.length;
|
||||
if (b.length === 0) return a.length;
|
||||
|
||||
const matrix = Array(b.length + 1).fill(null).map(() =>
|
||||
Array(a.length + 1).fill(null)
|
||||
);
|
||||
|
||||
for (let i = 0; i <= a.length; i++) matrix[0][i] = i;
|
||||
for (let j = 0; j <= b.length; j++) matrix[j][0] = j;
|
||||
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
||||
matrix[j][i] = Math.min(
|
||||
matrix[j][i - 1] + 1, // deletion
|
||||
matrix[j - 1][i] + 1, // insertion
|
||||
matrix[j - 1][i - 1] + cost // substitution
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[b.length][a.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the best matching file from a list of files based on the target name
|
||||
*
|
||||
* @param targetName - The name to match against
|
||||
* @param files - Array of TFile objects to search
|
||||
* @returns The best matching TFile or null if no matches found
|
||||
*/
|
||||
public findBestMatchingFile(targetName: string, files: TFile[]): TFile | null {
|
||||
const MAX_DISTANCE = 10;
|
||||
let bestMatch: { file: TFile, distance: number } | null = null;
|
||||
|
||||
for (const file of files) {
|
||||
// Try both with and without extension
|
||||
const distanceWithExt = this.levenshteinDistance(targetName.toLowerCase(), file.name.toLowerCase());
|
||||
const distanceWithoutExt = this.levenshteinDistance(targetName.toLowerCase(), file.basename.toLowerCase());
|
||||
const distance = Math.min(distanceWithExt, distanceWithoutExt);
|
||||
|
||||
if (distance <= MAX_DISTANCE && (!bestMatch || distance < bestMatch.distance)) {
|
||||
bestMatch = { file, distance };
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch?.file || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a text edit block from the content string
|
||||
* Enhanced to handle incomplete blocks and extract partial information
|
||||
*
|
||||
* @param content - The content from which to parse edit blocks
|
||||
* @param isComplete - Whether the edit block is complete (has closing tag)
|
||||
* @returns Object with the parsed edit data and cleaned content
|
||||
*/
|
||||
public parseEditBlock(content: string, isComplete: boolean = true): ParsedEditBlock {
|
||||
let cleanContent = '';
|
||||
try {
|
||||
// Normalize line breaks and clean control characters, but preserve empty lines
|
||||
cleanContent = content
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
|
||||
.trim();
|
||||
|
||||
// For incomplete blocks, try to extract partial information
|
||||
if (!isComplete) {
|
||||
// Initialize with basic structure
|
||||
const partialData: EditBlock = {
|
||||
file: "",
|
||||
find: "",
|
||||
replace: ""
|
||||
};
|
||||
|
||||
// Try to extract file name from the first line
|
||||
const firstLineMatch = cleanContent.match(/^([^\n]+)/);
|
||||
if (firstLineMatch) {
|
||||
partialData.file = firstLineMatch[1].trim();
|
||||
}
|
||||
|
||||
// Try to extract search content field
|
||||
const searchStartMatch = cleanContent.match(/<<<<<<< SEARCH\n([\s\S]*)/);
|
||||
if (searchStartMatch) {
|
||||
partialData.find = searchStartMatch[1];
|
||||
if (!partialData.file) { // If file not on first line, try line before SEARCH
|
||||
const lines = cleanContent.split('\n');
|
||||
const searchIndex = lines.findIndex(line => line.startsWith("<<<<<<< SEARCH"));
|
||||
if (searchIndex > 0) {
|
||||
partialData.file = lines[searchIndex - 1].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
editData: partialData,
|
||||
cleanContent,
|
||||
inProgress: true
|
||||
};
|
||||
}
|
||||
|
||||
// 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*$/;
|
||||
const newFormatMatch = newFormatRegex.exec(cleanContent);
|
||||
|
||||
let editData: EditBlock | null = null;
|
||||
if (newFormatMatch) {
|
||||
editData = {
|
||||
file: newFormatMatch[1].trim(),
|
||||
find: newFormatMatch[2],
|
||||
replace: newFormatMatch[3],
|
||||
};
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
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) {
|
||||
error = {
|
||||
type: 'missing_field',
|
||||
message: 'Missing "replace" field in edit block',
|
||||
details: 'The "replace" field is required and should contain the replacement text'
|
||||
};
|
||||
}
|
||||
|
||||
return error
|
||||
? { editData, cleanContent, error }
|
||||
: { editData, cleanContent };
|
||||
} catch (error) {
|
||||
console.error("Error parsing edit block:", error);
|
||||
console.error("Content causing error:", content);
|
||||
return {
|
||||
editData: null,
|
||||
cleanContent,
|
||||
error: {
|
||||
type: 'invalid_format',
|
||||
message: 'Invalid JSON format in edit block',
|
||||
details: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses all edit blocks from a message
|
||||
*
|
||||
* @param message - The message containing text edit blocks in XML format
|
||||
* @returns Array of EditBlock objects
|
||||
*/
|
||||
public parseEditBlocks(message: string): EditBlock[] {
|
||||
const editBlocks: EditBlock[] = [];
|
||||
// Set regex to match edit blocks based on Edit Start, End XML tags in the message
|
||||
const editBlockRegex = new RegExp(`${this.EDIT_BLOCK_START}([\\s\\S]*?)${this.EDIT_BLOCK_END}`, 'g');
|
||||
|
||||
let match;
|
||||
while ((match = editBlockRegex.exec(message)) !== null) {
|
||||
const { editData, cleanContent, error } = this.parseEditBlock(match[1]);
|
||||
|
||||
if (error) {
|
||||
console.error("Failed to parse edit block:", error);
|
||||
console.debug("Content causing error:", match[1]);
|
||||
editBlocks.push({
|
||||
file: "unknown", // Fallback value when editData is null
|
||||
find: "",
|
||||
replace: `Error: ${error.message}\nOriginal content:\n${match[1]}`,
|
||||
note: "Error parsing edit block",
|
||||
hasError: true,
|
||||
error: error
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!editData) {
|
||||
console.error("No edit data parsed");
|
||||
continue;
|
||||
}
|
||||
|
||||
editBlocks.push({
|
||||
note: "Suggested edit",
|
||||
file: editData.file,
|
||||
find: editData.find,
|
||||
replace: editData.replace,
|
||||
hasError: !!error,
|
||||
error: error || undefined
|
||||
});
|
||||
}
|
||||
|
||||
return editBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a preview with differences highlighted
|
||||
*
|
||||
* @param originalText - The original text
|
||||
* @param newText - The modified text
|
||||
* @returns A string with differences highlighted
|
||||
*/
|
||||
public createPreviewWithDiff(originalText: string, newText: string): string {
|
||||
// Define unique tokens to temporarily replace existing formatting markers
|
||||
const HIGHLIGHT_TOKEN = "___KHOJ_HIGHLIGHT_MARKER___";
|
||||
const STRIKETHROUGH_TOKEN = "___KHOJ_STRIKETHROUGH_MARKER___";
|
||||
|
||||
// Function to preserve existing formatting markers by replacing them with tokens
|
||||
const preserveFormatting = (text: string): string => {
|
||||
// Replace existing highlight markers with non-greedy pattern
|
||||
let processed = text.replace(/==(.*?)==/g, `${HIGHLIGHT_TOKEN}$1${HIGHLIGHT_TOKEN}`);
|
||||
// Replace existing strikethrough markers with non-greedy pattern
|
||||
processed = processed.replace(/~~(.*?)~~/g, `${STRIKETHROUGH_TOKEN}$1${STRIKETHROUGH_TOKEN}`);
|
||||
return processed;
|
||||
};
|
||||
|
||||
// Function to restore original formatting markers
|
||||
const restoreFormatting = (text: string): string => {
|
||||
// Restore highlight markers
|
||||
let processed = text.replace(new RegExp(HIGHLIGHT_TOKEN + "(.*?)" + HIGHLIGHT_TOKEN, "g"), "==$1==");
|
||||
// Restore strikethrough markers
|
||||
processed = processed.replace(new RegExp(STRIKETHROUGH_TOKEN + "(.*?)" + STRIKETHROUGH_TOKEN, "g"), "~~$1~~");
|
||||
return processed;
|
||||
};
|
||||
|
||||
// Preserve existing formatting in both texts
|
||||
const preservedOriginal = preserveFormatting(originalText);
|
||||
const preservedNew = preserveFormatting(newText);
|
||||
|
||||
// Find common prefix and suffix
|
||||
let prefixLength = 0;
|
||||
const minLength = Math.min(preservedOriginal.length, preservedNew.length);
|
||||
while (prefixLength < minLength && preservedOriginal[prefixLength] === preservedNew[prefixLength]) {
|
||||
prefixLength++;
|
||||
}
|
||||
|
||||
let suffixLength = 0;
|
||||
while (
|
||||
suffixLength < minLength - prefixLength &&
|
||||
preservedOriginal[preservedOriginal.length - 1 - suffixLength] === preservedNew[preservedNew.length - 1 - suffixLength]
|
||||
) {
|
||||
suffixLength++;
|
||||
}
|
||||
|
||||
// Extract the parts
|
||||
const commonPrefix = preservedOriginal.slice(0, prefixLength);
|
||||
const commonSuffix = preservedOriginal.slice(preservedOriginal.length - suffixLength);
|
||||
const originalDiff = preservedOriginal.slice(prefixLength, preservedOriginal.length - suffixLength);
|
||||
const newDiff = preservedNew.slice(prefixLength, preservedNew.length - suffixLength);
|
||||
|
||||
// Format the differences
|
||||
const formatLines = (text: string, marker: string): string => {
|
||||
if (!text) return '';
|
||||
return text.split('\n')
|
||||
.map(line => {
|
||||
line = line.trim();
|
||||
if (!line) {
|
||||
return marker === '==' ? '' : '~~';
|
||||
}
|
||||
return `${marker}${line}${marker}`;
|
||||
})
|
||||
.filter(line => line !== '~~')
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
// Create the diff preview with preserved formatting tokens
|
||||
const diffPreview = commonPrefix +
|
||||
(originalDiff ? formatLines(originalDiff, '~~') : '') +
|
||||
(newDiff ? formatLines(newDiff, '==') : '') +
|
||||
commonSuffix;
|
||||
|
||||
// Restore original formatting markers in the final result
|
||||
return restoreFormatting(diffPreview);
|
||||
}
|
||||
|
||||
private textNormalize(text: string): string {
|
||||
// Normalize whitespace and special characters
|
||||
return text
|
||||
.replace(/\u00A0/g, " ") // Replace non-breaking spaces with regular spaces
|
||||
.replace(/[\u2002\u2003\u2007\u2008\u2009\u200A\u205F\u3000]/g, " ") // Replace various other Unicode spaces with regular spaces
|
||||
.replace(/[\u2013\u2014]/g, '-') // Replace en-dash and em-dash with hyphen
|
||||
.replace(/[\u2018\u2019]/g, "'") // Replace smart quotes with regular quotes
|
||||
.replace(/[\u201C\u201D]/g, '"') // Replace smart double quotes with regular quotes
|
||||
.replace(/\u2026/g, '...') // Replace ellipsis with three dots
|
||||
.normalize('NFC') // Normalize to NFC form
|
||||
}
|
||||
|
||||
private processSingleEdit(
|
||||
rawFindText: string,
|
||||
replaceText: string,
|
||||
rawCurrentFileContent: string,
|
||||
frontmatterEndIndex: number
|
||||
): ProcessedEditResult {
|
||||
let startIndex = -1;
|
||||
let endIndex = -1;
|
||||
// Normalize special characters before searching
|
||||
const findText = this.textNormalize(rawFindText);
|
||||
const currentFileContent = this.textNormalize(rawCurrentFileContent);
|
||||
|
||||
if (findText === "") {
|
||||
// Empty search means replace entire content after frontmatter
|
||||
startIndex = frontmatterEndIndex;
|
||||
endIndex = currentFileContent.length;
|
||||
} else {
|
||||
startIndex = currentFileContent.indexOf(findText, frontmatterEndIndex);
|
||||
if (startIndex !== -1) {
|
||||
endIndex = startIndex + findText.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (startIndex === -1 || endIndex === -1 || startIndex > endIndex) {
|
||||
return {
|
||||
preview: "",
|
||||
newContent: currentFileContent,
|
||||
error: `No matching text found in file.`
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
const textToReplace = currentFileContent.substring(startIndex, endIndex);
|
||||
const newText = replaceText.trim();
|
||||
const preview = this.createPreviewWithDiff(textToReplace, newText);
|
||||
const newContent =
|
||||
currentFileContent.substring(0, startIndex) +
|
||||
preview +
|
||||
currentFileContent.substring(endIndex);
|
||||
|
||||
return { preview, newContent };
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies edit blocks to modify files
|
||||
*
|
||||
* @param editBlocks - Array of EditBlock objects to apply
|
||||
* @param addConfirmationButtons - Optional callback to add confirmation UI elements
|
||||
* @returns Object containing edit results and file backups
|
||||
*/
|
||||
public async applyEditBlocks(
|
||||
editBlocks: EditBlock[],
|
||||
onRetryNeeded?: (blockToRetry: EditBlock) => void
|
||||
): Promise<{
|
||||
editResults: { block: EditBlock, success: boolean, error?: string }[],
|
||||
fileBackups: Map<string, string>,
|
||||
}> {
|
||||
// Check for parsing errors first
|
||||
if (editBlocks.length === 0) {
|
||||
return { editResults: [], fileBackups: new Map() };
|
||||
}
|
||||
|
||||
// Store original content for each file in case we need to cancel
|
||||
const fileBackups = new Map<string, string>();
|
||||
|
||||
// Track current content for each file as we apply edits
|
||||
const currentFileContents = new Map<string, string>();
|
||||
|
||||
// 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');
|
||||
|
||||
// Track success/failure for each edit
|
||||
const editResults: { block: EditBlock, success: boolean, error?: string }[] = [];
|
||||
const blocksNeedingRetry: EditBlock[] = [];
|
||||
|
||||
// PHASE 1: Validation - Check all blocks before applying any changes
|
||||
const validationResults: { block: EditBlock, valid: boolean, error?: string, targetFile?: TFile }[] = [];
|
||||
|
||||
for (const block of editBlocks) {
|
||||
try {
|
||||
// Skip blocks with parsing errors
|
||||
if (block.hasError) {
|
||||
validationResults.push({
|
||||
block,
|
||||
valid: false,
|
||||
error: block.error?.message || 'Parsing error'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetFile = this.findBestMatchingFile(block.file, files);
|
||||
if (!targetFile) {
|
||||
validationResults.push({
|
||||
block,
|
||||
valid: false,
|
||||
error: `No matching file found for "${block.file}"`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read the file content if not already backed up
|
||||
if (!fileBackups.has(targetFile.path)) {
|
||||
const content = await this.app.vault.read(targetFile);
|
||||
fileBackups.set(targetFile.path, content);
|
||||
currentFileContents.set(targetFile.path, content);
|
||||
}
|
||||
|
||||
// Use current content (which may have been modified by previous validations)
|
||||
const currentContent = currentFileContents.get(targetFile.path)!;
|
||||
|
||||
// Find frontmatter boundaries
|
||||
const frontmatterMatch = currentContent.match(/^---\n[\s\S]*?\n---\n/);
|
||||
const frontmatterEndIndex = frontmatterMatch ? frontmatterMatch[0].length : 0;
|
||||
|
||||
const processedEdit = this.processSingleEdit(block.find, block.replace, currentContent, frontmatterEndIndex);
|
||||
|
||||
if (processedEdit.error) {
|
||||
validationResults.push({ block, valid: false, error: processedEdit.error });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validation passed
|
||||
validationResults.push({ block, valid: true, targetFile });
|
||||
|
||||
// Update the current content for this file for subsequent validations
|
||||
currentFileContents.set(targetFile.path, processedEdit.newContent);
|
||||
|
||||
} catch (error) {
|
||||
validationResults.push({ block, valid: false, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all blocks are valid
|
||||
const allValid = validationResults.every(result => result.valid);
|
||||
|
||||
// If any block is invalid, don't apply any changes
|
||||
if (!allValid) {
|
||||
// Reset current file contents
|
||||
currentFileContents.clear();
|
||||
|
||||
// Add all invalid blocks to retry list
|
||||
for (const result of validationResults) {
|
||||
if (!result.valid) {
|
||||
blocksNeedingRetry.push({
|
||||
...result.block,
|
||||
hasError: true,
|
||||
error: {
|
||||
type: 'invalid_format',
|
||||
message: result.error || 'Validation failed',
|
||||
details: result.error || 'Could not validate edit'
|
||||
}
|
||||
});
|
||||
|
||||
editResults.push({
|
||||
block: result.block,
|
||||
success: false,
|
||||
error: result.error || 'Validation failed'
|
||||
});
|
||||
} else {
|
||||
// Even valid blocks are considered failed in atomic mode if any block fails
|
||||
editResults.push({
|
||||
block: result.block,
|
||||
success: false,
|
||||
error: 'Other edits in the group failed validation'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger retry for the first failed block
|
||||
if (blocksNeedingRetry.length > 0 && onRetryNeeded) {
|
||||
onRetryNeeded(blocksNeedingRetry[0]);
|
||||
}
|
||||
|
||||
return { editResults, fileBackups };
|
||||
}
|
||||
|
||||
// PHASE 2: Application - Apply all changes since all blocks are valid
|
||||
try {
|
||||
// Reset current file contents to original state
|
||||
currentFileContents.clear();
|
||||
for (const [path, content] of fileBackups.entries()) {
|
||||
currentFileContents.set(path, content);
|
||||
}
|
||||
|
||||
// Apply all edits
|
||||
for (const result of validationResults) {
|
||||
const block = result.block;
|
||||
const targetFile = result.targetFile!;
|
||||
|
||||
// Use current content (which may have been modified by previous edits)
|
||||
const content = currentFileContents.get(targetFile.path)!;
|
||||
|
||||
// Find frontmatter boundaries
|
||||
const frontmatterMatch = content.match(/^---\n[\s\S]*?\n---\n/);
|
||||
const frontmatterEndIndex = frontmatterMatch ? frontmatterMatch[0].length : 0;
|
||||
|
||||
// Find the text to replace in original content
|
||||
// Recalculate based on the current state of the file content for this phase
|
||||
const processedEdit = this.processSingleEdit(block.find, block.replace, content, frontmatterEndIndex);
|
||||
|
||||
if (processedEdit.error) {
|
||||
throw new Error(`Failed to re-locate edit markers for file "${targetFile.basename}" during application. Content may have shifted.`);
|
||||
}
|
||||
|
||||
// Apply the changes to the file
|
||||
await this.app.vault.modify(targetFile, processedEdit.newContent);
|
||||
|
||||
// Update the current content for this file for subsequent edits
|
||||
currentFileContents.set(targetFile.path, processedEdit.newContent);
|
||||
|
||||
editResults.push({ block: {...block, replace: processedEdit.preview}, success: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error applying edits:`, error);
|
||||
|
||||
// Restore all files to their original state
|
||||
for (const [path, content] of fileBackups.entries()) {
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
if (file && file instanceof TFile) {
|
||||
await this.app.vault.modify(file, content);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all blocks as failed
|
||||
for (const block of editBlocks) {
|
||||
blocksNeedingRetry.push(block);
|
||||
editResults.push({
|
||||
block,
|
||||
success: false,
|
||||
error: `Failed to apply edits: ${error.message}`
|
||||
});
|
||||
}
|
||||
|
||||
// Trigger retry for the first block
|
||||
if (blocksNeedingRetry.length > 0 && onRetryNeeded) {
|
||||
onRetryNeeded(blocksNeedingRetry[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return { editResults, fileBackups };
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms content edit blocks in a message to HTML for display
|
||||
*
|
||||
* @param message - The message containing content edit blocks in XML format
|
||||
* @returns The transformed message with HTML for edit blocks
|
||||
*/
|
||||
public transformEditBlocks(message: string): string {
|
||||
// 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');
|
||||
|
||||
// Detect all edit blocks, including partial ones
|
||||
const partialBlocks = this.detectPartialEditBlocks(message);
|
||||
|
||||
// Process each detected block
|
||||
let transformedMessage = message;
|
||||
for (const block of partialBlocks) {
|
||||
const isComplete = block.isComplete;
|
||||
const content = block.content;
|
||||
|
||||
// Parse the block content
|
||||
const { editData, cleanContent, error, inProgress } = this.parseEditBlock(content, isComplete);
|
||||
|
||||
// Escape content for HTML display
|
||||
const diff = diffWords(editData?.find || '', editData?.replace || '');
|
||||
let diffContent = diff.map(part => {
|
||||
if (part.added) {
|
||||
return `<span class="cm-positive">${part.value}</span>`;
|
||||
} else if (part.removed) {
|
||||
return `<span class="cm-negative"><s>${part.value}</s></span>`;
|
||||
} else {
|
||||
return `<span>${part.value}</span>`;
|
||||
}
|
||||
}
|
||||
).join('').trim();
|
||||
|
||||
let htmlRender = '';
|
||||
if (error || !editData) {
|
||||
// Error block
|
||||
console.error("Error parsing khoj-edit block:", error);
|
||||
console.error("Content causing error:", content);
|
||||
|
||||
const errorTitle = `Error: ${error?.message || 'Parse error'}`;
|
||||
const errorDetails = `Failed to parse edit block. Please check the JSON format and ensure all required fields are present.`;
|
||||
|
||||
htmlRender = `<details class="khoj-edit-accordion error">
|
||||
<summary>${errorTitle}</summary>
|
||||
<div class="khoj-edit-content">
|
||||
<p class="khoj-edit-error-message">${errorDetails}</p>
|
||||
<pre><code class="language-md error">${diffContent}</code></pre>
|
||||
</div>
|
||||
</details>`;
|
||||
} else if (inProgress) {
|
||||
// In-progress block
|
||||
htmlRender = `<details class="khoj-edit-accordion in-progress">
|
||||
<summary>📄 ${editData.file} <span class="khoj-edit-status">In Progress</span></summary>
|
||||
<div class="khoj-edit-content">
|
||||
<pre><code class="language-md">${diffContent}</code></pre>
|
||||
</div>
|
||||
</details>`;
|
||||
} else {
|
||||
// Success block
|
||||
// Find the actual file that will be modified
|
||||
const targetFile = this.findBestMatchingFile(editData.file, files);
|
||||
const displayFileName = targetFile ? `${targetFile.basename}.${targetFile.extension}` : editData.file;
|
||||
|
||||
htmlRender = `<details class="khoj-edit-accordion success">
|
||||
<summary>📄 ${displayFileName}</summary>
|
||||
<div class="khoj-edit-content">
|
||||
<div>${diffContent}</div>
|
||||
</div>
|
||||
</details>`;
|
||||
}
|
||||
|
||||
// Replace the block in the message
|
||||
if (isComplete) {
|
||||
transformedMessage = transformedMessage.replace(`${this.EDIT_BLOCK_START}${content}${this.EDIT_BLOCK_END}`, htmlRender);
|
||||
} else {
|
||||
transformedMessage = transformedMessage.replace(`${this.EDIT_BLOCK_START}${content}`, htmlRender);
|
||||
}
|
||||
}
|
||||
|
||||
return transformedMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects partial edit blocks in a message
|
||||
* This allows for early detection of edit blocks before they are complete
|
||||
*
|
||||
* @param message - The message to search for partial edit blocks
|
||||
* @returns An array of detected blocks with their content and completion status
|
||||
*/
|
||||
public detectPartialEditBlocks(message: string): PartialEditBlockResult[] {
|
||||
const results: PartialEditBlockResult[] = [];
|
||||
|
||||
// This regex captures both complete and incomplete edit blocks
|
||||
// It looks for EDIT_BLOCK_START tag followed by any content, and then either EDIT_BLOCK_END or the end of the string
|
||||
const regex = new RegExp(`${this.EDIT_BLOCK_START}([\\s\\S]*?)(?:${this.EDIT_BLOCK_END}|$)`, 'g');
|
||||
|
||||
let match;
|
||||
while ((match = regex.exec(message)) !== null) {
|
||||
const content = match[1];
|
||||
const isComplete = match[0].endsWith(this.EDIT_BLOCK_END);
|
||||
|
||||
results.push({
|
||||
content,
|
||||
isComplete
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Plugin, WorkspaceLeaf } from 'obsidian';
|
||||
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';
|
||||
|
||||
@@ -17,6 +18,7 @@ export default class Khoj extends Plugin {
|
||||
this.addCommand({
|
||||
id: 'search',
|
||||
name: 'Search',
|
||||
hotkeys: [{ modifiers: ["Ctrl", "Alt"], key: "S" }],
|
||||
callback: () => { new KhojSearchModal(this.app, this.settings).open(); }
|
||||
});
|
||||
|
||||
@@ -24,7 +26,8 @@ export default class Khoj extends Plugin {
|
||||
this.addCommand({
|
||||
id: 'similar',
|
||||
name: 'Find similar notes',
|
||||
editorCallback: () => { new KhojSearchModal(this.app, this.settings, true).open(); }
|
||||
hotkeys: [{ modifiers: ["Ctrl", "Alt"], key: "F" }],
|
||||
editorCallback: () => { this.activateView(KhojView.SIMILAR); }
|
||||
});
|
||||
|
||||
// Add chat command. It can be triggered from anywhere
|
||||
@@ -34,6 +37,64 @@ export default class Khoj extends Plugin {
|
||||
callback: () => { this.activateView(KhojView.CHAT); }
|
||||
});
|
||||
|
||||
// Add similar documents view command
|
||||
this.addCommand({
|
||||
id: 'similar-view',
|
||||
name: 'Open Similar Documents View',
|
||||
callback: () => { this.activateView(KhojView.SIMILAR); }
|
||||
});
|
||||
|
||||
// Add new chat command with hotkey
|
||||
this.addCommand({
|
||||
id: 'new-chat',
|
||||
name: 'New Chat',
|
||||
hotkeys: [{ modifiers: ["Ctrl", "Alt"], key: "N" }],
|
||||
callback: async () => {
|
||||
// First, activate the chat view
|
||||
await this.activateView(KhojView.CHAT);
|
||||
|
||||
// Wait a short moment for the view to activate
|
||||
setTimeout(() => {
|
||||
// Try to get the active chat view
|
||||
const chatView = this.app.workspace.getActiveViewOfType(KhojChatView);
|
||||
if (chatView) {
|
||||
chatView.createNewConversation();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Add conversation history command with hotkey
|
||||
this.addCommand({
|
||||
id: 'conversation-history',
|
||||
name: 'Show Conversation History',
|
||||
hotkeys: [{ modifiers: ["Ctrl", "Alt"], key: "O" }],
|
||||
callback: () => {
|
||||
this.activateView(KhojView.CHAT).then(() => {
|
||||
const chatView = this.app.workspace.getActiveViewOfType(KhojChatView);
|
||||
if (chatView) {
|
||||
chatView.toggleChatSessions(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add voice capture command with hotkey
|
||||
this.addCommand({
|
||||
id: 'voice-capture',
|
||||
name: 'Start Voice Capture',
|
||||
hotkeys: [{ modifiers: ["Ctrl", "Alt"], key: "V" }],
|
||||
callback: () => {
|
||||
this.activateView(KhojView.CHAT).then(() => {
|
||||
const chatView = this.app.workspace.getActiveViewOfType(KhojChatView);
|
||||
if (chatView) {
|
||||
// Trigger speech to text functionality
|
||||
chatView.speechToText(new KeyboardEvent('keydown'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add sync command to manually sync new changes
|
||||
this.addCommand({
|
||||
id: 'sync',
|
||||
@@ -49,7 +110,34 @@ export default class Khoj extends Plugin {
|
||||
}
|
||||
});
|
||||
|
||||
// Add edit confirmation commands
|
||||
this.addCommand({
|
||||
id: 'apply-edits',
|
||||
name: 'Apply pending edits',
|
||||
hotkeys: [{ modifiers: ["Ctrl", "Shift"], key: "Enter" }],
|
||||
callback: () => {
|
||||
const chatView = this.app.workspace.getActiveViewOfType(KhojChatView);
|
||||
if (chatView) {
|
||||
chatView.applyPendingEdits();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'cancel-edits',
|
||||
name: 'Cancel pending edits',
|
||||
hotkeys: [{ modifiers: ["Ctrl", "Shift"], key: "Backspace" }],
|
||||
callback: () => {
|
||||
const chatView = this.app.workspace.getActiveViewOfType(KhojChatView);
|
||||
if (chatView) {
|
||||
chatView.cancelPendingEdits();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Register views
|
||||
this.registerView(KhojView.CHAT, (leaf) => new KhojChatView(leaf, this.settings));
|
||||
this.registerView(KhojView.SIMILAR, (leaf) => new KhojSimilarView(leaf, this.settings));
|
||||
|
||||
// Create an icon in the left ribbon.
|
||||
this.addRibbonIcon('message-circle', 'Khoj', (_: MouseEvent) => {
|
||||
@@ -108,35 +196,48 @@ export default class Khoj extends Plugin {
|
||||
this.unload();
|
||||
}
|
||||
|
||||
async activateView(viewType: KhojView) {
|
||||
async activateView(viewType: KhojView, existingLeaf?: WorkspaceLeaf) {
|
||||
const { workspace } = this.app;
|
||||
let leafToUse: WorkspaceLeaf | null = null;
|
||||
|
||||
let leaf: WorkspaceLeaf | null = null;
|
||||
const leaves = workspace.getLeavesOfType(viewType);
|
||||
|
||||
if (leaves.length > 0) {
|
||||
// A leaf with our view already exists, use that
|
||||
leaf = leaves[0];
|
||||
// Check if an existingLeaf is provided and is suitable for a view type switch
|
||||
if (existingLeaf && existingLeaf.view &&
|
||||
(existingLeaf.view.getViewType() === KhojView.CHAT || existingLeaf.view.getViewType() === KhojView.SIMILAR) &&
|
||||
existingLeaf.view.getViewType() !== viewType) {
|
||||
// The existing leaf is a Khoj pane and we want to switch its type
|
||||
leafToUse = existingLeaf;
|
||||
await leafToUse.setViewState({ type: viewType, active: true });
|
||||
} else {
|
||||
// Our view could not be found in the workspace, create a new leaf
|
||||
// in the right sidebar for it
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
await leaf?.setViewState({ type: viewType, active: true });
|
||||
// Standard logic: find an existing leaf of the target type, or create a new one
|
||||
const leaves = workspace.getLeavesOfType(viewType);
|
||||
if (leaves.length > 0) {
|
||||
leafToUse = leaves[0];
|
||||
} else {
|
||||
// If we are not switching an existing Khoj leaf,
|
||||
// and no leaf of the target type exists, create a new one.
|
||||
// Use the provided existingLeaf if it's not a Khoj pane we're trying to switch,
|
||||
// otherwise, get a new right leaf.
|
||||
leafToUse = (existingLeaf && !(existingLeaf.view instanceof KhojPaneView)) ? existingLeaf : workspace.getRightLeaf(false);
|
||||
if (leafToUse) {
|
||||
await leafToUse.setViewState({ type: viewType, active: true });
|
||||
} else {
|
||||
console.error("Khoj: Could not get a leaf to activate view.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (leaf) {
|
||||
const activeKhojLeaf = workspace.getActiveViewOfType(KhojPaneView)?.leaf;
|
||||
// Jump to the previous view if the current view is Khoj Side Pane
|
||||
if (activeKhojLeaf === leaf) jumpToPreviousView();
|
||||
// Else Reveal the leaf in case it is in a collapsed sidebar
|
||||
else {
|
||||
workspace.revealLeaf(leaf);
|
||||
if (leafToUse) {
|
||||
workspace.revealLeaf(leafToUse); // Ensure the leaf is visible
|
||||
|
||||
if (viewType === KhojView.CHAT) {
|
||||
// focus on the chat input when the chat view is opened
|
||||
let chatView = leaf.view as KhojChatView;
|
||||
let chatInput = <HTMLTextAreaElement>chatView.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
if (chatInput) chatInput.focus();
|
||||
// Specific actions after revealing/switching
|
||||
if (viewType === KhojView.CHAT) {
|
||||
// Ensure the view instance is correct after potential setViewState
|
||||
const chatView = leafToUse.view as KhojChatView;
|
||||
if (chatView instanceof KhojChatView) { // Double check instance type
|
||||
// Use a more robust way to get the input, or ensure it's always present after onOpen
|
||||
const chatInput = chatView.containerEl.querySelector<HTMLTextAreaElement>(".khoj-chat-input");
|
||||
chatInput?.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ItemView, WorkspaceLeaf } from 'obsidian';
|
||||
import { KhojSetting } from 'src/settings';
|
||||
import { KhojSearchModal } from 'src/search_modal';
|
||||
import { KhojView, populateHeaderPane } from './utils';
|
||||
|
||||
export abstract class KhojPaneView extends ItemView {
|
||||
@@ -20,13 +19,18 @@ export abstract class KhojPaneView extends ItemView {
|
||||
|
||||
// Add title to the Khoj Chat modal
|
||||
let headerEl = contentEl.createDiv(({ attr: { id: "khoj-header", class: "khoj-header" } }));
|
||||
|
||||
// Setup the header pane
|
||||
await populateHeaderPane(headerEl, this.setting);
|
||||
// Set the active nav pane
|
||||
headerEl.getElementsByClassName("chat-nav")[0]?.classList.add("khoj-nav-selected");
|
||||
headerEl.getElementsByClassName("chat-nav")[0]?.addEventListener("click", (_) => { this.activateView(KhojView.CHAT); });
|
||||
headerEl.getElementsByClassName("search-nav")[0]?.addEventListener("click", (_) => { new KhojSearchModal(this.app, this.setting).open(); });
|
||||
headerEl.getElementsByClassName("similar-nav")[0]?.addEventListener("click", (_) => { new KhojSearchModal(this.app, this.setting, true).open(); });
|
||||
const viewType = this.getViewType();
|
||||
await populateHeaderPane(headerEl, this.setting, viewType);
|
||||
|
||||
// Set the active nav pane based on the current view's type
|
||||
if (viewType === KhojView.CHAT) {
|
||||
headerEl.querySelector(".chat-nav")?.classList.add("khoj-nav-selected");
|
||||
} else if (viewType === KhojView.SIMILAR) {
|
||||
headerEl.querySelector(".similar-nav")?.classList.add("khoj-nav-selected");
|
||||
}
|
||||
// The similar-nav event listener is already set in utils.ts
|
||||
let similarNavSvgEl = headerEl.getElementsByClassName("khoj-nav-icon-similar")[0]?.firstElementChild;
|
||||
if (!!similarNavSvgEl) similarNavSvgEl.id = "similar-nav-icon-svg";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { App, Notice, PluginSettingTab, Setting, TFile, SuggestModal } from 'obsidian';
|
||||
import Khoj from 'src/main';
|
||||
import { canConnectToBackend, getBackendStatusMessage, updateContentIndex } from './utils';
|
||||
import { canConnectToBackend, fetchChatModels, fetchUserServerSettings, getBackendStatusMessage, updateContentIndex, updateServerChatModel } from './utils';
|
||||
|
||||
export interface UserInfo {
|
||||
username?: string;
|
||||
@@ -16,6 +16,16 @@ interface SyncFileTypes {
|
||||
pdf: boolean;
|
||||
}
|
||||
|
||||
export interface ModelOption {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ServerUserConfig {
|
||||
selected_chat_model_config?: number; // This is the ID from the server
|
||||
// Add other fields from UserConfig if needed by the plugin elsewhere
|
||||
}
|
||||
|
||||
export interface KhojSetting {
|
||||
resultsCount: number;
|
||||
khojUrl: string;
|
||||
@@ -27,10 +37,13 @@ export interface KhojSetting {
|
||||
userInfo: UserInfo | null;
|
||||
syncFolders: string[];
|
||||
syncInterval: number;
|
||||
autoVoiceResponse: boolean;
|
||||
selectedChatModelId: string | null; // Mirrors server's selected_chat_model_config
|
||||
availableChatModels: ModelOption[];
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: KhojSetting = {
|
||||
resultsCount: 6,
|
||||
resultsCount: 15,
|
||||
khojUrl: 'https://app.khoj.dev',
|
||||
khojApiKey: '',
|
||||
connectedToBackend: false,
|
||||
@@ -44,10 +57,14 @@ export const DEFAULT_SETTINGS: KhojSetting = {
|
||||
userInfo: null,
|
||||
syncFolders: [],
|
||||
syncInterval: 60,
|
||||
autoVoiceResponse: true,
|
||||
selectedChatModelId: null, // Will be populated from server
|
||||
availableChatModels: [],
|
||||
}
|
||||
|
||||
export class KhojSettingTab extends PluginSettingTab {
|
||||
plugin: Khoj;
|
||||
private chatModelSetting: Setting | null = null;
|
||||
|
||||
constructor(app: App, plugin: Khoj) {
|
||||
super(app, plugin);
|
||||
@@ -57,20 +74,75 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
this.chatModelSetting = null; // Reset when display is called
|
||||
|
||||
// Add notice whether able to connect to khoj backend or not
|
||||
let backendStatusEl = containerEl.createEl('small', {
|
||||
text: getBackendStatusMessage(
|
||||
this.plugin.settings.connectedToBackend,
|
||||
this.plugin.settings.userInfo?.email,
|
||||
this.plugin.settings.khojUrl,
|
||||
this.plugin.settings.khojApiKey
|
||||
)
|
||||
}
|
||||
let backendStatusMessage = getBackendStatusMessage(
|
||||
this.plugin.settings.connectedToBackend,
|
||||
this.plugin.settings.userInfo?.email,
|
||||
this.plugin.settings.khojUrl,
|
||||
this.plugin.settings.khojApiKey
|
||||
);
|
||||
let backendStatusMessage: string = '';
|
||||
|
||||
const connectHeaderEl = containerEl.createEl('h3', { title: backendStatusMessage });
|
||||
const connectHeaderContentEl = connectHeaderEl.createSpan({ cls: 'khoj-connect-settings-header' });
|
||||
const connectTitleEl = connectHeaderContentEl.createSpan({ text: 'Connect' });
|
||||
const backendStatusEl = connectTitleEl.createSpan({ text: this.connectStatusIcon(), cls: 'khoj-connect-settings-header-status' });
|
||||
if (this.plugin.settings.userInfo && this.plugin.settings.connectedToBackend) {
|
||||
if (this.plugin.settings.userInfo.photo) {
|
||||
const profilePicEl = connectHeaderContentEl.createEl('img', {
|
||||
attr: { src: this.plugin.settings.userInfo.photo },
|
||||
cls: 'khoj-profile'
|
||||
});
|
||||
profilePicEl.addEventListener('click', () => { new Notice(backendStatusMessage); });
|
||||
} else if (this.plugin.settings.userInfo.email) {
|
||||
const initial = this.plugin.settings.userInfo.email[0].toUpperCase();
|
||||
const profilePicEl = connectHeaderContentEl.createDiv({
|
||||
text: initial,
|
||||
cls: 'khoj-profile khoj-profile-initial'
|
||||
});
|
||||
profilePicEl.addEventListener('click', () => { new Notice(backendStatusMessage); });
|
||||
}
|
||||
}
|
||||
if (this.plugin.settings.userInfo && this.plugin.settings.userInfo.email) {
|
||||
connectHeaderEl.title = this.plugin.settings.userInfo?.email === 'default@example.com'
|
||||
? "Signed in"
|
||||
: `Signed in as ${this.plugin.settings.userInfo.email}`;
|
||||
}
|
||||
|
||||
// Add khoj settings configurable from the plugin settings tab
|
||||
const apiKeySetting = new Setting(containerEl)
|
||||
.setName('Khoj API Key')
|
||||
.addText(text => text
|
||||
.setValue(`${this.plugin.settings.khojApiKey}`)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.khojApiKey = value.trim();
|
||||
({
|
||||
connectedToBackend: this.plugin.settings.connectedToBackend,
|
||||
userInfo: this.plugin.settings.userInfo,
|
||||
statusMessage: backendStatusMessage,
|
||||
} = await canConnectToBackend(this.plugin.settings.khojUrl, this.plugin.settings.khojApiKey));
|
||||
|
||||
if (!this.plugin.settings.connectedToBackend) {
|
||||
this.plugin.settings.availableChatModels = [];
|
||||
this.plugin.settings.selectedChatModelId = null;
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
backendStatusEl.setText(this.connectStatusIcon())
|
||||
connectHeaderEl.title = backendStatusMessage;
|
||||
await this.refreshModelsAndServerPreference();
|
||||
}));
|
||||
|
||||
// Add API key setting description with link to get API key
|
||||
apiKeySetting.descEl.createEl('span', {
|
||||
text: 'Connect your Khoj Cloud account. ',
|
||||
});
|
||||
apiKeySetting.descEl.createEl('a', {
|
||||
text: 'Get your API Key',
|
||||
href: `${this.plugin.settings.khojUrl}/settings#clients`,
|
||||
attr: { target: '_blank' }
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Khoj URL')
|
||||
.setDesc('The URL of the Khoj backend.')
|
||||
@@ -84,29 +156,46 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
statusMessage: backendStatusMessage,
|
||||
} = await canConnectToBackend(this.plugin.settings.khojUrl, this.plugin.settings.khojApiKey));
|
||||
|
||||
if (!this.plugin.settings.connectedToBackend) {
|
||||
this.plugin.settings.availableChatModels = [];
|
||||
this.plugin.settings.selectedChatModelId = null;
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
backendStatusEl.setText(backendStatusMessage);
|
||||
backendStatusEl.setText(this.connectStatusIcon())
|
||||
connectHeaderEl.title = backendStatusMessage;
|
||||
await this.refreshModelsAndServerPreference();
|
||||
}));
|
||||
|
||||
// Interact section
|
||||
containerEl.createEl('h3', { text: 'Interact' });
|
||||
|
||||
// Chat Model Dropdown
|
||||
this.renderChatModelDropdown();
|
||||
|
||||
// Initial fetch of models and server preference if connected
|
||||
if (this.plugin.settings.connectedToBackend) {
|
||||
// Defer slightly to ensure UI is ready and avoid race conditions
|
||||
setTimeout(async () => {
|
||||
await this.refreshModelsAndServerPreference();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Add new setting for auto voice response after voice input
|
||||
new Setting(containerEl)
|
||||
.setName('Khoj API Key')
|
||||
.setDesc('Use Khoj Cloud with your Khoj API Key')
|
||||
.addText(text => text
|
||||
.setValue(`${this.plugin.settings.khojApiKey}`)
|
||||
.setName('Auto Voice Response')
|
||||
.setDesc('Automatically read responses after voice messages')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoVoiceResponse)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.khojApiKey = value.trim();
|
||||
({
|
||||
connectedToBackend: this.plugin.settings.connectedToBackend,
|
||||
userInfo: this.plugin.settings.userInfo,
|
||||
statusMessage: backendStatusMessage,
|
||||
} = await canConnectToBackend(this.plugin.settings.khojUrl, this.plugin.settings.khojApiKey));
|
||||
this.plugin.settings.autoVoiceResponse = value;
|
||||
await this.plugin.saveSettings();
|
||||
backendStatusEl.setText(backendStatusMessage);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Results Count')
|
||||
.setDesc('The number of results to show in search and use for chat.')
|
||||
.addSlider(slider => slider
|
||||
.setLimits(1, 10, 1)
|
||||
.setLimits(1, 30, 1)
|
||||
.setValue(this.plugin.settings.resultsCount)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value) => {
|
||||
@@ -117,6 +206,16 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
// Add new "Sync" heading
|
||||
containerEl.createEl('h3', { text: 'Sync' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto Sync')
|
||||
.setDesc('Automatically index your vault with Khoj.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoConfigure)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoConfigure = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Add setting to sync markdown notes
|
||||
new Setting(containerEl)
|
||||
.setName('Sync Notes')
|
||||
@@ -150,16 +249,6 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto Sync')
|
||||
.setDesc('Automatically index your vault with Khoj.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoConfigure)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoConfigure = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Add setting for sync interval
|
||||
const syncIntervalValues = [1, 5, 10, 20, 30, 45, 60, 120, 1440];
|
||||
new Setting(containerEl)
|
||||
@@ -184,7 +273,7 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
|
||||
// Add setting to manage sync folders
|
||||
const syncFoldersContainer = containerEl.createDiv('sync-folders-container');
|
||||
const foldersSetting = new Setting(syncFoldersContainer)
|
||||
new Setting(syncFoldersContainer)
|
||||
.setName('Sync Folders')
|
||||
.setDesc('Specify folders to sync (leave empty to sync entire vault)')
|
||||
.addButton(button => button
|
||||
@@ -252,6 +341,104 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
);
|
||||
}
|
||||
|
||||
private connectStatusIcon() {
|
||||
if (this.plugin.settings.connectedToBackend && this.plugin.settings.userInfo?.email)
|
||||
return '🟢';
|
||||
else if (this.plugin.settings.connectedToBackend)
|
||||
return '🟡'
|
||||
else
|
||||
return '🔴';
|
||||
}
|
||||
|
||||
private async refreshModelsAndServerPreference() {
|
||||
let serverSelectedModelId: string | null = null;
|
||||
if (this.plugin.settings.connectedToBackend) {
|
||||
const [availableModels, serverConfig] = await Promise.all([
|
||||
fetchChatModels(this.plugin.settings),
|
||||
fetchUserServerSettings(this.plugin.settings)
|
||||
]);
|
||||
|
||||
this.plugin.settings.availableChatModels = availableModels;
|
||||
|
||||
if (serverConfig && serverConfig.selected_chat_model_config !== undefined && serverConfig.selected_chat_model_config !== null) {
|
||||
const serverModelIdStr = serverConfig.selected_chat_model_config.toString();
|
||||
// Ensure the server's selected model is actually in the available list
|
||||
if (this.plugin.settings.availableChatModels.some(m => m.id === serverModelIdStr)) {
|
||||
serverSelectedModelId = serverModelIdStr;
|
||||
} else {
|
||||
// Server has a selection, but it's not in the options list (e.g. model removed, or different set of models)
|
||||
// In this case, we might fall back to null (Khoj Default)
|
||||
console.warn(`Khoj: Server's selected model ID ${serverModelIdStr} not in available models. Falling back to default.`);
|
||||
serverSelectedModelId = null;
|
||||
}
|
||||
} else {
|
||||
// No specific model configured on the server, or it's explicitly null
|
||||
serverSelectedModelId = null;
|
||||
}
|
||||
this.plugin.settings.selectedChatModelId = serverSelectedModelId;
|
||||
|
||||
} else {
|
||||
this.plugin.settings.availableChatModels = [];
|
||||
this.plugin.settings.selectedChatModelId = null; // Clear selection if disconnected
|
||||
}
|
||||
await this.plugin.saveSettings(); // Save the potentially updated selectedChatModelId
|
||||
this.renderChatModelDropdown(); // Re-render the dropdown with new data
|
||||
}
|
||||
|
||||
private renderChatModelDropdown() {
|
||||
if (!this.chatModelSetting) {
|
||||
this.chatModelSetting = new Setting(this.containerEl)
|
||||
.setName('Chat Model');
|
||||
} else {
|
||||
// Clear previous description and controls to prepare for re-rendering
|
||||
this.chatModelSetting.descEl.empty();
|
||||
this.chatModelSetting.controlEl.empty();
|
||||
}
|
||||
// Use this.chatModelSetting directly for modifications
|
||||
const modelSetting = this.chatModelSetting;
|
||||
|
||||
if (!this.plugin.settings.connectedToBackend) {
|
||||
modelSetting.setDesc('Connect to Khoj to load and set chat model options.');
|
||||
modelSetting.addText(text => text.setValue("Not connected").setDisabled(true));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.plugin.settings.availableChatModels.length === 0 && this.plugin.settings.connectedToBackend) {
|
||||
modelSetting.setDesc('Fetching models or no models available. Check Khoj connection or try refreshing.');
|
||||
modelSetting.addButton(button => button
|
||||
.setButtonText('Refresh Models')
|
||||
.onClick(async () => {
|
||||
button.setButtonText('Refreshing...').setDisabled(true);
|
||||
await this.refreshModelsAndServerPreference();
|
||||
// Re-rendering happens inside refreshModelsAndServerPreference
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
modelSetting.setDesc('The default AI model used for chat.');
|
||||
modelSetting.addDropdown(dropdown => {
|
||||
dropdown.addOption('', 'Default'); // Placeholder when cannot retrieve chat model options from server.
|
||||
this.plugin.settings.availableChatModels.forEach(model => {
|
||||
dropdown.addOption(model.id, model.name);
|
||||
});
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.selectedChatModelId || '')
|
||||
.onChange(async (value) => {
|
||||
// Attempt to update the server
|
||||
const success = await updateServerChatModel(value, this.plugin.settings);
|
||||
if (success) {
|
||||
await this.plugin.saveSettings();
|
||||
} else {
|
||||
// Server update failed, revert dropdown to the current setting value
|
||||
// to avoid UI mismatch.
|
||||
dropdown.setValue(this.plugin.settings.selectedChatModelId || '');
|
||||
}
|
||||
// Potentially re-render or refresh if needed, though setValue should update UI.
|
||||
// this.refreshModelsAndServerPreference(); // Could be called to ensure full sync, but might be too much
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper method to update the folder list display
|
||||
private updateFolderList(containerEl: HTMLElement) {
|
||||
containerEl.empty();
|
||||
|
||||
434
src/interface/obsidian/src/similar_view.ts
Normal file
434
src/interface/obsidian/src/similar_view.ts
Normal file
@@ -0,0 +1,434 @@
|
||||
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';
|
||||
|
||||
export interface SimilarResult {
|
||||
entry: string;
|
||||
file: string;
|
||||
inVault: boolean;
|
||||
}
|
||||
|
||||
export class KhojSimilarView extends KhojPaneView {
|
||||
static iconName: string = "search";
|
||||
setting: KhojSetting;
|
||||
currentController: AbortController | null = null;
|
||||
isLoading: boolean = false;
|
||||
loadingEl: HTMLElement;
|
||||
resultsContainerEl: HTMLElement;
|
||||
searchInputEl: HTMLInputElement;
|
||||
currentFile: TFile | null = null;
|
||||
fileWatcher: any;
|
||||
component: any;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, setting: KhojSetting) {
|
||||
super(leaf, setting);
|
||||
this.setting = setting;
|
||||
this.component = this;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return KhojView.SIMILAR;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return "Khoj Similar Documents";
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return "search";
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
await super.onOpen();
|
||||
const { contentEl } = this;
|
||||
|
||||
// Create main container
|
||||
const mainContainerEl = contentEl.createDiv({ cls: "khoj-similar-container" });
|
||||
|
||||
// Create search input container
|
||||
const searchContainerEl = mainContainerEl.createDiv({ cls: "khoj-similar-search-container" });
|
||||
|
||||
// Create search input
|
||||
this.searchInputEl = searchContainerEl.createEl("input", {
|
||||
cls: "khoj-similar-search-input",
|
||||
attr: {
|
||||
type: "text",
|
||||
placeholder: "Search or use current file"
|
||||
}
|
||||
});
|
||||
|
||||
// Create refresh button
|
||||
const refreshButtonEl = searchContainerEl.createEl("button", {
|
||||
cls: "khoj-similar-refresh-button"
|
||||
});
|
||||
setIcon(refreshButtonEl, "refresh-cw");
|
||||
refreshButtonEl.createSpan({ text: "Refresh" });
|
||||
refreshButtonEl.addEventListener("click", () => {
|
||||
this.updateSimilarDocuments();
|
||||
});
|
||||
|
||||
// Create results container
|
||||
this.resultsContainerEl = mainContainerEl.createDiv({ cls: "khoj-similar-results" });
|
||||
|
||||
// Create loading element
|
||||
this.loadingEl = mainContainerEl.createDiv({ cls: "search-loading" });
|
||||
const spinnerEl = this.loadingEl.createDiv({ cls: "search-loading-spinner" });
|
||||
|
||||
this.loadingEl.style.position = "absolute";
|
||||
this.loadingEl.style.top = "50%";
|
||||
this.loadingEl.style.left = "50%";
|
||||
this.loadingEl.style.transform = "translate(-50%, -50%)";
|
||||
this.loadingEl.style.zIndex = "1000";
|
||||
this.loadingEl.style.display = "none";
|
||||
|
||||
// Register event handlers
|
||||
this.registerFileActiveHandler();
|
||||
|
||||
// Add event listener for search input
|
||||
this.searchInputEl.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
this.getSimilarDocuments(this.searchInputEl.value);
|
||||
}
|
||||
});
|
||||
|
||||
// Update similar documents for current file
|
||||
this.updateSimilarDocuments();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register handler for file active events
|
||||
*/
|
||||
registerFileActiveHandler() {
|
||||
// Clean up existing watcher if any
|
||||
if (this.fileWatcher) {
|
||||
this.app.workspace.off("file-open", this.fileWatcher);
|
||||
}
|
||||
|
||||
// Set up new watcher
|
||||
this.fileWatcher = this.app.workspace.on("file-open", (file) => {
|
||||
if (file) {
|
||||
this.currentFile = file;
|
||||
this.updateSimilarDocuments();
|
||||
}
|
||||
});
|
||||
|
||||
// Register for cleanup when view is closed
|
||||
this.register(() => {
|
||||
this.app.workspace.off("file-open", this.fileWatcher);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update similar documents based on current file
|
||||
*/
|
||||
async updateSimilarDocuments() {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
|
||||
if (!file) {
|
||||
this.updateUI("no-file");
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.extension !== 'md') {
|
||||
this.updateUI("unsupported-file");
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentFile = file;
|
||||
|
||||
// Read file content
|
||||
const content = await this.app.vault.read(file);
|
||||
|
||||
// Get similar documents
|
||||
await this.getSimilarDocuments(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get similar documents from Khoj API
|
||||
*
|
||||
* @param query - The query text to find similar documents
|
||||
*/
|
||||
async getSimilarDocuments(query: string): Promise<SimilarResult[]> {
|
||||
// Do not show loading if the query is empty
|
||||
if (!query.trim()) {
|
||||
this.isLoading = false;
|
||||
this.updateLoadingState();
|
||||
this.updateUI("empty-query");
|
||||
return [];
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
this.isLoading = true;
|
||||
this.updateLoadingState();
|
||||
this.updateUI("loading");
|
||||
|
||||
// Cancel previous request if it exists
|
||||
if (this.currentController) {
|
||||
this.currentController.abort();
|
||||
}
|
||||
|
||||
try {
|
||||
// Create a new controller for this request
|
||||
this.currentController = new AbortController();
|
||||
|
||||
// Setup Query Khoj backend for search results
|
||||
let encodedQuery = encodeURIComponent(query);
|
||||
let searchUrl = `${this.setting.khojUrl}/api/search?q=${encodedQuery}&n=${this.setting.resultsCount}&r=true&client=obsidian`;
|
||||
let headers = {
|
||||
'Authorization': `Bearer ${this.setting.khojApiKey}`,
|
||||
}
|
||||
|
||||
// Get search results from Khoj backend
|
||||
const response = await fetch(searchUrl, {
|
||||
headers: headers,
|
||||
signal: this.currentController.signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Parse search results
|
||||
let results = data
|
||||
.filter((result: any) => {
|
||||
// Filter out the current file if it's in the results
|
||||
if (this.currentFile && result.additional.file.endsWith(this.currentFile.path)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((result: any) => {
|
||||
return {
|
||||
entry: result.entry,
|
||||
file: result.additional.file,
|
||||
inVault: this.isFileInVault(result.additional.file)
|
||||
} as SimilarResult;
|
||||
})
|
||||
.sort((a: SimilarResult, b: SimilarResult) => {
|
||||
if (a.inVault === b.inVault) return 0;
|
||||
return a.inVault ? -1 : 1;
|
||||
});
|
||||
|
||||
// Hide loading state
|
||||
this.isLoading = false;
|
||||
this.updateLoadingState();
|
||||
|
||||
// Render results
|
||||
this.renderResults(results);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
// Ignore cancellation errors
|
||||
if (error.name === 'AbortError') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// For other errors, show error state
|
||||
console.error('Search error:', error);
|
||||
this.isLoading = false;
|
||||
this.updateLoadingState();
|
||||
this.updateUI("error", error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is in the vault
|
||||
*
|
||||
* @param filePath - The file path to check
|
||||
* @returns True if the file is in the vault
|
||||
*/
|
||||
isFileInVault(filePath: string): boolean {
|
||||
const files = this.app.vault.getFiles();
|
||||
return files.some(file => filePath.endsWith(file.path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render search results
|
||||
*
|
||||
* @param results - The search results to render
|
||||
*/
|
||||
renderResults(results: SimilarResult[]) {
|
||||
// Clear previous results
|
||||
this.resultsContainerEl.empty();
|
||||
|
||||
if (results.length === 0) {
|
||||
this.updateUI("no-results");
|
||||
return;
|
||||
}
|
||||
|
||||
// Show results count
|
||||
this.resultsContainerEl.createEl("div", {
|
||||
cls: "khoj-results-count",
|
||||
text: `Found ${results.length} similar document${results.length > 1 ? 's' : ''}`
|
||||
});
|
||||
|
||||
// Create results list
|
||||
const resultsListEl = this.resultsContainerEl.createEl("div", { cls: "khoj-similar-results-list" });
|
||||
|
||||
// Render each result
|
||||
results.forEach(async (result) => {
|
||||
const resultEl = resultsListEl.createEl("div", { cls: "khoj-similar-result-item" });
|
||||
|
||||
// Extract filename
|
||||
let os_path_separator = result.file.includes('\\') ? '\\' : '/';
|
||||
let filename = result.file.split(os_path_separator).pop();
|
||||
|
||||
// Create header container for filename and more context button
|
||||
const headerEl = resultEl.createEl("div", { cls: "khoj-similar-result-header" });
|
||||
|
||||
// Show filename with appropriate color
|
||||
const fileEl = headerEl.createEl("div", {
|
||||
cls: `khoj-result-file ${result.inVault ? 'in-vault' : 'not-in-vault'}`
|
||||
});
|
||||
fileEl.setText(filename ?? "");
|
||||
|
||||
// Add indicator for files not in vault
|
||||
if (!result.inVault) {
|
||||
fileEl.createSpan({
|
||||
text: " (not in vault)",
|
||||
cls: "khoj-result-file-status"
|
||||
});
|
||||
}
|
||||
|
||||
// Add "More context" button
|
||||
const moreContextButton = headerEl.createEl("button", {
|
||||
cls: "khoj-more-context-button"
|
||||
});
|
||||
moreContextButton.createSpan({ text: "More context" });
|
||||
setIcon(moreContextButton.createSpan(), "chevron-down");
|
||||
|
||||
// Create content element (hidden by default)
|
||||
const contentEl = resultEl.createEl("div", {
|
||||
cls: "khoj-result-entry khoj-similar-content-hidden"
|
||||
});
|
||||
|
||||
// Prepare content for rendering
|
||||
let contentToRender = "";
|
||||
|
||||
// Remove YAML frontmatter
|
||||
result.entry = result.entry.replace(/---[\n\r][\s\S]*---[\n\r]/, '');
|
||||
|
||||
// Truncate to 8 lines
|
||||
const lines_to_render = 8;
|
||||
let entry_snipped_indicator = result.entry.split('\n').length > lines_to_render ? ' **...**' : '';
|
||||
let snipped_entry = result.entry.split('\n').slice(0, lines_to_render).join('\n');
|
||||
contentToRender = `${snipped_entry}${entry_snipped_indicator}`;
|
||||
|
||||
// Render markdown
|
||||
await MarkdownRenderer.renderMarkdown(
|
||||
contentToRender,
|
||||
contentEl,
|
||||
result.file,
|
||||
this.component
|
||||
);
|
||||
|
||||
// Add click handler to the more context button
|
||||
moreContextButton.addEventListener("click", (e) => {
|
||||
e.stopPropagation(); // Prevent opening the file
|
||||
|
||||
// Toggle content visibility
|
||||
if (contentEl.classList.contains("khoj-similar-content-hidden")) {
|
||||
contentEl.classList.remove("khoj-similar-content-hidden");
|
||||
contentEl.classList.add("khoj-similar-content-visible");
|
||||
moreContextButton.empty();
|
||||
moreContextButton.createSpan({ text: "Less context" });
|
||||
setIcon(moreContextButton.createSpan(), "chevron-up");
|
||||
} else {
|
||||
contentEl.classList.remove("khoj-similar-content-visible");
|
||||
contentEl.classList.add("khoj-similar-content-hidden");
|
||||
moreContextButton.empty();
|
||||
moreContextButton.createSpan({ text: "More context" });
|
||||
setIcon(moreContextButton.createSpan(), "chevron-down");
|
||||
}
|
||||
});
|
||||
|
||||
// Add click handler to open the file
|
||||
resultEl.addEventListener("click", (e) => {
|
||||
// Don't open if clicking on the more context button
|
||||
if (e.target === moreContextButton || moreContextButton.contains(e.target as Node)) {
|
||||
return;
|
||||
}
|
||||
this.openResult(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a search result
|
||||
*
|
||||
* @param result - The result to open
|
||||
*/
|
||||
async openResult(result: SimilarResult) {
|
||||
// Only open files that are in the vault
|
||||
if (!result.inVault) {
|
||||
new Notice("This file is not in your vault");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all markdown and binary files in vault
|
||||
const mdFiles = this.app.vault.getMarkdownFiles();
|
||||
const binaryFiles = this.app.vault.getFiles().filter(file =>
|
||||
supportedBinaryFileTypes.includes(file.extension)
|
||||
);
|
||||
|
||||
// Find and open the file
|
||||
let linkToEntry = getLinkToEntry(
|
||||
mdFiles.concat(binaryFiles),
|
||||
result.file,
|
||||
result.entry
|
||||
);
|
||||
|
||||
if (linkToEntry) {
|
||||
this.app.workspace.openLinkText(linkToEntry, '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the loading state
|
||||
*/
|
||||
private updateLoadingState() {
|
||||
this.loadingEl.style.display = this.isLoading ? "block" : "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the UI based on the current state
|
||||
*
|
||||
* @param state - The current state
|
||||
* @param message - Optional message for error states
|
||||
*/
|
||||
updateUI(state: "loading" | "no-file" | "unsupported-file" | "no-results" | "error" | "empty-query", message?: string) {
|
||||
// Clear results container if not loading
|
||||
if (state !== "loading") {
|
||||
this.resultsContainerEl.empty();
|
||||
}
|
||||
|
||||
// Create message element
|
||||
const messageEl = this.resultsContainerEl.createEl("div", { cls: "khoj-similar-message" });
|
||||
|
||||
// Set message based on state
|
||||
switch (state) {
|
||||
case "loading":
|
||||
// Loading is handled by the loading spinner
|
||||
break;
|
||||
case "no-file":
|
||||
messageEl.setText("No file is currently open. Open a markdown file to see similar documents.");
|
||||
break;
|
||||
case "unsupported-file":
|
||||
messageEl.setText("This file type is not supported. Only markdown files are supported.");
|
||||
break;
|
||||
case "no-results":
|
||||
messageEl.setText("No similar documents found.");
|
||||
break;
|
||||
case "error":
|
||||
messageEl.setText(`Error: ${message || "Failed to fetch similar documents"}`);
|
||||
break;
|
||||
case "empty-query":
|
||||
messageEl.setText("Please enter a search query or open a markdown file.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FileSystemAdapter, Notice, Vault, Modal, TFile, request, setIcon, Editor } from 'obsidian';
|
||||
import { KhojSetting, UserInfo } from 'src/settings'
|
||||
import { FileSystemAdapter, Notice, Vault, Modal, TFile, request, setIcon, Editor, App, WorkspaceLeaf } from 'obsidian';
|
||||
import { KhojSetting, ModelOption, ServerUserConfig, UserInfo } from 'src/settings'
|
||||
import { KhojSearchModal } from './search_modal';
|
||||
|
||||
export function getVaultAbsolutePath(vault: Vault): string {
|
||||
let adaptor = vault.adapter;
|
||||
@@ -162,8 +163,9 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
|
||||
batch.forEach(fileItem => { formData.append('files', fileItem.blob, fileItem.path) });
|
||||
|
||||
// Call Khoj backend to sync index with updated files in vault
|
||||
const method = regenerate ? "PUT" : "PATCH";
|
||||
const response = await fetch(`${setting.khojUrl}/api/content?client=obsidian`, {
|
||||
method: "PATCH",
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${setting.khojApiKey}`,
|
||||
},
|
||||
@@ -317,12 +319,12 @@ export function getBackendStatusMessage(
|
||||
return `✅ Connected to Khoj. ❗️Get a valid API key from ${khojUrl}/settings#clients to log in`;
|
||||
else if (userEmail === 'default@example.com')
|
||||
// Logged in as default user in anonymous mode
|
||||
return `✅ Signed in to Khoj`;
|
||||
return `✅ Welcome back to Khoj`;
|
||||
else
|
||||
return `✅ Signed in to Khoj as ${userEmail}`;
|
||||
return `✅ Welcome back to Khoj, ${userEmail}`;
|
||||
}
|
||||
|
||||
export async function populateHeaderPane(headerEl: Element, setting: KhojSetting): Promise<void> {
|
||||
export async function populateHeaderPane(headerEl: Element, setting: KhojSetting, viewType: string): Promise<void> {
|
||||
let userInfo: UserInfo | null = null;
|
||||
try {
|
||||
const { userInfo: extractedUserInfo } = await canConnectToBackend(setting.khojUrl, setting.khojApiKey, false);
|
||||
@@ -332,19 +334,26 @@ export async function populateHeaderPane(headerEl: Element, setting: KhojSetting
|
||||
}
|
||||
|
||||
// Add Khoj title to header element
|
||||
const titleEl = headerEl.createDiv();
|
||||
const titlePaneEl = headerEl.createDiv();
|
||||
titlePaneEl.className = 'khoj-header-title-pane';
|
||||
const titleEl = titlePaneEl.createDiv();
|
||||
titleEl.className = 'khoj-logo';
|
||||
titleEl.textContent = "KHOJ"
|
||||
titleEl.textContent = "Khoj";
|
||||
|
||||
// Populate the header element with the navigation pane
|
||||
// Create the nav element
|
||||
const nav = headerEl.createEl('nav');
|
||||
const nav = titlePaneEl.createEl('nav');
|
||||
nav.className = 'khoj-nav';
|
||||
|
||||
// Create the title pane element
|
||||
titlePaneEl.appendChild(titleEl);
|
||||
titlePaneEl.appendChild(nav);
|
||||
|
||||
// Create the chat link
|
||||
const chatLink = nav.createEl('a');
|
||||
chatLink.id = 'chat-nav';
|
||||
chatLink.className = 'khoj-nav chat-nav';
|
||||
chatLink.dataset.view = KhojView.CHAT;
|
||||
|
||||
// Create the chat icon
|
||||
const chatIcon = chatLink.createEl('span');
|
||||
@@ -368,6 +377,7 @@ export async function populateHeaderPane(headerEl: Element, setting: KhojSetting
|
||||
// Create the search icon
|
||||
const searchIcon = searchLink.createEl('span');
|
||||
searchIcon.className = 'khoj-nav-icon khoj-nav-icon-search';
|
||||
setIcon(searchIcon, 'khoj-search');
|
||||
|
||||
// Create the search text
|
||||
const searchText = searchLink.createEl('span');
|
||||
@@ -378,38 +388,142 @@ export async function populateHeaderPane(headerEl: Element, setting: KhojSetting
|
||||
searchLink.appendChild(searchIcon);
|
||||
searchLink.appendChild(searchText);
|
||||
|
||||
// Create the search link
|
||||
// Create the similar link
|
||||
const similarLink = nav.createEl('a');
|
||||
similarLink.id = 'similar-nav';
|
||||
similarLink.className = 'khoj-nav similar-nav';
|
||||
similarLink.dataset.view = KhojView.SIMILAR;
|
||||
|
||||
// Create the search icon
|
||||
const similarIcon = searchLink.createEl('span');
|
||||
// Create the similar icon
|
||||
const similarIcon = similarLink.createEl('span');
|
||||
similarIcon.id = 'similar-nav-icon';
|
||||
similarIcon.className = 'khoj-nav-icon khoj-nav-icon-similar';
|
||||
setIcon(similarIcon, 'webhook');
|
||||
|
||||
// Create the search text
|
||||
const similarText = searchLink.createEl('span');
|
||||
// Create the similar text
|
||||
const similarText = similarLink.createEl('span');
|
||||
similarText.className = 'khoj-nav-item-text';
|
||||
similarText.textContent = 'Similar';
|
||||
|
||||
// Append the search icon and text to the search link
|
||||
// Append the similar icon and text to the similar link
|
||||
similarLink.appendChild(similarIcon);
|
||||
similarLink.appendChild(similarText);
|
||||
|
||||
// Helper to get the current Khoj leaf if active
|
||||
const getCurrentKhojLeaf = (): WorkspaceLeaf | undefined => {
|
||||
const activeLeaf = this.app.workspace.activeLeaf;
|
||||
if (activeLeaf && activeLeaf.view &&
|
||||
(activeLeaf.view.getViewType() === KhojView.CHAT || activeLeaf.view.getViewType() === KhojView.SIMILAR)) {
|
||||
return activeLeaf;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Add event listeners to the navigation links
|
||||
// Chat link event listener
|
||||
chatLink.addEventListener('click', () => {
|
||||
// Get the activateView method from the plugin instance
|
||||
const khojPlugin = this.app.plugins.plugins.khoj;
|
||||
khojPlugin?.activateView(KhojView.CHAT, getCurrentKhojLeaf());
|
||||
});
|
||||
|
||||
// Search link event listener
|
||||
searchLink.addEventListener('click', () => {
|
||||
// Open the search modal
|
||||
new KhojSearchModal(this.app, setting).open();
|
||||
});
|
||||
|
||||
// Similar link event listener
|
||||
similarLink.addEventListener('click', () => {
|
||||
// Get the activateView method from the plugin instance
|
||||
const khojPlugin = this.app.plugins.plugins.khoj;
|
||||
khojPlugin?.activateView(KhojView.SIMILAR, getCurrentKhojLeaf());
|
||||
});
|
||||
|
||||
// Append the nav items to the nav element
|
||||
nav.appendChild(chatLink);
|
||||
nav.appendChild(searchLink);
|
||||
nav.appendChild(similarLink);
|
||||
|
||||
// Append the title, nav items to the header element
|
||||
headerEl.appendChild(titleEl);
|
||||
headerEl.appendChild(nav);
|
||||
// Append the title and new chat container to the header element
|
||||
headerEl.appendChild(titlePaneEl);
|
||||
|
||||
if (viewType === KhojView.CHAT) {
|
||||
// Create subtitle pane for New Chat button and agent selector
|
||||
const newChatEl = headerEl.createDiv("khoj-header-right-container");
|
||||
|
||||
// Add agent selector container
|
||||
const agentContainer = newChatEl.createDiv("khoj-header-agent-container");
|
||||
|
||||
// Add agent selector
|
||||
agentContainer.createEl("select", {
|
||||
attr: {
|
||||
class: "khoj-header-agent-select",
|
||||
id: "khoj-header-agent-select"
|
||||
}
|
||||
});
|
||||
|
||||
// Add New Chat button
|
||||
const newChatButton = newChatEl.createEl('button');
|
||||
newChatButton.className = 'khoj-header-new-chat-button';
|
||||
newChatButton.title = 'Start New Chat (Ctrl+Alt+N)';
|
||||
setIcon(newChatButton, 'plus-circle');
|
||||
newChatButton.textContent = 'New Chat';
|
||||
|
||||
// Add event listener to the New Chat button
|
||||
newChatButton.addEventListener('click', () => {
|
||||
const khojPlugin = this.app.plugins.plugins.khoj;
|
||||
if (khojPlugin) {
|
||||
// First activate the chat view
|
||||
khojPlugin.activateView(KhojView.CHAT).then(() => {
|
||||
// Then create a new conversation
|
||||
setTimeout(() => {
|
||||
// Access the chat view directly from the leaf after activation
|
||||
const leaves = this.app.workspace.getLeavesOfType(KhojView.CHAT);
|
||||
if (leaves.length > 0) {
|
||||
const chatView = leaves[0].view;
|
||||
if (chatView && typeof chatView.createNewConversation === 'function') {
|
||||
chatView.createNewConversation();
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Append the new chat container to the header element
|
||||
headerEl.appendChild(newChatEl);
|
||||
}
|
||||
|
||||
// Update active state based on current view
|
||||
const updateActiveState = () => {
|
||||
const activeLeaf = this.app.workspace.activeLeaf;
|
||||
if (!activeLeaf) return;
|
||||
|
||||
const viewType = activeLeaf.view?.getViewType();
|
||||
|
||||
// Remove active class from all links
|
||||
chatLink.classList.remove('khoj-nav-selected');
|
||||
similarLink.classList.remove('khoj-nav-selected');
|
||||
|
||||
// Add active class to the current view link
|
||||
if (viewType === KhojView.CHAT) {
|
||||
chatLink.classList.add('khoj-nav-selected');
|
||||
} else if (viewType === KhojView.SIMILAR) {
|
||||
similarLink.classList.add('khoj-nav-selected');
|
||||
}
|
||||
};
|
||||
|
||||
// Initial update
|
||||
updateActiveState();
|
||||
|
||||
// Register event for workspace changes
|
||||
this.app.workspace.on('active-leaf-change', updateActiveState);
|
||||
}
|
||||
|
||||
export enum KhojView {
|
||||
CHAT = "khoj-chat-view",
|
||||
SIMILAR = "khoj-similar-view",
|
||||
}
|
||||
|
||||
function copyParentText(event: MouseEvent, message: string, originalButton: string) {
|
||||
@@ -425,7 +539,7 @@ function copyParentText(event: MouseEvent, message: string, originalButton: stri
|
||||
}).catch((error) => {
|
||||
console.error("Error copying text to clipboard:", error);
|
||||
const originalButtonText = button.innerHTML;
|
||||
button.innerHTML = "⛔️";
|
||||
setIcon((button as HTMLElement), 'x-circle');
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalButtonText;
|
||||
setIcon((button as HTMLElement), originalButton);
|
||||
@@ -437,7 +551,13 @@ function copyParentText(event: MouseEvent, message: string, originalButton: stri
|
||||
|
||||
export function createCopyParentText(message: string, originalButton: string = 'copy-plus') {
|
||||
return function (event: MouseEvent) {
|
||||
return copyParentText(event, message, originalButton);
|
||||
let markdownMessage = copyParentText(event, message, originalButton);
|
||||
// Convert edit blocks back to markdown format before pasting
|
||||
const editRegex = /<details class="khoj-edit-accordion">[\s\S]*?<pre><code class="language-khoj-edit">([\s\S]*?)<\/code><\/pre>[\s\S]*?<\/details>/g;
|
||||
markdownMessage = markdownMessage?.replace(editRegex, (_, content) => {
|
||||
return `<khoj-edit>\n${content}\n</khoj-edit>`;
|
||||
});
|
||||
return markdownMessage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,3 +605,76 @@ export function getLinkToEntry(sourceFiles: TFile[], chosenFile: string, chosenE
|
||||
return linkToEntry;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchChatModels(settings: KhojSetting): Promise<ModelOption[]> {
|
||||
if (!settings.connectedToBackend || !settings.khojUrl) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${settings.khojUrl}/api/model/chat/options`, {
|
||||
method: 'GET',
|
||||
headers: settings.khojApiKey ? { 'Authorization': `Bearer ${settings.khojApiKey}` } : {},
|
||||
});
|
||||
if (response.ok) {
|
||||
const modelsData = await response.json();
|
||||
if (Array.isArray(modelsData)) {
|
||||
return modelsData.map((model: any) => ({
|
||||
id: model.id.toString(),
|
||||
name: model.name,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
console.warn("Khoj: Failed to fetch chat models:", response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Khoj: Error fetching chat models:", error);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function fetchUserServerSettings(settings: KhojSetting): Promise<ServerUserConfig | null> {
|
||||
if (!settings.connectedToBackend || !settings.khojUrl) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${settings.khojUrl}/api/settings?detailed=true`, {
|
||||
method: 'GET',
|
||||
headers: settings.khojApiKey ? { 'Authorization': `Bearer ${settings.khojApiKey}` } : {},
|
||||
});
|
||||
if (response.ok) {
|
||||
return await response.json() as ServerUserConfig;
|
||||
} else {
|
||||
console.warn("Khoj: Failed to fetch user server settings:", response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Khoj: Error fetching user server settings:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function updateServerChatModel(modelId: string, settings: KhojSetting): Promise<boolean> {
|
||||
if (!settings.connectedToBackend || !settings.khojUrl) {
|
||||
new Notice("️⛔️ Connect to Khoj to update chat model.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${settings.khojUrl}/api/model/chat?id=${modelId}`, {
|
||||
method: 'POST', // As per web app's updateModel function
|
||||
headers: settings.khojApiKey ? { 'Authorization': `Bearer ${settings.khojApiKey}` } : {},
|
||||
});
|
||||
if (response.ok) {
|
||||
settings.selectedChatModelId = modelId; // Update local mirror
|
||||
return true;
|
||||
} else {
|
||||
const errorData = await response.text();
|
||||
new Notice(`️⛔️ Failed to update chat model on server: ${response.status} ${errorData}`);
|
||||
console.error("Khoj: Failed to update chat model:", response.status, errorData);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
new Notice("️⛔️ Error updating chat model on server. See console.");
|
||||
console.error("Khoj: Error updating chat model:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,18 +4,19 @@
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"target": "ES2018",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
"ES7",
|
||||
"ES2018"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -2,17 +2,56 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@eslint-community/eslint-utils@^4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
||||
integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
|
||||
"@asamuzakjp/css-color@^3.1.2":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz#cc42f5b85c593f79f1fa4f25d2b9b321e61d1794"
|
||||
integrity sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
"@csstools/css-calc" "^2.1.3"
|
||||
"@csstools/css-color-parser" "^3.0.9"
|
||||
"@csstools/css-parser-algorithms" "^3.0.4"
|
||||
"@csstools/css-tokenizer" "^3.0.3"
|
||||
lru-cache "^10.4.3"
|
||||
|
||||
"@csstools/color-helpers@^5.0.2":
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.0.2.tgz#82592c9a7c2b83c293d9161894e2a6471feb97b8"
|
||||
integrity sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==
|
||||
|
||||
"@csstools/css-calc@^2.1.3":
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.3.tgz#6f68affcb569a86b91965e8622d644be35a08423"
|
||||
integrity sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==
|
||||
|
||||
"@csstools/css-color-parser@^3.0.9":
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.0.9.tgz#8d81b77d6f211495b5100ec4cad4c8828de49f6b"
|
||||
integrity sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==
|
||||
dependencies:
|
||||
"@csstools/color-helpers" "^5.0.2"
|
||||
"@csstools/css-calc" "^2.1.3"
|
||||
|
||||
"@csstools/css-parser-algorithms@^3.0.4":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz#74426e93bd1c4dcab3e441f5cc7ba4fb35d94356"
|
||||
integrity sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==
|
||||
|
||||
"@csstools/css-tokenizer@^3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz#a5502c8539265fecbd873c1e395a890339f119c2"
|
||||
integrity sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.4.0":
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
|
||||
integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@eslint-community/regexpp@^4.10.0":
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae"
|
||||
integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==
|
||||
version "4.12.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
|
||||
integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
@@ -42,22 +81,22 @@
|
||||
dependencies:
|
||||
"@types/tern" "*"
|
||||
|
||||
"@types/dompurify@^3.0.5":
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7"
|
||||
integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==
|
||||
"@types/dompurify@3.2.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.2.0.tgz#56610bf3e4250df57744d61fbd95422e07dfb840"
|
||||
integrity sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==
|
||||
dependencies:
|
||||
"@types/trusted-types" "*"
|
||||
dompurify "*"
|
||||
|
||||
"@types/estree@*":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
|
||||
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
|
||||
|
||||
"@types/node@^16.11.6":
|
||||
version "16.18.108"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.108.tgz#b794e2b2a85b4c12935ea7d0f18641be68b352f9"
|
||||
integrity sha512-fj42LD82fSv6yN9C6Q4dzS+hujHj+pTv0IpRR3kI20fnYeS0ytBpjFO9OjmDowSPPt4lNKN46JLaKbCyP+BW2A==
|
||||
version "16.18.126"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.126.tgz#27875faa2926c0f475b39a8bb1e546c0176f8d4b"
|
||||
integrity sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==
|
||||
|
||||
"@types/tern@*":
|
||||
version "0.23.9"
|
||||
@@ -66,7 +105,7 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@types/trusted-types@*":
|
||||
"@types/trusted-types@^2.0.7":
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
||||
@@ -152,6 +191,11 @@
|
||||
"@typescript-eslint/types" "7.13.1"
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
agent-base@^7.1.0, agent-base@^7.1.2:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1"
|
||||
integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==
|
||||
|
||||
array-union@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
||||
@@ -181,12 +225,38 @@ builtin-modules@3.3.0:
|
||||
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
|
||||
integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
|
||||
|
||||
debug@^4.3.4:
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b"
|
||||
integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==
|
||||
cssstyle@^4.2.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.3.1.tgz#68a3c9f5a70aa97d5a6ebecc9805e511fc022eb8"
|
||||
integrity sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
"@asamuzakjp/css-color" "^3.1.2"
|
||||
rrweb-cssom "^0.8.0"
|
||||
|
||||
data-urls@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde"
|
||||
integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==
|
||||
dependencies:
|
||||
whatwg-mimetype "^4.0.0"
|
||||
whatwg-url "^14.0.0"
|
||||
|
||||
debug@4, debug@^4.3.4:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
|
||||
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
decimal.js@^10.5.0:
|
||||
version "10.5.0"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.5.0.tgz#0f371c7cf6c4898ce0afb09836db73cd82010f22"
|
||||
integrity sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==
|
||||
|
||||
diff@^8.0.2:
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-8.0.2.tgz#712156a6dd288e66ebb986864e190c2fc9eddfae"
|
||||
integrity sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==
|
||||
|
||||
dir-glob@^3.0.1:
|
||||
version "3.0.1"
|
||||
@@ -195,10 +265,17 @@ dir-glob@^3.0.1:
|
||||
dependencies:
|
||||
path-type "^4.0.0"
|
||||
|
||||
dompurify@^3.1.4:
|
||||
version "3.1.6"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.6.tgz#43c714a94c6a7b8801850f82e756685300a027e2"
|
||||
integrity sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==
|
||||
dompurify@*, dompurify@^3.2.6:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.6.tgz#ca040a6ad2b88e2a92dc45f38c79f84a714a1cad"
|
||||
integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==
|
||||
optionalDependencies:
|
||||
"@types/trusted-types" "^2.0.7"
|
||||
|
||||
entities@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.0.tgz#09c9e29cb79b0a6459a9b9db9efb418ac5bb8e51"
|
||||
integrity sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==
|
||||
|
||||
esbuild-android-64@0.14.47:
|
||||
version "0.14.47"
|
||||
@@ -326,26 +403,26 @@ esbuild@0.14.47:
|
||||
esbuild-windows-64 "0.14.47"
|
||||
esbuild-windows-arm64 "0.14.47"
|
||||
|
||||
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3:
|
||||
eslint-visitor-keys@^3.4.3:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
|
||||
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
|
||||
|
||||
fast-glob@^3.2.9:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
|
||||
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
|
||||
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "^2.0.2"
|
||||
"@nodelib/fs.walk" "^1.2.3"
|
||||
glob-parent "^5.1.2"
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.4"
|
||||
micromatch "^4.0.8"
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
|
||||
integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==
|
||||
version "1.19.1"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5"
|
||||
integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
|
||||
dependencies:
|
||||
reusify "^1.0.4"
|
||||
|
||||
@@ -380,6 +457,36 @@ graphemer@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
||||
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
|
||||
|
||||
html-encoding-sniffer@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448"
|
||||
integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==
|
||||
dependencies:
|
||||
whatwg-encoding "^3.1.1"
|
||||
|
||||
http-proxy-agent@^7.0.2:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
|
||||
integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
|
||||
dependencies:
|
||||
agent-base "^7.1.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
https-proxy-agent@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
|
||||
integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==
|
||||
dependencies:
|
||||
agent-base "^7.1.2"
|
||||
debug "4"
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
|
||||
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
ignore@^5.2.0, ignore@^5.3.1:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||
@@ -402,12 +509,56 @@ is-number@^7.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||
|
||||
is-potential-custom-element-name@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
|
||||
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
|
||||
|
||||
isomorphic-dompurify@^2.25.0:
|
||||
version "2.25.0"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-dompurify/-/isomorphic-dompurify-2.25.0.tgz#063e3ea7399bc1146783a9527be6c10baa25dc15"
|
||||
integrity sha512-bcpJzu9DOjN21qaCVpcoCwUX1ytpvA6EFqCK5RNtPg5+F0Jz9PX50jl6jbEicBNeO87eDDfC7XtPs4zjDClZJg==
|
||||
dependencies:
|
||||
dompurify "^3.2.6"
|
||||
jsdom "^26.1.0"
|
||||
|
||||
jsdom@^26.1.0:
|
||||
version "26.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-26.1.0.tgz#ab5f1c1cafc04bd878725490974ea5e8bf0c72b3"
|
||||
integrity sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==
|
||||
dependencies:
|
||||
cssstyle "^4.2.1"
|
||||
data-urls "^5.0.0"
|
||||
decimal.js "^10.5.0"
|
||||
html-encoding-sniffer "^4.0.0"
|
||||
http-proxy-agent "^7.0.2"
|
||||
https-proxy-agent "^7.0.6"
|
||||
is-potential-custom-element-name "^1.0.1"
|
||||
nwsapi "^2.2.16"
|
||||
parse5 "^7.2.1"
|
||||
rrweb-cssom "^0.8.0"
|
||||
saxes "^6.0.0"
|
||||
symbol-tree "^3.2.4"
|
||||
tough-cookie "^5.1.1"
|
||||
w3c-xmlserializer "^5.0.0"
|
||||
webidl-conversions "^7.0.0"
|
||||
whatwg-encoding "^3.1.1"
|
||||
whatwg-mimetype "^4.0.0"
|
||||
whatwg-url "^14.1.1"
|
||||
ws "^8.18.0"
|
||||
xml-name-validator "^5.0.0"
|
||||
|
||||
lru-cache@^10.4.3:
|
||||
version "10.4.3"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
|
||||
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
|
||||
|
||||
merge2@^1.3.0, merge2@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||
|
||||
micromatch@^4.0.4:
|
||||
micromatch@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
|
||||
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
|
||||
@@ -427,24 +578,36 @@ moment@2.29.4:
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
ms@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
nwsapi@^2.2.16:
|
||||
version "2.2.20"
|
||||
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.20.tgz#22e53253c61e7b0e7e93cef42c891154bcca11ef"
|
||||
integrity sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==
|
||||
|
||||
obsidian@^1.6.6:
|
||||
version "1.6.6"
|
||||
resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-1.6.6.tgz#d45c4021c291765e1b77ed4a1c645e562ff6e77f"
|
||||
integrity sha512-GZHzeOiwmw/wBjB5JwrsxAZBLqxGQmqtEKSvJJvT0LtTcqeOFnV8jv0ZK5kO7hBb44WxJc+LdS7mZgLXbb+qXQ==
|
||||
version "1.8.7"
|
||||
resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-1.8.7.tgz#601e9ea1724289effa4c9bb3b4e20d327263634f"
|
||||
integrity sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==
|
||||
dependencies:
|
||||
"@types/codemirror" "5.60.8"
|
||||
moment "2.29.4"
|
||||
|
||||
parse5@^7.2.1:
|
||||
version "7.3.0"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05"
|
||||
integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==
|
||||
dependencies:
|
||||
entities "^6.0.0"
|
||||
|
||||
path-type@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||
@@ -455,15 +618,25 @@ picomatch@^2.3.1:
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
punycode@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||
|
||||
queue-microtask@^1.2.2:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
reusify@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f"
|
||||
integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
|
||||
|
||||
rrweb-cssom@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz#3021d1b4352fbf3b614aaeed0bc0d5739abe0bc2"
|
||||
integrity sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
@@ -472,16 +645,45 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3.0.0":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
saxes@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
|
||||
integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
|
||||
dependencies:
|
||||
xmlchars "^2.2.0"
|
||||
|
||||
semver@^7.6.0:
|
||||
version "7.6.3"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
|
||||
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
|
||||
version "7.7.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
|
||||
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
||||
|
||||
slash@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
||||
|
||||
symbol-tree@^3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
||||
|
||||
tldts-core@^6.1.86:
|
||||
version "6.1.86"
|
||||
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.86.tgz#a93e6ed9d505cb54c542ce43feb14c73913265d8"
|
||||
integrity sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==
|
||||
|
||||
tldts@^6.1.32:
|
||||
version "6.1.86"
|
||||
resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.86.tgz#087e0555b31b9725ee48ca7e77edc56115cd82f7"
|
||||
integrity sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==
|
||||
dependencies:
|
||||
tldts-core "^6.1.86"
|
||||
|
||||
to-regex-range@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
||||
@@ -489,10 +691,24 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
tough-cookie@^5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.1.2.tgz#66d774b4a1d9e12dc75089725af3ac75ec31bed7"
|
||||
integrity sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==
|
||||
dependencies:
|
||||
tldts "^6.1.32"
|
||||
|
||||
tr46@^5.1.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.1.1.tgz#96ae867cddb8fdb64a49cc3059a8d428bcf238ca"
|
||||
integrity sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==
|
||||
dependencies:
|
||||
punycode "^2.3.1"
|
||||
|
||||
ts-api-utils@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1"
|
||||
integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064"
|
||||
integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==
|
||||
|
||||
tslib@2.4.0:
|
||||
version "2.4.0"
|
||||
@@ -503,3 +719,50 @@ typescript@4.7.4:
|
||||
version "4.7.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
|
||||
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
||||
|
||||
w3c-xmlserializer@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c"
|
||||
integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==
|
||||
dependencies:
|
||||
xml-name-validator "^5.0.0"
|
||||
|
||||
webidl-conversions@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
|
||||
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
|
||||
|
||||
whatwg-encoding@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5"
|
||||
integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==
|
||||
dependencies:
|
||||
iconv-lite "0.6.3"
|
||||
|
||||
whatwg-mimetype@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a"
|
||||
integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==
|
||||
|
||||
whatwg-url@^14.0.0, whatwg-url@^14.1.1:
|
||||
version "14.2.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.2.0.tgz#4ee02d5d725155dae004f6ae95c73e7ef5d95663"
|
||||
integrity sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==
|
||||
dependencies:
|
||||
tr46 "^5.1.0"
|
||||
webidl-conversions "^7.0.0"
|
||||
|
||||
ws@^8.18.0:
|
||||
version "8.18.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a"
|
||||
integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==
|
||||
|
||||
xml-name-validator@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673"
|
||||
integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==
|
||||
|
||||
xmlchars@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||
|
||||
Reference in New Issue
Block a user