Add in-app debug log panel and rich vault search
- Add listener/history API to Logger for live log streaming - Add toggle button and styled log panel to chat view - Enhance ToolExecutor to use VaultIndexer for rich search results - Instruct all agent modes to emit tool calls immediately instead of describing intent
This commit is contained in:
@@ -3124,10 +3124,7 @@ var ALL_AGENT_MODES = ["ask", "edit", "organize", "research", "workflow"];
|
|||||||
function filterToolsByName(tools, allowed) {
|
function filterToolsByName(tools, allowed) {
|
||||||
return tools.filter((t) => allowed.has(t.function.name));
|
return tools.filter((t) => allowed.has(t.function.name));
|
||||||
}
|
}
|
||||||
var READ_TOOLS = /* @__PURE__ */ new Set([
|
var READ_TOOLS = /* @__PURE__ */ new Set(["read_vault_file", "search_vault_files"]);
|
||||||
"read_vault_file",
|
|
||||||
"search_vault_files"
|
|
||||||
]);
|
|
||||||
var ORGANIZE_TOOLS = /* @__PURE__ */ new Set([
|
var ORGANIZE_TOOLS = /* @__PURE__ */ new Set([
|
||||||
"read_vault_file",
|
"read_vault_file",
|
||||||
"search_vault_files",
|
"search_vault_files",
|
||||||
@@ -3148,16 +3145,15 @@ var EDIT_TOOLS = /* @__PURE__ */ new Set([
|
|||||||
"delete_note",
|
"delete_note",
|
||||||
"insert_link"
|
"insert_link"
|
||||||
]);
|
]);
|
||||||
var RESEARCH_TOOLS = /* @__PURE__ */ new Set([
|
var RESEARCH_TOOLS = /* @__PURE__ */ new Set(["read_vault_file", "search_vault_files"]);
|
||||||
"read_vault_file",
|
|
||||||
"search_vault_files"
|
|
||||||
]);
|
|
||||||
var AGENT_MODE_CONFIGS = {
|
var AGENT_MODE_CONFIGS = {
|
||||||
ask: {
|
ask: {
|
||||||
label: "Ask",
|
label: "Ask",
|
||||||
description: "Answer questions using vault context. Read-only mode.",
|
description: "Answer questions using vault context. Read-only mode.",
|
||||||
systemPrompt: `You are a helpful assistant that answers questions using the contents of the user's Obsidian vault.
|
systemPrompt: `You are a helpful assistant that answers questions using the contents of the user's Obsidian vault.
|
||||||
You have access to search and read tools to find relevant information.
|
You have access to search and read tools to find relevant information.
|
||||||
|
IMPORTANT: When you need vault information, do NOT say you will search or read files. You MUST immediately emit a tool_call to search_vault_files or read_vault_file.
|
||||||
|
Only respond to the user after you have received and analyzed the tool results.
|
||||||
Always base your answers on vault content when possible.
|
Always base your answers on vault content when possible.
|
||||||
If you cannot find relevant information, say so clearly.
|
If you cannot find relevant information, say so clearly.
|
||||||
Do not make up facts.`,
|
Do not make up facts.`,
|
||||||
@@ -3170,6 +3166,8 @@ Do not make up facts.`,
|
|||||||
description: "Create, modify, and organize notes with full editing tools.",
|
description: "Create, modify, and organize notes with full editing tools.",
|
||||||
systemPrompt: `You are an assistant that helps edit and manage notes in the user's Obsidian vault.
|
systemPrompt: `You are an assistant that helps edit and manage notes in the user's Obsidian vault.
|
||||||
You have full access to reading, searching, creating, appending, renaming, moving, and deleting notes.
|
You have full access to reading, searching, creating, appending, renaming, moving, and deleting notes.
|
||||||
|
IMPORTANT: When you need vault information, do NOT say you will search or read files. You MUST immediately emit a tool_call to the appropriate tool.
|
||||||
|
Only respond to the user after you have received and analyzed the tool results.
|
||||||
When editing notes:
|
When editing notes:
|
||||||
- Prefer modifying existing content over creating duplicates.
|
- Prefer modifying existing content over creating duplicates.
|
||||||
- Use the replace_note_section tool to update specific sections.
|
- Use the replace_note_section tool to update specific sections.
|
||||||
@@ -3185,6 +3183,8 @@ When editing notes:
|
|||||||
description: "Tag, rename, move, and link notes to keep the vault tidy.",
|
description: "Tag, rename, move, and link notes to keep the vault tidy.",
|
||||||
systemPrompt: `You are an assistant that helps organize the user's Obsidian vault.
|
systemPrompt: `You are an assistant that helps organize the user's Obsidian vault.
|
||||||
You can search notes, read them, update frontmatter tags, rename files, move files to folders, and insert wiki-links.
|
You can search notes, read them, update frontmatter tags, rename files, move files to folders, and insert wiki-links.
|
||||||
|
IMPORTANT: When you need vault information, do NOT say you will search or read files. You MUST immediately emit a tool_call to the appropriate tool.
|
||||||
|
Only respond to the user after you have received and analyzed the tool results.
|
||||||
When organizing:
|
When organizing:
|
||||||
- Suggest consistent tag vocabularies.
|
- Suggest consistent tag vocabularies.
|
||||||
- Group related notes by linking them.
|
- Group related notes by linking them.
|
||||||
@@ -3199,6 +3199,8 @@ When organizing:
|
|||||||
description: "Deep vault search and synthesis across multiple notes.",
|
description: "Deep vault search and synthesis across multiple notes.",
|
||||||
systemPrompt: `You are a research assistant that dives deep into the user's Obsidian vault.
|
systemPrompt: `You are a research assistant that dives deep into the user's Obsidian vault.
|
||||||
Your job is to synthesize information across multiple notes, find connections, and produce comprehensive summaries.
|
Your job is to synthesize information across multiple notes, find connections, and produce comprehensive summaries.
|
||||||
|
IMPORTANT: When you need vault information, do NOT say you will search or read files. You MUST immediately emit a tool_call to search_vault_files or read_vault_file.
|
||||||
|
Only respond to the user after you have received and analyzed the tool results.
|
||||||
Search broadly, read key sources, and cross-reference information.
|
Search broadly, read key sources, and cross-reference information.
|
||||||
Cite specific notes and quotes where possible.
|
Cite specific notes and quotes where possible.
|
||||||
If information is incomplete or contradictory, note it explicitly.`,
|
If information is incomplete or contradictory, note it explicitly.`,
|
||||||
@@ -3251,28 +3253,69 @@ var _Logger = class _Logger {
|
|||||||
_Logger.minLevel = level;
|
_Logger.minLevel = level;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
static addListener(callback) {
|
||||||
|
_Logger.listeners.push(callback);
|
||||||
|
return () => {
|
||||||
|
const idx = _Logger.listeners.indexOf(callback);
|
||||||
|
if (idx !== -1) {
|
||||||
|
_Logger.listeners.splice(idx, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
static getHistory() {
|
||||||
|
return [..._Logger.history];
|
||||||
|
}
|
||||||
|
static record(level, levelLabel, message, category) {
|
||||||
|
if (level < _Logger.minLevel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entry = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
level,
|
||||||
|
levelLabel,
|
||||||
|
category,
|
||||||
|
message
|
||||||
|
};
|
||||||
|
_Logger.history.push(entry);
|
||||||
|
if (_Logger.history.length > _Logger.maxHistory) {
|
||||||
|
_Logger.history = _Logger.history.slice(-_Logger.maxHistory);
|
||||||
|
}
|
||||||
|
for (const listener of _Logger.listeners) {
|
||||||
|
try {
|
||||||
|
listener(entry);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
static debug(message, category = "general") {
|
static debug(message, category = "general") {
|
||||||
if (0 /* DEBUG */ >= _Logger.minLevel) {
|
if (0 /* DEBUG */ >= _Logger.minLevel) {
|
||||||
console.debug(`[${category}] DEBUG: ${message}`);
|
console.debug(`[${category}] DEBUG: ${message}`);
|
||||||
}
|
}
|
||||||
|
_Logger.record(0 /* DEBUG */, "DEBUG", message, category);
|
||||||
}
|
}
|
||||||
static info(message, category = "general") {
|
static info(message, category = "general") {
|
||||||
if (1 /* INFO */ >= _Logger.minLevel) {
|
if (1 /* INFO */ >= _Logger.minLevel) {
|
||||||
console.info(`[${category}] INFO: ${message}`);
|
console.info(`[${category}] INFO: ${message}`);
|
||||||
}
|
}
|
||||||
|
_Logger.record(1 /* INFO */, "INFO", message, category);
|
||||||
}
|
}
|
||||||
static warn(message, category = "general") {
|
static warn(message, category = "general") {
|
||||||
if (2 /* WARN */ >= _Logger.minLevel) {
|
if (2 /* WARN */ >= _Logger.minLevel) {
|
||||||
console.warn(`[${category}] WARN: ${message}`);
|
console.warn(`[${category}] WARN: ${message}`);
|
||||||
}
|
}
|
||||||
|
_Logger.record(2 /* WARN */, "WARN", message, category);
|
||||||
}
|
}
|
||||||
static error(message, category = "general") {
|
static error(message, category = "general") {
|
||||||
if (3 /* ERROR */ >= _Logger.minLevel) {
|
if (3 /* ERROR */ >= _Logger.minLevel) {
|
||||||
console.error(`[${category}] ERROR: ${message}`);
|
console.error(`[${category}] ERROR: ${message}`);
|
||||||
}
|
}
|
||||||
|
_Logger.record(3 /* ERROR */, "ERROR", message, category);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
_Logger.minLevel = 0 /* DEBUG */;
|
_Logger.minLevel = 0 /* DEBUG */;
|
||||||
|
_Logger.listeners = [];
|
||||||
|
_Logger.history = [];
|
||||||
|
_Logger.maxHistory = 500;
|
||||||
var Logger = _Logger;
|
var Logger = _Logger;
|
||||||
var MAX_JSON_SIZE = 1e6;
|
var MAX_JSON_SIZE = 1e6;
|
||||||
var MAX_JSON_NESTING = 24;
|
var MAX_JSON_NESTING = 24;
|
||||||
@@ -8772,10 +8815,11 @@ var INVALID_PATH_CHARS = /[<>:"|?*~]/;
|
|||||||
var MAX_PATH_LENGTH = 200;
|
var MAX_PATH_LENGTH = 200;
|
||||||
var FORBIDDEN_DIRS = [".obsidian", ".git"];
|
var FORBIDDEN_DIRS = [".obsidian", ".git"];
|
||||||
var ToolExecutor = class {
|
var ToolExecutor = class {
|
||||||
constructor(vault, app, telemetryManager) {
|
constructor(vault, app, telemetryManager, vaultIndexer) {
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
this.app = app;
|
this.app = app;
|
||||||
this.telemetryManager = telemetryManager;
|
this.telemetryManager = telemetryManager;
|
||||||
|
this.vaultIndexer = vaultIndexer;
|
||||||
}
|
}
|
||||||
isSafePath(path) {
|
isSafePath(path) {
|
||||||
if (!path || path.trim().length === 0) {
|
if (!path || path.trim().length === 0) {
|
||||||
@@ -8856,7 +8900,7 @@ var ToolExecutor = class {
|
|||||||
result = await this.handleReadVaultFile(parsedArgs);
|
result = await this.handleReadVaultFile(parsedArgs);
|
||||||
break;
|
break;
|
||||||
case "search_vault_files":
|
case "search_vault_files":
|
||||||
result = this.handleSearchVaultFiles(parsedArgs);
|
result = await this.handleSearchVaultFiles(parsedArgs);
|
||||||
break;
|
break;
|
||||||
case "append_to_note":
|
case "append_to_note":
|
||||||
result = await this.handleAppendToNote(parsedArgs);
|
result = await this.handleAppendToNote(parsedArgs);
|
||||||
@@ -8945,13 +8989,28 @@ var ToolExecutor = class {
|
|||||||
data: { path, content }
|
data: { path, content }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
handleSearchVaultFiles(args) {
|
async handleSearchVaultFiles(args) {
|
||||||
const query = args.query;
|
const query = args.query;
|
||||||
const limitArg = args.limit;
|
const limitArg = args.limit;
|
||||||
if (typeof query !== "string") {
|
if (typeof query !== "string") {
|
||||||
throw new Error("Query must be a string");
|
throw new Error("Query must be a string");
|
||||||
}
|
}
|
||||||
const limit = typeof limitArg === "number" && Number.isFinite(limitArg) ? limitArg : 10;
|
const limit = typeof limitArg === "number" && Number.isFinite(limitArg) ? limitArg : 10;
|
||||||
|
if (this.vaultIndexer) {
|
||||||
|
const results = await this.vaultIndexer.searchVault(query, limit);
|
||||||
|
const files2 = results.map((entry) => ({
|
||||||
|
path: entry.path,
|
||||||
|
basename: entry.path.split("/").pop() ?? entry.path,
|
||||||
|
title: entry.title,
|
||||||
|
score: entry.score,
|
||||||
|
tags: entry.tags
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Found ${files2.length} matching files`,
|
||||||
|
data: files2
|
||||||
|
};
|
||||||
|
}
|
||||||
const normalizedQuery = query.toLowerCase();
|
const normalizedQuery = query.toLowerCase();
|
||||||
const files = this.vault.getMarkdownFiles().filter((file) => file.path.toLowerCase().includes(normalizedQuery)).slice(0, limit).map((file) => ({ path: file.path, basename: file.basename }));
|
const files = this.vault.getMarkdownFiles().filter((file) => file.path.toLowerCase().includes(normalizedQuery)).slice(0, limit).map((file) => ({ path: file.path, basename: file.basename }));
|
||||||
return {
|
return {
|
||||||
@@ -9505,8 +9564,11 @@ ${lines.join("\n")}
|
|||||||
};
|
};
|
||||||
|
|
||||||
// src/conversation-state.ts
|
// src/conversation-state.ts
|
||||||
|
var DEFAULT_SYSTEM_PROMPT = `You are an assistant that can help answer questions using the contents of a vault.
|
||||||
|
When a user asks for information about their vault, you MUST call the search_vault_files or read_vault_file tool to find the answer.
|
||||||
|
Do not say you will search or read files \u2014 immediately emit the tool_call.`;
|
||||||
var ConversationStateManager = class {
|
var ConversationStateManager = class {
|
||||||
constructor() {
|
constructor(initialSystemPrompt) {
|
||||||
this.shortTermContext = [];
|
this.shortTermContext = [];
|
||||||
this.mediumTermContext = [];
|
this.mediumTermContext = [];
|
||||||
this.longTermContext = [];
|
this.longTermContext = [];
|
||||||
@@ -9515,10 +9577,7 @@ var ConversationStateManager = class {
|
|||||||
this.longTermContext = [
|
this.longTermContext = [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
content: `You are an assistant that can help answer questions using the contents of a vault.
|
content: initialSystemPrompt ?? DEFAULT_SYSTEM_PROMPT
|
||||||
The user can ask questions about their vault contents, and you should provide helpful responses based on the files.
|
|
||||||
When a user asks for information, try to find relevant files using the search_vault_files tool and read their contents with the read_vault_file tool.
|
|
||||||
Only use the tools if you need to access vault content that is not already in the context.`
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -9546,12 +9605,15 @@ var ConversationStateManager = class {
|
|||||||
* Sets the user's persona or core knowledge as long-term context
|
* Sets the user's persona or core knowledge as long-term context
|
||||||
* @param personaContent The persona or core knowledge content
|
* @param personaContent The persona or core knowledge content
|
||||||
*/
|
*/
|
||||||
|
setSystemPrompt(systemPrompt) {
|
||||||
|
this.longTermContext = this.longTermContext.filter((msg) => msg.role !== "system");
|
||||||
|
this.longTermContext.unshift({
|
||||||
|
role: "system",
|
||||||
|
content: systemPrompt
|
||||||
|
});
|
||||||
|
}
|
||||||
setPersona(personaContent) {
|
setPersona(personaContent) {
|
||||||
this.longTermContext = this.longTermContext.filter(
|
this.longTermContext = this.longTermContext.filter((msg) => msg.role !== "system");
|
||||||
(msg) => msg.role !== "system" || !msg.content.includes(
|
|
||||||
"You are an assistant that can help answer questions using the contents of a vault"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this.longTermContext.push({
|
this.longTermContext.push({
|
||||||
role: "system",
|
role: "system",
|
||||||
content: personaContent
|
content: personaContent
|
||||||
@@ -9589,16 +9651,13 @@ var ConversationStateManager = class {
|
|||||||
/**
|
/**
|
||||||
* Clears all conversation context
|
* Clears all conversation context
|
||||||
*/
|
*/
|
||||||
clear() {
|
clear(systemPrompt) {
|
||||||
this.shortTermContext = [];
|
this.shortTermContext = [];
|
||||||
this.mediumTermContext = [];
|
this.mediumTermContext = [];
|
||||||
this.longTermContext = [
|
this.longTermContext = [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
content: `You are an assistant that can help answer questions using the contents of a vault.
|
content: systemPrompt ?? DEFAULT_SYSTEM_PROMPT
|
||||||
The user can ask questions about their vault contents, and you should provide helpful responses based on the files.
|
|
||||||
When a user asks for information, try to find relevant files using the search_vault_files tool and read their contents with the read_vault_file tool.
|
|
||||||
Only use the tools if you need to access vault content that is not already in the context.`
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -9641,7 +9700,7 @@ var VARIABLE_PATTERN = /\{\{([\w.]+)\}\}/g;
|
|||||||
var WorkflowEngine = class _WorkflowEngine {
|
var WorkflowEngine = class _WorkflowEngine {
|
||||||
constructor(vault, app, ollamaUrl, model, options) {
|
constructor(vault, app, ollamaUrl, model, options) {
|
||||||
this.vaultIndexer = new VaultIndexer(vault);
|
this.vaultIndexer = new VaultIndexer(vault);
|
||||||
this.toolExecutor = new ToolExecutor(vault, app);
|
this.toolExecutor = new ToolExecutor(vault, app, void 0, this.vaultIndexer);
|
||||||
this.ollamaClient = new OllamaClient(ollamaUrl, model, void 0, options?.cacheConfig);
|
this.ollamaClient = new OllamaClient(ollamaUrl, model, void 0, options?.cacheConfig);
|
||||||
this.conversationStateManager = new ConversationStateManager();
|
this.conversationStateManager = new ConversationStateManager();
|
||||||
this.maxSteps = options?.maxSteps ?? 20;
|
this.maxSteps = options?.maxSteps ?? 20;
|
||||||
@@ -10660,10 +10719,17 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
this.newChatButtonClickWrapper = null;
|
this.newChatButtonClickWrapper = null;
|
||||||
this.listenersAttached = false;
|
this.listenersAttached = false;
|
||||||
this.modeSelectorEl = null;
|
this.modeSelectorEl = null;
|
||||||
// Pending action state
|
|
||||||
this.pendingActions = [];
|
this.pendingActions = [];
|
||||||
this.pendingReadResults = [];
|
this.pendingReadResults = [];
|
||||||
this.pendingFollowUpContext = null;
|
this.pendingFollowUpContext = null;
|
||||||
|
// Auto-scroll & logs UI
|
||||||
|
this.shouldAutoScroll = true;
|
||||||
|
this.logsVisible = false;
|
||||||
|
this.showLogsButton = null;
|
||||||
|
this.logsContainer = null;
|
||||||
|
this.removeLogListener = null;
|
||||||
|
this.showLogsClickHandler = null;
|
||||||
|
this.chatScrollHandler = null;
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this.lastMessageEl = null;
|
this.lastMessageEl = null;
|
||||||
this.newChatButton = null;
|
this.newChatButton = null;
|
||||||
@@ -10682,10 +10748,17 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
this.ollamaClient = this.createOllamaClient(settings.chatModel ?? settings.model, settings);
|
this.ollamaClient = this.createOllamaClient(settings.chatModel ?? settings.model, settings);
|
||||||
this.agentOllamaClient = (settings.agentModel ?? settings.model) === (settings.chatModel ?? settings.model) ? this.ollamaClient : this.createOllamaClient(settings.agentModel ?? settings.model, settings);
|
this.agentOllamaClient = (settings.agentModel ?? settings.model) === (settings.chatModel ?? settings.model) ? this.ollamaClient : this.createOllamaClient(settings.agentModel ?? settings.model, settings);
|
||||||
this.vaultIndexer = new VaultIndexer(this.app.vault, void 0, vectorStore);
|
this.vaultIndexer = new VaultIndexer(this.app.vault, void 0, vectorStore);
|
||||||
this.toolExecutor = new ToolExecutor(this.app.vault, this.app, telemetryManager);
|
this.toolExecutor = new ToolExecutor(
|
||||||
|
this.app.vault,
|
||||||
|
this.app,
|
||||||
|
telemetryManager,
|
||||||
|
this.vaultIndexer
|
||||||
|
);
|
||||||
this.actionPreviewBuilder = new ActionPreviewBuilder(this.app.vault, this.app);
|
this.actionPreviewBuilder = new ActionPreviewBuilder(this.app.vault, this.app);
|
||||||
this.noteContextBuilder = new NoteContextBuilder(this.app.vault, this.app, this.vaultIndexer);
|
this.noteContextBuilder = new NoteContextBuilder(this.app.vault, this.app, this.vaultIndexer);
|
||||||
this.conversationStateManager = new ConversationStateManager();
|
this.conversationStateManager = new ConversationStateManager(
|
||||||
|
getSystemPromptForMode(this.currentAgentMode)
|
||||||
|
);
|
||||||
this.structuredMemoryManager = structuredMemoryManager;
|
this.structuredMemoryManager = structuredMemoryManager;
|
||||||
this.telemetryManager = telemetryManager;
|
this.telemetryManager = telemetryManager;
|
||||||
this.workflowEngine = new WorkflowEngine(
|
this.workflowEngine = new WorkflowEngine(
|
||||||
@@ -10695,6 +10768,11 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
settings.agentModel ?? settings.model,
|
settings.agentModel ?? settings.model,
|
||||||
{ cacheConfig: settings.cacheConfig }
|
{ cacheConfig: settings.cacheConfig }
|
||||||
);
|
);
|
||||||
|
this.removeLogListener = Logger.addListener((entry) => {
|
||||||
|
if (this.logsVisible && this.logsContainer) {
|
||||||
|
this.appendLogEntry(entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Getters for testing
|
// Getters for testing
|
||||||
getSendButtonClickHandler() {
|
getSendButtonClickHandler() {
|
||||||
@@ -10768,10 +10846,19 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
this.ollamaClient.cancelStream();
|
this.ollamaClient.cancelStream();
|
||||||
this.removeEventListeners();
|
this.removeEventListeners();
|
||||||
this.cleanupStreamingResources();
|
this.cleanupStreamingResources();
|
||||||
|
if (this.removeLogListener) {
|
||||||
|
this.removeLogListener();
|
||||||
|
this.removeLogListener = null;
|
||||||
|
}
|
||||||
|
if (this.logsContainer && this.logsContainer.parentElement) {
|
||||||
|
this.logsContainer.parentElement.removeChild(this.logsContainer);
|
||||||
|
}
|
||||||
this.lastMessageEl = null;
|
this.lastMessageEl = null;
|
||||||
this.sendButton = null;
|
this.sendButton = null;
|
||||||
this.inputEl = null;
|
this.inputEl = null;
|
||||||
this.chatContainer = null;
|
this.chatContainer = null;
|
||||||
|
this.showLogsButton = null;
|
||||||
|
this.logsContainer = null;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
cleanupStreamingResources() {
|
cleanupStreamingResources() {
|
||||||
@@ -10857,6 +10944,27 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
} else {
|
} else {
|
||||||
newChatContainer.appendChild(this.newChatButton);
|
newChatContainer.appendChild(this.newChatButton);
|
||||||
}
|
}
|
||||||
|
if (!this.showLogsButton) {
|
||||||
|
this.showLogsButton = newChatContainer.createEl("button", {
|
||||||
|
cls: "ollama-show-logs-button",
|
||||||
|
text: this.logsVisible ? "Hide Logs" : "Show Logs"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.showLogsButton.textContent = this.logsVisible ? "Hide Logs" : "Show Logs";
|
||||||
|
newChatContainer.appendChild(this.showLogsButton);
|
||||||
|
}
|
||||||
|
if (this.logsVisible) {
|
||||||
|
if (!this.logsContainer) {
|
||||||
|
this.logsContainer = this.contentEl.createEl("div", { cls: "ollama-logs-container" });
|
||||||
|
for (const entry of Logger.getHistory()) {
|
||||||
|
this.renderLogEntry(entry, this.logsContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.contentEl.insertBefore(this.logsContainer, inputContainer);
|
||||||
|
this.scrollLogsToBottom();
|
||||||
|
} else if (this.logsContainer && this.logsContainer.parentElement) {
|
||||||
|
this.logsContainer.parentElement.removeChild(this.logsContainer);
|
||||||
|
}
|
||||||
if (!this.inputEl) {
|
if (!this.inputEl) {
|
||||||
this.inputEl = inputContainer.createEl("textarea", {
|
this.inputEl = inputContainer.createEl("textarea", {
|
||||||
cls: "ollama-input",
|
cls: "ollama-input",
|
||||||
@@ -10876,6 +10984,9 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
this.contentEl.appendChild(newChatContainer);
|
this.contentEl.appendChild(newChatContainer);
|
||||||
this.contentEl.appendChild(inputContainer);
|
this.contentEl.appendChild(inputContainer);
|
||||||
this.contentEl.appendChild(container);
|
this.contentEl.appendChild(container);
|
||||||
|
if (this.shouldAutoScroll && container) {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
this.inputEl.focus();
|
this.inputEl.focus();
|
||||||
}
|
}
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
@@ -10894,6 +11005,10 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
this.newChatButtonClickHandler = () => {
|
this.newChatButtonClickHandler = () => {
|
||||||
this.clearConversation();
|
this.clearConversation();
|
||||||
};
|
};
|
||||||
|
this.showLogsClickHandler = () => {
|
||||||
|
this.logsVisible = !this.logsVisible;
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
if (this.sendButton && this.sendButtonClickHandler) {
|
if (this.sendButton && this.sendButtonClickHandler) {
|
||||||
this.sendButton.addEventListener("click", this.sendButtonClickHandler);
|
this.sendButton.addEventListener("click", this.sendButtonClickHandler);
|
||||||
}
|
}
|
||||||
@@ -10903,6 +11018,18 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
if (this.newChatButton && this.newChatButtonClickHandler) {
|
if (this.newChatButton && this.newChatButtonClickHandler) {
|
||||||
this.newChatButton.addEventListener("click", this.newChatButtonClickHandler);
|
this.newChatButton.addEventListener("click", this.newChatButtonClickHandler);
|
||||||
}
|
}
|
||||||
|
if (this.showLogsButton && this.showLogsClickHandler) {
|
||||||
|
this.showLogsButton.addEventListener("click", this.showLogsClickHandler);
|
||||||
|
}
|
||||||
|
if (this.chatContainer) {
|
||||||
|
this.chatScrollHandler = () => {
|
||||||
|
if (!this.chatContainer) return;
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = this.chatContainer;
|
||||||
|
const isNearBottom = scrollHeight - scrollTop - clientHeight < 50;
|
||||||
|
this.shouldAutoScroll = isNearBottom;
|
||||||
|
};
|
||||||
|
this.chatContainer.addEventListener("scroll", this.chatScrollHandler);
|
||||||
|
}
|
||||||
this.listenersAttached = true;
|
this.listenersAttached = true;
|
||||||
}
|
}
|
||||||
removeEventListeners() {
|
removeEventListeners() {
|
||||||
@@ -10918,6 +11045,12 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
if (this.newChatButton && this.newChatButtonClickHandler) {
|
if (this.newChatButton && this.newChatButtonClickHandler) {
|
||||||
this.newChatButton.removeEventListener("click", this.newChatButtonClickHandler);
|
this.newChatButton.removeEventListener("click", this.newChatButtonClickHandler);
|
||||||
}
|
}
|
||||||
|
if (this.showLogsButton && this.showLogsClickHandler) {
|
||||||
|
this.showLogsButton.removeEventListener("click", this.showLogsClickHandler);
|
||||||
|
}
|
||||||
|
if (this.chatContainer && this.chatScrollHandler) {
|
||||||
|
this.chatContainer.removeEventListener("scroll", this.chatScrollHandler);
|
||||||
|
}
|
||||||
this.listenersAttached = false;
|
this.listenersAttached = false;
|
||||||
}
|
}
|
||||||
getAgentMode() {
|
getAgentMode() {
|
||||||
@@ -10928,10 +11061,11 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
if (this.modeSelectorEl) {
|
if (this.modeSelectorEl) {
|
||||||
this.modeSelectorEl.value = mode;
|
this.modeSelectorEl.value = mode;
|
||||||
}
|
}
|
||||||
|
this.conversationStateManager.setSystemPrompt(getSystemPromptForMode(mode));
|
||||||
}
|
}
|
||||||
clearConversation() {
|
clearConversation() {
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this.conversationStateManager.clear();
|
this.conversationStateManager.clear(getSystemPromptForMode(this.currentAgentMode));
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
updateMessageById(id, updates) {
|
updateMessageById(id, updates) {
|
||||||
@@ -11177,35 +11311,6 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
}
|
}
|
||||||
return baseMessages;
|
return baseMessages;
|
||||||
}
|
}
|
||||||
buildMessages(userMessageContent, tools) {
|
|
||||||
const systemContent = getSystemPromptForMode(this.currentAgentMode);
|
|
||||||
const messages = [];
|
|
||||||
if (this.structuredMemoryManager) {
|
|
||||||
const memoryContext = this.structuredMemoryManager.buildMemoryContext();
|
|
||||||
if (memoryContext) {
|
|
||||||
messages.push({
|
|
||||||
role: "system",
|
|
||||||
content: memoryContext
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
messages.push({
|
|
||||||
role: "system",
|
|
||||||
content: systemContent
|
|
||||||
});
|
|
||||||
const userMessage = {
|
|
||||||
role: "user",
|
|
||||||
content: userMessageContent
|
|
||||||
};
|
|
||||||
messages.push(userMessage);
|
|
||||||
if (tools && tools.length > 0) {
|
|
||||||
messages.push({
|
|
||||||
role: "assistant",
|
|
||||||
content: "I have access to the following tools to help answer your questions:"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
async processToolCalls(toolCalls, messages, tools, fullResponse, assistantMessageId) {
|
async processToolCalls(toolCalls, messages, tools, fullResponse, assistantMessageId) {
|
||||||
const readToolCalls = toolCalls.filter((tc) => !isWriteTool(tc.function?.name ?? ""));
|
const readToolCalls = toolCalls.filter((tc) => !isWriteTool(tc.function?.name ?? ""));
|
||||||
const writeToolCalls = toolCalls.filter((tc) => isWriteTool(tc.function?.name ?? ""));
|
const writeToolCalls = toolCalls.filter((tc) => isWriteTool(tc.function?.name ?? ""));
|
||||||
@@ -11675,6 +11780,31 @@ ${actualMessage}` : actualMessage;
|
|||||||
isAgenticMode(mode) {
|
isAgenticMode(mode) {
|
||||||
return mode === "edit" || mode === "organize" || mode === "workflow";
|
return mode === "edit" || mode === "organize" || mode === "workflow";
|
||||||
}
|
}
|
||||||
|
renderLogEntry(entry, container) {
|
||||||
|
const row = container.createEl("div", { cls: "ollama-log-row" });
|
||||||
|
row.addClass(`ollama-log-row-${entry.levelLabel.toLowerCase()}`);
|
||||||
|
const time = new Date(entry.timestamp).toLocaleTimeString();
|
||||||
|
const timeSpan = row.createEl("span", { cls: "ollama-log-time", text: time });
|
||||||
|
const levelSpan = row.createEl("span", {
|
||||||
|
cls: `ollama-log-level ollama-log-level-${entry.levelLabel.toLowerCase()}`,
|
||||||
|
text: entry.levelLabel
|
||||||
|
});
|
||||||
|
const catSpan = row.createEl("span", { cls: "ollama-log-category", text: entry.category });
|
||||||
|
const msgSpan = row.createEl("span", { cls: "ollama-log-message", text: entry.message });
|
||||||
|
if (container.children.length > 250) {
|
||||||
|
container.removeChild(container.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendLogEntry(entry) {
|
||||||
|
if (!this.logsContainer) return;
|
||||||
|
this.renderLogEntry(entry, this.logsContainer);
|
||||||
|
this.scrollLogsToBottom();
|
||||||
|
}
|
||||||
|
scrollLogsToBottom() {
|
||||||
|
if (this.logsContainer) {
|
||||||
|
this.logsContainer.scrollTop = this.logsContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
var MAX_TOOL_CALLS = 5;
|
var MAX_TOOL_CALLS = 5;
|
||||||
|
|
||||||
|
|||||||
+122
-3
@@ -27,6 +27,7 @@ import { ConversationStateManager } from './conversation-state';
|
|||||||
import { ErrorHandler } from './error-handler';
|
import { ErrorHandler } from './error-handler';
|
||||||
import { StructuredMemoryManager } from './structured-memory';
|
import { StructuredMemoryManager } from './structured-memory';
|
||||||
import { TelemetryManager } from './tool-telemetry';
|
import { TelemetryManager } from './tool-telemetry';
|
||||||
|
import { Logger, LogEntry } from './utils';
|
||||||
|
|
||||||
export const VIEW_TYPE_OLLAMA_CHAT = 'ollama-chat-view';
|
export const VIEW_TYPE_OLLAMA_CHAT = 'ollama-chat-view';
|
||||||
|
|
||||||
@@ -73,7 +74,12 @@ export class ChatView extends ItemView {
|
|||||||
? this.ollamaClient
|
? this.ollamaClient
|
||||||
: this.createOllamaClient(settings.agentModel ?? settings.model, settings);
|
: this.createOllamaClient(settings.agentModel ?? settings.model, settings);
|
||||||
this.vaultIndexer = new VaultIndexer(this.app.vault, undefined, vectorStore);
|
this.vaultIndexer = new VaultIndexer(this.app.vault, undefined, vectorStore);
|
||||||
this.toolExecutor = new ToolExecutor(this.app.vault, this.app, telemetryManager);
|
this.toolExecutor = new ToolExecutor(
|
||||||
|
this.app.vault,
|
||||||
|
this.app,
|
||||||
|
telemetryManager,
|
||||||
|
this.vaultIndexer
|
||||||
|
);
|
||||||
this.actionPreviewBuilder = new ActionPreviewBuilder(this.app.vault, this.app);
|
this.actionPreviewBuilder = new ActionPreviewBuilder(this.app.vault, this.app);
|
||||||
this.noteContextBuilder = new NoteContextBuilder(this.app.vault, this.app, this.vaultIndexer);
|
this.noteContextBuilder = new NoteContextBuilder(this.app.vault, this.app, this.vaultIndexer);
|
||||||
this.conversationStateManager = new ConversationStateManager(
|
this.conversationStateManager = new ConversationStateManager(
|
||||||
@@ -88,6 +94,13 @@ export class ChatView extends ItemView {
|
|||||||
settings.agentModel ?? settings.model,
|
settings.agentModel ?? settings.model,
|
||||||
{ cacheConfig: settings.cacheConfig }
|
{ cacheConfig: settings.cacheConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Subscribe to plugin logs
|
||||||
|
this.removeLogListener = Logger.addListener((entry) => {
|
||||||
|
if (this.logsVisible && this.logsContainer) {
|
||||||
|
this.appendLogEntry(entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSettings(newSettings: PluginSettings) {
|
updateSettings(newSettings: PluginSettings) {
|
||||||
@@ -163,10 +176,19 @@ export class ChatView extends ItemView {
|
|||||||
this.ollamaClient.cancelStream();
|
this.ollamaClient.cancelStream();
|
||||||
this.removeEventListeners();
|
this.removeEventListeners();
|
||||||
this.cleanupStreamingResources();
|
this.cleanupStreamingResources();
|
||||||
|
if (this.removeLogListener) {
|
||||||
|
this.removeLogListener();
|
||||||
|
this.removeLogListener = null;
|
||||||
|
}
|
||||||
|
if (this.logsContainer && this.logsContainer.parentElement) {
|
||||||
|
this.logsContainer.parentElement.removeChild(this.logsContainer);
|
||||||
|
}
|
||||||
this.lastMessageEl = null;
|
this.lastMessageEl = null;
|
||||||
this.sendButton = null;
|
this.sendButton = null;
|
||||||
this.inputEl = null;
|
this.inputEl = null;
|
||||||
this.chatContainer = null;
|
this.chatContainer = null;
|
||||||
|
this.showLogsButton = null;
|
||||||
|
this.logsContainer = null;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +296,32 @@ export class ChatView extends ItemView {
|
|||||||
newChatContainer.appendChild(this.newChatButton);
|
newChatContainer.appendChild(this.newChatButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup show-logs toggle button
|
||||||
|
if (!this.showLogsButton) {
|
||||||
|
this.showLogsButton = newChatContainer.createEl('button', {
|
||||||
|
cls: 'ollama-show-logs-button',
|
||||||
|
text: this.logsVisible ? 'Hide Logs' : 'Show Logs',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.showLogsButton.textContent = this.logsVisible ? 'Hide Logs' : 'Show Logs';
|
||||||
|
newChatContainer.appendChild(this.showLogsButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup / toggle logs container
|
||||||
|
if (this.logsVisible) {
|
||||||
|
if (!this.logsContainer) {
|
||||||
|
this.logsContainer = this.contentEl.createEl('div', { cls: 'ollama-logs-container' });
|
||||||
|
// Populate with existing history
|
||||||
|
for (const entry of Logger.getHistory()) {
|
||||||
|
this.renderLogEntry(entry, this.logsContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.contentEl.insertBefore(this.logsContainer, inputContainer);
|
||||||
|
this.scrollLogsToBottom();
|
||||||
|
} else if (this.logsContainer && this.logsContainer.parentElement) {
|
||||||
|
this.logsContainer.parentElement.removeChild(this.logsContainer);
|
||||||
|
}
|
||||||
|
|
||||||
// Setup input area
|
// Setup input area
|
||||||
if (!this.inputEl) {
|
if (!this.inputEl) {
|
||||||
this.inputEl = inputContainer.createEl('textarea', {
|
this.inputEl = inputContainer.createEl('textarea', {
|
||||||
@@ -299,6 +347,11 @@ export class ChatView extends ItemView {
|
|||||||
this.contentEl.appendChild(inputContainer);
|
this.contentEl.appendChild(inputContainer);
|
||||||
this.contentEl.appendChild(container);
|
this.contentEl.appendChild(container);
|
||||||
|
|
||||||
|
// Auto-scroll chat to bottom if enabled
|
||||||
|
if (this.shouldAutoScroll && container) {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// Focus input on open
|
// Focus input on open
|
||||||
this.inputEl.focus();
|
this.inputEl.focus();
|
||||||
}
|
}
|
||||||
@@ -323,6 +376,11 @@ export class ChatView extends ItemView {
|
|||||||
this.clearConversation();
|
this.clearConversation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.showLogsClickHandler = () => {
|
||||||
|
this.logsVisible = !this.logsVisible;
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
if (this.sendButton && this.sendButtonClickHandler) {
|
if (this.sendButton && this.sendButtonClickHandler) {
|
||||||
this.sendButton.addEventListener('click', this.sendButtonClickHandler);
|
this.sendButton.addEventListener('click', this.sendButtonClickHandler);
|
||||||
}
|
}
|
||||||
@@ -335,6 +393,21 @@ export class ChatView extends ItemView {
|
|||||||
this.newChatButton.addEventListener('click', this.newChatButtonClickHandler);
|
this.newChatButton.addEventListener('click', this.newChatButtonClickHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.showLogsButton && this.showLogsClickHandler) {
|
||||||
|
this.showLogsButton.addEventListener('click', this.showLogsClickHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chat container scroll listener for auto-scroll toggle
|
||||||
|
if (this.chatContainer) {
|
||||||
|
this.chatScrollHandler = () => {
|
||||||
|
if (!this.chatContainer) return;
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = this.chatContainer;
|
||||||
|
const isNearBottom = scrollHeight - scrollTop - clientHeight < 50;
|
||||||
|
this.shouldAutoScroll = isNearBottom;
|
||||||
|
};
|
||||||
|
this.chatContainer.addEventListener('scroll', this.chatScrollHandler);
|
||||||
|
}
|
||||||
|
|
||||||
this.listenersAttached = true;
|
this.listenersAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,6 +428,14 @@ export class ChatView extends ItemView {
|
|||||||
this.newChatButton.removeEventListener('click', this.newChatButtonClickHandler);
|
this.newChatButton.removeEventListener('click', this.newChatButtonClickHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.showLogsButton && this.showLogsClickHandler) {
|
||||||
|
this.showLogsButton.removeEventListener('click', this.showLogsClickHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.chatContainer && this.chatScrollHandler) {
|
||||||
|
this.chatContainer.removeEventListener('scroll', this.chatScrollHandler);
|
||||||
|
}
|
||||||
|
|
||||||
this.listenersAttached = false;
|
this.listenersAttached = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1217,8 +1298,6 @@ export class ChatView extends ItemView {
|
|||||||
|
|
||||||
private modeSelectorEl: HTMLSelectElement | null = null;
|
private modeSelectorEl: HTMLSelectElement | null = null;
|
||||||
private currentAgentMode: AgentMode;
|
private currentAgentMode: AgentMode;
|
||||||
|
|
||||||
// Pending action state
|
|
||||||
private pendingActions: ProposedAction[] = [];
|
private pendingActions: ProposedAction[] = [];
|
||||||
private pendingReadResults: (ToolResult & { id?: string })[] = [];
|
private pendingReadResults: (ToolResult & { id?: string })[] = [];
|
||||||
private pendingFollowUpContext: {
|
private pendingFollowUpContext: {
|
||||||
@@ -1227,6 +1306,15 @@ export class ChatView extends ItemView {
|
|||||||
assistantMessageId: string;
|
assistantMessageId: string;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
|
// Auto-scroll & logs UI
|
||||||
|
private shouldAutoScroll: boolean = true;
|
||||||
|
private logsVisible: boolean = false;
|
||||||
|
private showLogsButton: HTMLElement | null = null;
|
||||||
|
private logsContainer: HTMLElement | null = null;
|
||||||
|
private removeLogListener: (() => void) | null = null;
|
||||||
|
private showLogsClickHandler: (() => void) | null = null;
|
||||||
|
private chatScrollHandler: (() => void) | null = null;
|
||||||
|
|
||||||
private createOllamaClient(model: string, settings: PluginSettings): OllamaClient {
|
private createOllamaClient(model: string, settings: PluginSettings): OllamaClient {
|
||||||
return new OllamaClient(settings.ollamaUrl, model, undefined, settings.cacheConfig);
|
return new OllamaClient(settings.ollamaUrl, model, undefined, settings.cacheConfig);
|
||||||
}
|
}
|
||||||
@@ -1251,6 +1339,37 @@ export class ChatView extends ItemView {
|
|||||||
private isAgenticMode(mode: AgentMode): boolean {
|
private isAgenticMode(mode: AgentMode): boolean {
|
||||||
return mode === 'edit' || mode === 'organize' || mode === 'workflow';
|
return mode === 'edit' || mode === 'organize' || mode === 'workflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderLogEntry(entry: LogEntry, container: HTMLElement): void {
|
||||||
|
const row = container.createEl('div', { cls: 'ollama-log-row' });
|
||||||
|
row.addClass(`ollama-log-row-${entry.levelLabel.toLowerCase()}`);
|
||||||
|
|
||||||
|
const time = new Date(entry.timestamp).toLocaleTimeString();
|
||||||
|
const timeSpan = row.createEl('span', { cls: 'ollama-log-time', text: time });
|
||||||
|
const levelSpan = row.createEl('span', {
|
||||||
|
cls: `ollama-log-level ollama-log-level-${entry.levelLabel.toLowerCase()}`,
|
||||||
|
text: entry.levelLabel,
|
||||||
|
});
|
||||||
|
const catSpan = row.createEl('span', { cls: 'ollama-log-category', text: entry.category });
|
||||||
|
const msgSpan = row.createEl('span', { cls: 'ollama-log-message', text: entry.message });
|
||||||
|
|
||||||
|
// Keep DOM lean
|
||||||
|
if (container.children.length > 250) {
|
||||||
|
container.removeChild(container.firstChild!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private appendLogEntry(entry: LogEntry): void {
|
||||||
|
if (!this.logsContainer) return;
|
||||||
|
this.renderLogEntry(entry, this.logsContainer);
|
||||||
|
this.scrollLogsToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollLogsToBottom(): void {
|
||||||
|
if (this.logsContainer) {
|
||||||
|
this.logsContainer.scrollTop = this.logsContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_TOOL_CALLS = 5;
|
const MAX_TOOL_CALLS = 5;
|
||||||
|
|||||||
+31
-4
@@ -1,9 +1,10 @@
|
|||||||
// src/tool-executor.ts
|
// src/tool-executor.ts
|
||||||
|
|
||||||
import { Vault, App, TFile } from 'obsidian';
|
import { Vault, App, TFile } from 'obsidian';
|
||||||
import type { ToolCall, ToolResult } from './types';
|
import type { ToolCall, ToolResult, VaultIndexEntry } from './types';
|
||||||
import { safeParseJson } from './utils';
|
import { safeParseJson } from './utils';
|
||||||
import { TelemetryManager } from './tool-telemetry';
|
import { TelemetryManager } from './tool-telemetry';
|
||||||
|
import { VaultIndexer } from './vault-indexer';
|
||||||
|
|
||||||
// Disallow characters that are invalid in file paths
|
// Disallow characters that are invalid in file paths
|
||||||
const INVALID_PATH_CHARS = /[<>:"|?*~]/;
|
const INVALID_PATH_CHARS = /[<>:"|?*~]/;
|
||||||
@@ -14,11 +15,18 @@ export class ToolExecutor {
|
|||||||
private vault: Vault;
|
private vault: Vault;
|
||||||
private app: App;
|
private app: App;
|
||||||
private telemetryManager?: TelemetryManager;
|
private telemetryManager?: TelemetryManager;
|
||||||
|
private vaultIndexer?: VaultIndexer;
|
||||||
|
|
||||||
constructor(vault: Vault, app: App, telemetryManager?: TelemetryManager) {
|
constructor(
|
||||||
|
vault: Vault,
|
||||||
|
app: App,
|
||||||
|
telemetryManager?: TelemetryManager,
|
||||||
|
vaultIndexer?: VaultIndexer
|
||||||
|
) {
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
this.app = app;
|
this.app = app;
|
||||||
this.telemetryManager = telemetryManager;
|
this.telemetryManager = telemetryManager;
|
||||||
|
this.vaultIndexer = vaultIndexer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isSafePath(path: string): boolean {
|
private isSafePath(path: string): boolean {
|
||||||
@@ -126,7 +134,7 @@ export class ToolExecutor {
|
|||||||
result = await this.handleReadVaultFile(parsedArgs);
|
result = await this.handleReadVaultFile(parsedArgs);
|
||||||
break;
|
break;
|
||||||
case 'search_vault_files':
|
case 'search_vault_files':
|
||||||
result = this.handleSearchVaultFiles(parsedArgs);
|
result = await this.handleSearchVaultFiles(parsedArgs);
|
||||||
break;
|
break;
|
||||||
case 'append_to_note':
|
case 'append_to_note':
|
||||||
result = await this.handleAppendToNote(parsedArgs);
|
result = await this.handleAppendToNote(parsedArgs);
|
||||||
@@ -227,7 +235,7 @@ export class ToolExecutor {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleSearchVaultFiles(args: Record<string, unknown>): ToolResult {
|
private async handleSearchVaultFiles(args: Record<string, unknown>): Promise<ToolResult> {
|
||||||
const query = args.query;
|
const query = args.query;
|
||||||
const limitArg = args.limit;
|
const limitArg = args.limit;
|
||||||
|
|
||||||
@@ -236,6 +244,25 @@ export class ToolExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const limit = typeof limitArg === 'number' && Number.isFinite(limitArg) ? limitArg : 10;
|
const limit = typeof limitArg === 'number' && Number.isFinite(limitArg) ? limitArg : 10;
|
||||||
|
|
||||||
|
// Use VaultIndexer for rich content/tag/search if available
|
||||||
|
if (this.vaultIndexer) {
|
||||||
|
const results = await this.vaultIndexer.searchVault(query, limit);
|
||||||
|
const files = results.map((entry: VaultIndexEntry) => ({
|
||||||
|
path: entry.path,
|
||||||
|
basename: entry.path.split('/').pop() ?? entry.path,
|
||||||
|
title: entry.title,
|
||||||
|
score: entry.score,
|
||||||
|
tags: entry.tags,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Found ${files.length} matching files`,
|
||||||
|
data: files,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to simple path-based search
|
||||||
const normalizedQuery = query.toLowerCase();
|
const normalizedQuery = query.toLowerCase();
|
||||||
const files = this.vault
|
const files = this.vault
|
||||||
.getMarkdownFiles()
|
.getMarkdownFiles()
|
||||||
|
|||||||
@@ -12,8 +12,19 @@ const SEVERITY_ORDER: Record<string, number> = {
|
|||||||
error: LogLevel.ERROR,
|
error: LogLevel.ERROR,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
timestamp: number;
|
||||||
|
level: LogLevel;
|
||||||
|
levelLabel: string;
|
||||||
|
category: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class Logger {
|
export class Logger {
|
||||||
private static minLevel: LogLevel = LogLevel.DEBUG;
|
private static minLevel: LogLevel = LogLevel.DEBUG;
|
||||||
|
private static listeners: Array<(entry: LogEntry) => void> = [];
|
||||||
|
private static history: LogEntry[] = [];
|
||||||
|
private static maxHistory: number = 500;
|
||||||
|
|
||||||
static setLevel(level: string | LogLevel): void {
|
static setLevel(level: string | LogLevel): void {
|
||||||
if (typeof level === 'string') {
|
if (typeof level === 'string') {
|
||||||
@@ -24,28 +35,75 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static addListener(callback: (entry: LogEntry) => void): () => void {
|
||||||
|
Logger.listeners.push(callback);
|
||||||
|
return () => {
|
||||||
|
const idx = Logger.listeners.indexOf(callback);
|
||||||
|
if (idx !== -1) {
|
||||||
|
Logger.listeners.splice(idx, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getHistory(): LogEntry[] {
|
||||||
|
return [...Logger.history];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static record(
|
||||||
|
level: LogLevel,
|
||||||
|
levelLabel: string,
|
||||||
|
message: string,
|
||||||
|
category: string
|
||||||
|
): void {
|
||||||
|
if (level < Logger.minLevel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entry: LogEntry = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
level,
|
||||||
|
levelLabel,
|
||||||
|
category,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
Logger.history.push(entry);
|
||||||
|
if (Logger.history.length > Logger.maxHistory) {
|
||||||
|
Logger.history = Logger.history.slice(-Logger.maxHistory);
|
||||||
|
}
|
||||||
|
for (const listener of Logger.listeners) {
|
||||||
|
try {
|
||||||
|
listener(entry);
|
||||||
|
} catch {
|
||||||
|
// ignore listener errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static debug(message: string, category: string = 'general'): void {
|
static debug(message: string, category: string = 'general'): void {
|
||||||
if (LogLevel.DEBUG >= Logger.minLevel) {
|
if (LogLevel.DEBUG >= Logger.minLevel) {
|
||||||
console.debug(`[${category}] DEBUG: ${message}`);
|
console.debug(`[${category}] DEBUG: ${message}`);
|
||||||
}
|
}
|
||||||
|
Logger.record(LogLevel.DEBUG, 'DEBUG', message, category);
|
||||||
}
|
}
|
||||||
|
|
||||||
static info(message: string, category: string = 'general'): void {
|
static info(message: string, category: string = 'general'): void {
|
||||||
if (LogLevel.INFO >= Logger.minLevel) {
|
if (LogLevel.INFO >= Logger.minLevel) {
|
||||||
console.info(`[${category}] INFO: ${message}`);
|
console.info(`[${category}] INFO: ${message}`);
|
||||||
}
|
}
|
||||||
|
Logger.record(LogLevel.INFO, 'INFO', message, category);
|
||||||
}
|
}
|
||||||
|
|
||||||
static warn(message: string, category: string = 'general'): void {
|
static warn(message: string, category: string = 'general'): void {
|
||||||
if (LogLevel.WARN >= Logger.minLevel) {
|
if (LogLevel.WARN >= Logger.minLevel) {
|
||||||
console.warn(`[${category}] WARN: ${message}`);
|
console.warn(`[${category}] WARN: ${message}`);
|
||||||
}
|
}
|
||||||
|
Logger.record(LogLevel.WARN, 'WARN', message, category);
|
||||||
}
|
}
|
||||||
|
|
||||||
static error(message: string, category: string = 'general'): void {
|
static error(message: string, category: string = 'general'): void {
|
||||||
if (LogLevel.ERROR >= Logger.minLevel) {
|
if (LogLevel.ERROR >= Logger.minLevel) {
|
||||||
console.error(`[${category}] ERROR: ${message}`);
|
console.error(`[${category}] ERROR: ${message}`);
|
||||||
}
|
}
|
||||||
|
Logger.record(LogLevel.ERROR, 'ERROR', message, category);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export class WorkflowEngine {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
this.vaultIndexer = new VaultIndexer(vault);
|
this.vaultIndexer = new VaultIndexer(vault);
|
||||||
this.toolExecutor = new ToolExecutor(vault, app);
|
this.toolExecutor = new ToolExecutor(vault, app, undefined, this.vaultIndexer);
|
||||||
this.ollamaClient = new OllamaClient(ollamaUrl, model, undefined, options?.cacheConfig);
|
this.ollamaClient = new OllamaClient(ollamaUrl, model, undefined, options?.cacheConfig);
|
||||||
this.conversationStateManager = new ConversationStateManager();
|
this.conversationStateManager = new ConversationStateManager();
|
||||||
this.maxSteps = options?.maxSteps ?? 20;
|
this.maxSteps = options?.maxSteps ?? 20;
|
||||||
|
|||||||
+84
@@ -4,6 +4,11 @@
|
|||||||
--ollama-border: var(--background-modifier-border);
|
--ollama-border: var(--background-modifier-border);
|
||||||
--ollama-radius: var(--radius-m);
|
--ollama-radius: var(--radius-m);
|
||||||
--ollama-gap: var(--size-4-2);
|
--ollama-gap: var(--size-4-2);
|
||||||
|
--ollama-log-debug: var(--text-muted);
|
||||||
|
--ollama-log-info: var(--text-accent);
|
||||||
|
--ollama-log-warn: var(--text-warning);
|
||||||
|
--ollama-log-error: var(--text-error);
|
||||||
|
--ollama-log-bg: var(--background-primary-alt);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ollama-chat-container {
|
.ollama-chat-container {
|
||||||
@@ -297,3 +302,82 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--interactive-accent);
|
border-color: var(--interactive-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Show Logs Button */
|
||||||
|
.ollama-show-logs-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-4-1);
|
||||||
|
padding: var(--size-4-1) var(--size-4-2);
|
||||||
|
font-size: var(--font-smallest);
|
||||||
|
border-radius: var(--ollama-radius);
|
||||||
|
border: 1px solid var(--ollama-border);
|
||||||
|
background-color: var(--background-modifier-form-field);
|
||||||
|
color: var(--text-normal);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ollama-show-logs-button:hover {
|
||||||
|
background-color: var(--background-modifier-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logs Panel */
|
||||||
|
.ollama-logs-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: var(--size-4-1) var(--size-4-2);
|
||||||
|
max-height: 12rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: var(--ollama-log-bg);
|
||||||
|
border-top: 1px solid var(--ollama-border);
|
||||||
|
border-bottom: 1px solid var(--ollama-border);
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
font-size: var(--font-smallest);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ollama-log-row {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--size-4-1);
|
||||||
|
align-items: baseline;
|
||||||
|
line-height: var(--line-height-tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ollama-log-time {
|
||||||
|
color: var(--text-faint);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ollama-log-level {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
text-transform: uppercase;
|
||||||
|
min-width: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ollama-log-level-debug {
|
||||||
|
color: var(--ollama-log-debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ollama-log-level-info {
|
||||||
|
color: var(--ollama-log-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ollama-log-level-warn {
|
||||||
|
color: var(--ollama-log-warn);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ollama-log-level-error {
|
||||||
|
color: var(--ollama-log-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ollama-log-category {
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ollama-log-message {
|
||||||
|
color: var(--text-normal);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|||||||
@@ -725,6 +725,57 @@ describe('ToolExecutor', () => {
|
|||||||
expect(result.data).toHaveLength(0);
|
expect(result.data).toHaveLength(0);
|
||||||
expect(result.message).toContain('Found 0 matching files');
|
expect(result.message).toContain('Found 0 matching files');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use VaultIndexer for rich search when available', async () => {
|
||||||
|
const mockIndexer = {
|
||||||
|
searchVault: jest.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
path: 'Projects/AI/ml-basics.md',
|
||||||
|
title: 'Machine Learning Basics',
|
||||||
|
content: 'Intro to ML...',
|
||||||
|
score: 0.95,
|
||||||
|
tags: 'ai, ml, tutorial',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'Projects/AI/deep-learning.md',
|
||||||
|
title: 'Deep Learning',
|
||||||
|
content: 'Neural networks...',
|
||||||
|
score: 0.88,
|
||||||
|
tags: 'ai, neural-networks',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
const indexedExecutor = new ToolExecutor(
|
||||||
|
mockVault as unknown as any,
|
||||||
|
mockApp as unknown as any,
|
||||||
|
undefined,
|
||||||
|
mockIndexer as unknown as any
|
||||||
|
);
|
||||||
|
|
||||||
|
const call: ToolCall = {
|
||||||
|
id: 'call_39',
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'search_vault_files',
|
||||||
|
arguments: JSON.stringify({
|
||||||
|
query: 'machine learning',
|
||||||
|
limit: 5,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = await indexedExecutor.handleToolCall(call);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data).toHaveLength(2);
|
||||||
|
expect(mockIndexer.searchVault).toHaveBeenCalledWith('machine learning', 5);
|
||||||
|
const firstResult = (result.data as any[])[0];
|
||||||
|
expect(firstResult).toMatchObject({
|
||||||
|
path: 'Projects/AI/ml-basics.md',
|
||||||
|
basename: 'ml-basics.md',
|
||||||
|
title: 'Machine Learning Basics',
|
||||||
|
score: 0.95,
|
||||||
|
tags: 'ai, ml, tutorial',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('executeTool method', () => {
|
describe('executeTool method', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user