Fix function call parsing and conversation loading issues

- Extract functionCall/functionResponse objects properly in Gemini
- Reset chat area state when loading conversations
- Add SanitiserService for path validation
- Prevent duplicate conversation names
- Improve chat scrolling and padding logic
- Clear streaming state on submission complete
This commit is contained in:
Andrew Beal 2025-10-24 17:25:19 +01:00
parent 8347ddbdf2
commit 4a42b1f84c
10 changed files with 302 additions and 244 deletions

View file

@ -164,7 +164,14 @@ export class Gemini implements IAIClass {
if (content.isFunctionCallResponse) {
if (isValidJson(content.content)) {
try {
parts.push(JSON.parse(content.content));
const parsedContent = JSON.parse(content.content);
if (parsedContent.functionResponse) {
parts.push({
functionResponse: parsedContent.functionResponse
});
} else {
parts.push({ text: content.content });
}
} catch (error) {
console.error("Failed to parse function response:", error);
parts.push({ text: content.content });
@ -181,7 +188,15 @@ export class Gemini implements IAIClass {
if (content.isFunctionCall && content.functionCall.trim() !== "") {
if (isValidJson(content.functionCall)) {
try {
parts.push(JSON.parse(content.functionCall));
const parsedContent = JSON.parse(content.functionCall);
if (parsedContent.functionCall) {
parts.push({
functionCall: {
name: parsedContent.functionCall.name,
args: parsedContent.functionCall.args
}
});
}
} catch (error) {
console.error("Failed to parse function call:", error);
}

View file

@ -8,7 +8,6 @@
import { Role } from "Enums/Role";
import type { ConversationContent } from "Conversations/ConversationContent";
import { tick } from "svelte";
import type AIAgentPlugin from "main";
import { Copy } from "Enums/Copy";
import { Selector } from "Enums/Selector";
@ -19,47 +18,66 @@
export let currentStreamingMessageId: string | null = null;
export let editModeActive: boolean = false;
export function onFinishedSubmitting() {
setTimeout(() => {
if (lastAssistantMessageElement && lastAssistantMessageElement.offsetHeight <
chatContainer.offsetHeight - parseFloat(getComputedStyle(chatContainer).padding) * 2) {
// Recalculate padding when streaming ends to fix race condition with streaming indicator removal
tick().then(() => {
if (lastAssistantMessageElement) {
assistantMessageAction(lastAssistantMessageElement);
}
});
let conversationKey = 0;
// use an interval to complete scrolling once the dom has finished updating
const scrollInterval: number = plugin.registerInterval(window.setInterval(() => {
tick().then(() => {
chatContainer.scroll({ top: chatContainer.scrollHeight, behavior: "smooth" });
});
}, 50));
setTimeout(() => {
if (scrollInterval) {
clearInterval(scrollInterval);
}
}, 1000);
export function resetChatArea() {
messageElements = [];
lastProcessedContent.clear();
currentStreamFinalized = false;
conversationKey++; // Force complete re-render
if (chatAreaPaddingElement) {
chatAreaPaddingElement.style.padding = "0px";
}
}, 500);
chatContainer.scroll({ top: 0, behavior: "instant" });
}
export function scrollChatArea() {
tick().then(() => {
settled = false;
const lastMessage = messageElements.sort((a, b) => a.index - b.index).last();
if (!lastMessage || !chatAreaPaddingElement) {
if (chatAreaPaddingElement) {
chatAreaPaddingElement.style.padding = "0px";
}
return;
}
const gap = parseFloat(getComputedStyle(chatContainer).gap) || 0;
const paddingTop = parseFloat(getComputedStyle(chatContainer).paddingTop) || 0;
const paddingBottom = parseFloat(getComputedStyle(chatContainer).paddingBottom) || 0;
let usedSpace = 0;
for (let i = messageElements.length - 1; i >= 0; i--) {
const messageElement = messageElements[i];
usedSpace += messageElement.element.offsetHeight + gap;
if (messageElement.element.classList.contains(Role.User)) {
break;
}
}
const padding = chatContainer.offsetHeight - paddingTop - paddingBottom - usedSpace;
chatAreaPaddingElement.style.padding = `${Math.max(0, padding / 2)}px`;
tick().then(() => {
chatContainer.scroll({ top: chatContainer.scrollHeight, behavior: "smooth" })
tick().then(() => settled = true);
});
});
}
let settled: boolean = false;
let thoughtElement: HTMLElement | undefined;
let streamingElement: HTMLElement | undefined;
let chatAreaPaddingElement: HTMLElement | undefined;
let plugin: AIAgentPlugin = Resolve<AIAgentPlugin>(Services.AIAgentPlugin);
let streamingMarkdownService: StreamingMarkdownService = Resolve<StreamingMarkdownService>(Services.StreamingMarkdownService);
let messageElements: Map<string, HTMLElement> = new Map<string, HTMLElement>();
let messageElements: { index: number, element: HTMLElement }[] = [];
let lastProcessedContent: Map<string, string> = new Map<string, string>();
let currentStreamFinalized: boolean = false;
let messagePadding: number = 0;
let staticMessagePadding: number = 0;
let lastAssistantMessageElement: HTMLElement | undefined;
function getGreetingByTime(): string {
const hour = new Date().getHours();
@ -81,31 +99,7 @@
}
}
// Track streaming messages and update them incrementally
$: {
messages.forEach((message) => {
if (message.role !== Role.User) {
const messageId = message.timestamp.getTime().toString();
const lastContent = lastProcessedContent.get(messageId) || "";
// Only update if content has changed
if (message.content !== lastContent) {
// Check if this specific message is currently streaming
const isCurrentlyStreaming = currentStreamingMessageId === messageId;
updateMessageContent({ ...message, id: messageId, isCurrentlyStreaming });
lastProcessedContent.set(messageId, message.content);
}
}
});
}
function updateMessageContent(message: {id: string, content: string, role: string, isCurrentlyStreaming: boolean}) {
const element = messageElements.get(message.id);
if (!element) {
return;
}
if (message.isCurrentlyStreaming) {
streamingMarkdownService.streamChunk(message.id, message.content);
currentStreamFinalized = false;
@ -115,22 +109,6 @@
}
}
function initializeMessageElement(messageId: string, element: HTMLElement) {
messageElements.set(messageId, element);
streamingMarkdownService.initializeStream(messageId, element);
}
// Svelte action to handle element initialization
function streamingAction(element: HTMLElement, messageId: string) {
initializeMessageElement(messageId, element);
return {
destroy() {
messageElements.delete(messageId);
}
};
}
// Process static messages (user messages and initial load)
function getStaticHTML(message: ConversationContent): string {
if (message.role === Role.User) {
@ -159,160 +137,47 @@
return ""; // Streaming messages will be handled by the streaming service
}
// Make sure to clean up when messages are removed
function streamingAction(element: HTMLElement, messageId: string) {
streamingMarkdownService.initializeStream(messageId, element);
}
function trackingAction(element: HTMLElement, index: number) {
messageElements.push({ index: index, element: element });
}
// Track streaming messages and update them incrementally
$: {
const currentMessageIds = new Set(messages.map((message) => message.timestamp.getTime().toString()));
// Remove tracking for messages that no longer exist
for (const [id] of messageElements) {
if (!currentMessageIds.has(id)) {
messageElements.delete(id);
lastProcessedContent.delete(id);
}
}
}
/**
* Chat area padding logic during message streaming
*/
function messageContainerAction(element: HTMLElement) {
tick().then(() => {
if (element.classList.contains(Role.Assistant)) {
assistantMessageAction(element);
}
else {
userMessageAction(element);
}
});
}
function userMessageAction(element: HTMLElement) {
requestAnimationFrame(() => {
const paddingTop = parseFloat(getComputedStyle(chatContainer).paddingTop) || 0;
if (element.offsetHeight > chatContainer.offsetHeight / 2) {
messagePadding = chatContainer.offsetHeight * 0.75;
staticMessagePadding = messagePadding;
} else {
staticMessagePadding = Math.max(0,
chatContainer.offsetHeight -
element.offsetHeight -
paddingTop);
messagePadding = Math.max(0,
chatContainer.offsetHeight -
element.offsetHeight -
calculateStreamingThoughtSize() -
paddingTop);
}
chatContainer.style.paddingBottom = messagePadding == 0 ? "" : `${messagePadding}px`;
tick().then(() => {
chatContainer.scroll({ top: chatContainer.scrollHeight, behavior: "smooth" });
});
});
}
function assistantMessageAction(element: HTMLElement) {
lastAssistantMessageElement = element;
const resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => {
const previousAssistantMessagesHeight = calculatePreviousAssistantMessagesHeight(element);
messagePadding = Math.max(0,
staticMessagePadding -
previousAssistantMessagesHeight -
element.offsetHeight -
calculateStreamingThoughtSize() -
parseFloat(getComputedStyle(chatContainer).gap) || 0);
chatContainer.style.paddingBottom = messagePadding == 0 ? "" : `${messagePadding}px`;
});
});
resizeObserver.observe(element);
return {
destroy() {
resizeObserver.disconnect();
}
}
}
function calculatePreviousAssistantMessagesHeight(currentElement: HTMLElement): number {
// Find the index of the current message in the messages array
const currentMessageIndex = messages.findIndex((message) => {
const messageId = message.timestamp.getTime().toString();
const messageElement = messageElements.get(messageId);
return messageElement?.parentElement === currentElement;
});
if (currentMessageIndex === -1) {
return 0;
}
// Walk backward from current message to find the last user message
let totalHeight = 0;
const gap = parseFloat(getComputedStyle(chatContainer).gap) || 0;
for (let i = currentMessageIndex - 1; i >= 0; i--) {
const message = messages[i];
// Stop when we hit a user message
if (message.role === Role.User) {
break;
}
// Sum up heights of all assistant messages before the current one
if (message.role === Role.Assistant && !message.isFunctionCall && !message.isFunctionCallResponse && message.content) {
messages.forEach((message) => {
if (message.role !== Role.User) {
const messageId = message.timestamp.getTime().toString();
const messageElement = messageElements.get(messageId);
if (messageElement) {
const containerElement = messageElement.parentElement;
if (containerElement) {
totalHeight += containerElement.offsetHeight + gap;
const lastContent = lastProcessedContent.get(messageId) || "";
// Only update if content has changed
if (message.content !== lastContent) {
// Check if this specific message is currently streaming
const isCurrentlyStreaming = currentStreamingMessageId === messageId;
// Only process through streaming service if actively streaming
if (isCurrentlyStreaming) {
updateMessageContent({ ...message, id: messageId, isCurrentlyStreaming });
}
lastProcessedContent.set(messageId, message.content);
}
}
}
return totalHeight;
});
}
$: {
if (chatContainer && messages.length == 0) {
chatContainer.style.paddingBottom = "";
if (messages.length === 0 && chatAreaPaddingElement) {
chatAreaPaddingElement.style.padding = "0px";
}
}
function calculateStreamingThoughtSize() {
const thoughtHeight = thoughtElement?.offsetHeight || 0;
const streamingHeight = streamingElement?.offsetHeight || 0;
let thoughtMargins = 0;
if (thoughtElement) {
thoughtMargins = parseFloat(getComputedStyle(thoughtElement).marginTop) || 0 +
parseFloat(getComputedStyle(thoughtElement).marginBottom) || 0;
}
let indicatorMargins = 0;
if (streamingElement) {
indicatorMargins = parseFloat(getComputedStyle(streamingElement).marginTop) || 0 +
parseFloat(getComputedStyle(streamingElement).marginBottom) || 0;
}
const gap = parseFloat(getComputedStyle(chatContainer).gap) || 0;
const gaps = thoughtElement && streamingElement ? 2 : thoughtElement || streamingElement ? 1 : 0;
return thoughtHeight + streamingHeight + thoughtMargins + indicatorMargins + (gaps * gap);
}
</script>
<div class="chat-area" bind:this={chatContainer}>
{#each messages as message}
{#each messages as message, index}
{#if !message.isFunctionCallResponse && message.content}
<div class="message-container {message.role === Role.User ? Role.User : Role.Assistant}" use:messageContainerAction>
<div class="message-container {message.role === Role.User ? Role.User : Role.Assistant}" use:trackingAction={index}>
<div class="message-bubble {message.role === Role.User ? Role.User : Role.Assistant}">
{#if message.role === Role.User}
<div class="message-text-user fade-in-fast">{message.content}</div>
@ -330,11 +195,15 @@
</div>
{/if}
{/each}
<ChatAreaThought bind:thoughtElement thought={currentThought}/>
{#if isSubmitting}
<StreamingIndicator bind:streamingElement editModeActive={editModeActive}/>
{#if settled}
<ChatAreaThought bind:thoughtElement thought={currentThought}/>
{#if isSubmitting}
<StreamingIndicator bind:streamingElement editModeActive={editModeActive}/>
{/if}
{/if}
<div bind:this={chatAreaPaddingElement}></div>
{#if messages.length === 0}
<div class="conversation-empty-state">

View file

@ -30,7 +30,7 @@
let editModeActive = false;
let currentStreamingMessageId: string | null = null;
let conversation = new Conversation();
let conversation: Conversation = new Conversation();
let currentThought: string | null = null;
@ -40,6 +40,10 @@
});
}
export function resetChatArea() {
chatArea.resetChatArea();
}
onMount(() => {
if (chatContainer) {
plugin.registerDomEvent(chatContainer, 'click', handleLinkClick);
@ -85,7 +89,7 @@
chatService.stop();
currentThought = null;
isSubmitting = false;
chatArea.onFinishedSubmitting();
chatArea.scrollChatArea();
}
async function handleSubmit() {
@ -99,15 +103,17 @@
return;
}
isSubmitting = true;
const currentRequest = userRequest;
textareaElement.value = "";
userRequest = "";
autoResize();
scrollToBottom();
await chatService.submit(conversation, editModeActive, currentRequest, {
onSubmit: () => {
chatArea.scrollChatArea();
isSubmitting = true;
},
onStreamingUpdate: (streamingId) => {
conversation = conversation;
currentStreamingMessageId = streamingId;
@ -117,7 +123,7 @@
},
onComplete: () => {
isSubmitting = false;
chatArea.onFinishedSubmitting();
chatArea.scrollChatArea();
chatService.updateTokenDisplay(conversation);
}
});
@ -141,17 +147,6 @@
}
}
function scrollToBottom() {
tick().then(() => {
if (chatContainer) {
chatContainer.scroll({
top: chatContainer.scrollHeight,
behavior: 'smooth'
});
}
});
}
$: if (submitButton) {
setIcon(submitButton, isSubmitting ? "square" : "send-horizontal");
}
@ -167,13 +162,20 @@
}
$: if ($conversationStore.conversationToLoad) {
const { conversation: loadedConversation, filePath } = $conversationStore.conversationToLoad;
conversation = loadedConversation;
conversationService.setCurrentConversationPath(filePath);
chatService.onNameChanged?.(loadedConversation.title);
chatService.updateTokenDisplay(loadedConversation);
conversationStore.clearLoadFlag();
scrollToBottom();
conversation.contents = [];
chatArea.resetChatArea();
tick().then(() => {
if ($conversationStore.conversationToLoad) {
const { conversation: loadedConversation, filePath } = $conversationStore.conversationToLoad;
conversation = loadedConversation;
conversationService.setCurrentConversationPath(filePath);
chatService.onNameChanged?.(loadedConversation.title);
chatService.updateTokenDisplay(loadedConversation);
conversationStore.clearLoadFlag();
chatArea.scrollChatArea();
}
});
}
$: if ($conversationStore.shouldDeactivateEditMode) {

View file

@ -14,6 +14,7 @@ import { Role } from "Enums/Role";
import type { AIFunctionCall } from "AIClasses/AIFunctionCall";
export interface ChatServiceCallbacks {
onSubmit: () => void;
onStreamingUpdate: (streamingMessageId: string | null) => void;
onThoughtUpdate: (thought: string | null) => void;
onComplete: () => void;
@ -64,6 +65,8 @@ export class ChatService {
conversation.contents.push(new ConversationContent(Role.User, userRequest));
this.conversationService.saveConversation(conversation);
callbacks.onSubmit();
callbacks.onStreamingUpdate(null);
if (conversation.contents.length === 1) {
@ -189,6 +192,8 @@ export class ChatService {
callbacks.onStreamingUpdate(aiMessage.timestamp.getTime().toString());
}
callbacks.onStreamingUpdate(null);
return { functionCall: capturedFunctionCall, shouldContinue: capturedShouldContinue };
}
}

View file

@ -3,13 +3,17 @@ import { Services } from "./Services";
import type { IConversationNamingService } from "AIClasses/IConversationNamingService";
import type { ConversationFileSystemService } from "./ConversationFileSystemService";
import type { Conversation } from "Conversations/Conversation";
import type { VaultService } from "./VaultService";
import { Path } from "Enums/Path";
export class ConversationNamingService {
private namingProvider: IConversationNamingService | undefined;
private conversationService: ConversationFileSystemService;
private vaultService: VaultService;
constructor() {
this.conversationService = Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService);
this.vaultService = Resolve<VaultService>(Services.VaultService);
}
public resolveNamingProvider() {
@ -49,7 +53,14 @@ export class ConversationNamingService {
}
private validateName(generatedName: string): string {
const cleanedTitle = generatedName.trim().replace(/^["']|["']$/g, "");
return cleanedTitle.split(/\s+/).slice(0, 6).join(" ");
let cleanedTitle = generatedName.trim().replace(/^["']|["']$/g, "").split(/\s+/).slice(0, 6).join(" ");
let index = 1;
let availableTitle = cleanedTitle;
while (this.vaultService.exists(`${Path.Conversations}/${availableTitle}.json`, true)) {
availableTitle = `${cleanedTitle}(${index})`;
index++;
}
return availableTitle;
}
}

View file

@ -0,0 +1,130 @@
export interface SanitizeOptions {
replacement?: string;
separator?: string;
}
export class SanitiserService {
// Regular expressions for different character classes
private readonly illegalRe = /[\?<>\\:\*\|"]/g;
private readonly controlRe = /[\x00-\x1f\x80-\x9f]/g;
private readonly reservedRe = /^\.+$/;
private readonly windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
private readonly windowsTrailingRe = /[\. ]+$/;
constructor() {}
/**
* Sanitizes a file path with directories, removing illegal characters and ensuring cross-platform compatibility
* @param input - The file path to sanitize
* @param options - Optional configuration for replacement character and output separator
* @returns Sanitized file path
*/
public sanitize(input: string, options: SanitizeOptions = {}): string {
// Type check
if (typeof input !== 'string') {
throw new Error('Input must be a string');
}
// Default options
const replacement = options.replacement || '';
const outputSeparator = options.separator || '/';
// Detect if this is an absolute path
const isAbsolute = input.startsWith('/') || /^[a-zA-Z]:[\\\/]/.test(input);
// Detect Windows drive letter (e.g., C:)
const driveMatch = input.match(/^([a-zA-Z]:)[\\\/]/);
const driveLetter = driveMatch ? driveMatch[1] : '';
// Split by both forward and back slashes
// This handles mixed separators like: /home/user\docs/file.txt
let segments = input.split(/[\\\/]+/);
// Remove empty segments (from leading/trailing slashes or multiple consecutive slashes)
// But keep track of whether we started with a slash
segments = segments.filter((seg, index) => {
// Keep the first segment even if empty (for absolute paths like /home/...)
if (index === 0 && seg === '' && isAbsolute && !driveLetter) {
return true;
}
return seg !== '';
});
// Sanitize each segment
const sanitizedSegments = segments.map((segment, index) => {
// Don't sanitize the drive letter (first segment if it's something like "C:")
if (index === 0 && driveLetter && segment === driveLetter.replace(':', '')) {
return driveLetter;
}
// For the first empty segment of an absolute path, keep it empty
if (index === 0 && segment === '' && isAbsolute) {
return '';
}
return this.sanitizeSegment(segment, replacement);
});
// Rejoin with the desired separator
let result = sanitizedSegments.join(outputSeparator);
// For absolute Unix paths, ensure leading slash
if (isAbsolute && !driveLetter && !result.startsWith(outputSeparator)) {
result = outputSeparator + result;
}
// Truncate the entire path if needed (paths can be up to 4096 bytes on most systems)
// But for individual filenames, most systems have a 255-byte limit
// We'll apply the 255-byte limit to the filename part only
const lastSeparatorIndex = result.lastIndexOf(outputSeparator);
if (lastSeparatorIndex !== -1) {
const directory = result.substring(0, lastSeparatorIndex + 1);
const filename = result.substring(lastSeparatorIndex + 1);
const truncatedFilename = this.truncateToByteLength(filename, 255);
result = directory + truncatedFilename;
} else {
// If there's no separator, the whole thing is a filename
result = this.truncateToByteLength(result, 255);
}
return result;
}
/**
* Sanitizes a single path segment (filename or directory name)
*/
private sanitizeSegment(segment: string, replacement: string): string {
if (!segment || segment === '') {
return segment;
}
let sanitized = segment
.replace(this.illegalRe, replacement)
.replace(this.controlRe, replacement)
.replace(this.reservedRe, replacement)
.replace(this.windowsReservedRe, replacement)
.replace(this.windowsTrailingRe, replacement);
return sanitized;
}
/**
* Truncates a string to a maximum byte length while preserving UTF-8 character integrity
* @param str - String to truncate
* @param maxBytes - Maximum byte length
* @returns Truncated string
*/
private truncateToByteLength(str: string, maxBytes: number): string {
const encoder = new TextEncoder();
const decoder = new TextDecoder('utf-8');
const encoded = encoder.encode(str);
if (encoded.length <= maxBytes) {
return str;
}
const truncated = encoded.slice(0, maxBytes);
return decoder.decode(truncated);
}
}

View file

@ -28,11 +28,13 @@ import { ClaudeConversationNamingService } from "AIClasses/Claude/ClaudeConversa
import { Claude } from "AIClasses/Claude/Claude";
import { OpenAIConversationNamingService } from "AIClasses/OpenAI/OpenAIConversationNamingService";
import { OpenAI } from "AIClasses/OpenAI/OpenAI";
import { SanitiserService } from "./SanitiserService";
export function RegisterDependencies(plugin: AIAgentPlugin) {
RegisterSingleton<AIAgentPlugin>(Services.AIAgentPlugin, plugin);
RegisterSingleton<FileManager>(Services.FileManager, plugin.app.fileManager);
RegisterSingleton<StatusBarService>(Services.StatusBarService, new StatusBarService());
RegisterSingleton<SanitiserService>(Services.SanitiserService, new SanitiserService());
RegisterSingleton<VaultService>(Services.VaultService, new VaultService());
RegisterSingleton<WorkSpaceService>(Services.WorkSpaceService, new WorkSpaceService());
RegisterSingleton<FileSystemService>(Services.FileSystemService, new FileSystemService());

View file

@ -13,6 +13,7 @@ export class Services {
static AIFunctionDefinitions = Symbol("AIFunctionDefinitions");
static AIFunctionService = Symbol("AIFunctionService");
static ChatService = Symbol("ChatService");
static SanitiserService = Symbol("SanitiserService");
// interfaces
static IAIClass = Symbol("IAIClass");

View file

@ -5,6 +5,7 @@ import type AIAgentPlugin from "main";
import { Path } from "Enums/Path";
import { escapeRegex, randomSample } from "Helpers/Helpers";
import type { SearchMatch, SearchSnippet } from "../Helpers/SearchTypes";
import type { SanitiserService } from "./SanitiserService";
/* This service protects the users vault through their exclusions. The plugin root is excluded by default */
export class VaultService {
@ -15,11 +16,13 @@ export class VaultService {
private readonly plugin: AIAgentPlugin;
private readonly fileManager: FileManager;
private readonly vault: Vault;
private readonly sanitiserService: SanitiserService;
public constructor() {
this.plugin = Resolve<AIAgentPlugin>(Services.AIAgentPlugin);
this.fileManager = Resolve<FileManager>(Services.FileManager);
this.vault = this.plugin.app.vault;
this.sanitiserService = Resolve<SanitiserService>(Services.SanitiserService);
}
public getMarkdownFiles(allowAccessToPluginRoot: boolean = false): TFile[] {
@ -27,6 +30,8 @@ export class VaultService {
}
public getAbstractFileByPath(filePath: string, allowAccessToPluginRoot: boolean = false): TAbstractFile | null {
filePath = this.sanitiserService.sanitize(filePath);
if (this.isExclusion(filePath, allowAccessToPluginRoot)) {
console.error(`Plugin attempted to retrieve a file that is in the exclusions list: ${filePath}`);
return null;
@ -34,6 +39,14 @@ export class VaultService {
return this.vault.getAbstractFileByPath(filePath);
}
public exists(filePath: string, allowAccessToPluginRoot: boolean = false): boolean {
if (this.isExclusion(filePath, allowAccessToPluginRoot)) {
console.error(`Plugin attempted to access a file that is in the exclusions list: ${filePath}`);
return false;
}
return this.vault.getAbstractFileByPath(filePath) instanceof TFile;
}
public async read(file: TFile, allowAccessToPluginRoot: boolean = false): Promise<string> {
if (this.isExclusion(file.path, allowAccessToPluginRoot)) {
console.error(`Plugin attempted to read a file that is in the exclusions list: ${file.path}`);
@ -43,6 +56,8 @@ export class VaultService {
}
public async create(filePath: string, content: string, allowAccessToPluginRoot: boolean = false): Promise<TFile> {
filePath = this.sanitiserService.sanitize(filePath);
if (this.isExclusion(filePath, allowAccessToPluginRoot)) {
throw new Error(`Plugin attempted to create a file that is in the exclusion list: ${filePath}`);
}
@ -73,6 +88,9 @@ export class VaultService {
}
public async move(sourcePath: string, destinationPath: string, allowAccessToPluginRoot: boolean = false): Promise<{ success: true } | { success: false, error: string }> {
sourcePath = this.sanitiserService.sanitize(sourcePath);
destinationPath = this.sanitiserService.sanitize(destinationPath);
if (this.isExclusion(sourcePath, allowAccessToPluginRoot)) {
console.error(`Plugin attempted to move a file that is in the exclusions list: ${sourcePath}`)
return { success: false, error: "Source file is in exclusion list" };
@ -94,6 +112,8 @@ export class VaultService {
}
public async createFolder(path: string, allowAccessToPluginRoot: boolean = false): Promise<TFolder> {
path = this.sanitiserService.sanitize(path);
if (this.isExclusion(path, allowAccessToPluginRoot)) {
throw new Error(`Plugin attempted to create a folder that is in the exclusion list: ${path}`);
}
@ -101,7 +121,7 @@ export class VaultService {
}
public async listFilesInDirectory(dirPath: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise<TFile[]> {
const dir: TAbstractFile | null = this.getAbstractFileByPath(dirPath, true);
const dir: TAbstractFile | null = this.getAbstractFileByPath(this.sanitiserService.sanitize(dirPath), true);
if (dir == null || !(dir instanceof TFolder)) {
return [];
@ -177,7 +197,7 @@ export class VaultService {
for (const file of files) {
if (file.basename.match(regex) &&
!results.some(result => result.file.basename === file.basename)) {
results.push({ file, snippets: [] });
results.push({ file, snippets: [] });
}
}

View file

@ -40,7 +40,10 @@ export class MainView extends ItemView {
target: container,
props: {
leaf: this.leaf,
onNewConversation: () => this.input?.focusInput()
onNewConversation: () => {
this.input?.resetChatArea();
this.input?.focusInput();
}
}
});