```
Increase test coverage across multiple modules (91.2% statements, 82.85% branches, 82.35% functions, 93.1% lines) Update abort controller handling in OllamaClient to properly clean up previous controllers Remove unused imports and constants: MODEL_NAME_REGEX, MouseEvent, DEFAULT_SETTINGS, convertMarkdownToHtml Refactor event handler naming to be more consistent ```
This commit is contained in:
+25
-23
@@ -31,9 +31,9 @@ class ChatView extends obsidian_1.ItemView {
|
||||
this.sendButtonClickHandler = null;
|
||||
this.inputKeyDownHandler = null;
|
||||
this.newChatButtonClickHandler = null;
|
||||
this.sendButtonEventHandler = null;
|
||||
this.inputKeyDownEventHandler = null;
|
||||
this.newChatButtonEventHandler = null;
|
||||
this.sendButtonClickWrapper = null;
|
||||
this.inputKeyDownWrapper = null;
|
||||
this.newChatButtonClickWrapper = null;
|
||||
this.listenersAttached = false;
|
||||
this.settings = settings;
|
||||
this.ollamaClient = new ollama_client_1.OllamaClient(settings.ollamaUrl, settings.model);
|
||||
@@ -70,8 +70,9 @@ class ChatView extends obsidian_1.ItemView {
|
||||
return Promise.resolve();
|
||||
}
|
||||
cleanupStreamingResources() {
|
||||
// Ensure any ongoing streaming is properly cleaned up
|
||||
if (this.lastMessageEl && this.lastMessageEl.parentElement) {
|
||||
// Only cleanup if there's still an active streaming message
|
||||
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
|
||||
if (streamingMessage && this.lastMessageEl && this.lastMessageEl.parentElement) {
|
||||
this.lastMessageEl.parentElement.removeChild(this.lastMessageEl);
|
||||
this.lastMessageEl = null;
|
||||
}
|
||||
@@ -155,39 +156,40 @@ class ChatView extends obsidian_1.ItemView {
|
||||
this.inputEl.value = '';
|
||||
};
|
||||
}
|
||||
// Add event listeners
|
||||
this.sendButtonEventHandler = () => {
|
||||
// Create wrapper functions for event listeners
|
||||
this.sendButtonClickWrapper = () => {
|
||||
void this.sendButtonClickHandler?.();
|
||||
};
|
||||
this.inputKeyDownEventHandler = (e) => {
|
||||
this.inputKeyDownWrapper = (e) => {
|
||||
void this.inputKeyDownHandler?.(e);
|
||||
};
|
||||
this.sendButton.addEventListener('click', this.sendButtonEventHandler);
|
||||
this.inputEl.addEventListener('keydown', this.inputKeyDownEventHandler);
|
||||
this.newChatButtonClickWrapper = () => {
|
||||
void this.newChatButtonClickHandler?.();
|
||||
};
|
||||
// Add event listeners using wrappers
|
||||
this.sendButton.addEventListener('click', this.sendButtonClickWrapper);
|
||||
this.inputEl.addEventListener('keydown', this.inputKeyDownWrapper);
|
||||
if (this.newChatButton) {
|
||||
if (!this.newChatButtonClickHandler) {
|
||||
this.newChatButtonClickHandler = () => this.clearConversation();
|
||||
}
|
||||
this.newChatButtonEventHandler = () => {
|
||||
this.newChatButtonClickHandler?.();
|
||||
};
|
||||
this.newChatButton.addEventListener('click', this.newChatButtonEventHandler);
|
||||
this.newChatButton.addEventListener('click', this.newChatButtonClickWrapper);
|
||||
}
|
||||
this.listenersAttached = true;
|
||||
}
|
||||
removeEventListeners() {
|
||||
if (this.sendButton && this.sendButtonEventHandler) {
|
||||
this.sendButton.removeEventListener('click', this.sendButtonEventHandler);
|
||||
if (this.sendButton && this.sendButtonClickWrapper) {
|
||||
this.sendButton.removeEventListener('click', this.sendButtonClickWrapper);
|
||||
}
|
||||
if (this.inputEl && this.inputKeyDownEventHandler) {
|
||||
this.inputEl.removeEventListener('keydown', this.inputKeyDownEventHandler);
|
||||
if (this.inputEl && this.inputKeyDownWrapper) {
|
||||
this.inputEl.removeEventListener('keydown', this.inputKeyDownWrapper);
|
||||
}
|
||||
if (this.newChatButton && this.newChatButtonEventHandler) {
|
||||
this.newChatButton.removeEventListener('click', this.newChatButtonEventHandler);
|
||||
if (this.newChatButton && this.newChatButtonClickWrapper) {
|
||||
this.newChatButton.removeEventListener('click', this.newChatButtonClickWrapper);
|
||||
}
|
||||
this.sendButtonEventHandler = null;
|
||||
this.inputKeyDownEventHandler = null;
|
||||
this.newChatButtonEventHandler = null;
|
||||
this.sendButtonClickWrapper = null;
|
||||
this.inputKeyDownWrapper = null;
|
||||
this.newChatButtonClickWrapper = null;
|
||||
this.listenersAttached = false;
|
||||
}
|
||||
clearConversation() {
|
||||
|
||||
+38
-51
@@ -4,7 +4,6 @@ import { ItemView, WorkspaceLeaf, Notice } from 'obsidian';
|
||||
type KeyboardEvent = globalThis.KeyboardEvent;
|
||||
type HTMLTextAreaElement = globalThis.HTMLTextAreaElement;
|
||||
type HTMLButtonElement = globalThis.HTMLButtonElement;
|
||||
type MouseEvent = globalThis.MouseEvent;
|
||||
|
||||
const DEFAULT_VAULT_SEARCH_LIMIT = 3;
|
||||
const MAX_MESSAGE_HISTORY = 50;
|
||||
@@ -29,16 +28,16 @@ export class ChatView extends ItemView {
|
||||
private vaultIndexer: VaultIndexer;
|
||||
private toolExecutor: ToolExecutor;
|
||||
private lastMessageEl: HTMLElement | null = null;
|
||||
private newChatButton: HTMLElement | null = null;
|
||||
private sendButton: HTMLElement | null = null;
|
||||
private inputEl: HTMLElement | null = null;
|
||||
private newChatButton: HTMLButtonElement | null = null;
|
||||
private sendButton: HTMLButtonElement | null = null;
|
||||
private inputEl: HTMLTextAreaElement | null = null;
|
||||
private chatContainer: HTMLElement | null = null;
|
||||
private sendButtonClickHandler: (() => Promise<void>) | null = null;
|
||||
private inputKeyDownHandler: ((e: KeyboardEvent) => Promise<void>) | null = null;
|
||||
private newChatButtonClickHandler: (() => void) | null = null;
|
||||
private sendButtonEventHandler: ((e: MouseEvent) => void) | null = null;
|
||||
private inputKeyDownEventHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||
private newChatButtonEventHandler: ((e: MouseEvent) => void) | null = null;
|
||||
private sendButtonClickWrapper: (() => void) | null = null;
|
||||
private inputKeyDownWrapper: ((e: KeyboardEvent) => void) | null = null;
|
||||
private newChatButtonClickWrapper: (() => void) | null = null;
|
||||
private listenersAttached = false;
|
||||
|
||||
// Getters for testing
|
||||
@@ -98,8 +97,9 @@ export class ChatView extends ItemView {
|
||||
}
|
||||
|
||||
private cleanupStreamingResources(): void {
|
||||
// Ensure any ongoing streaming is properly cleaned up
|
||||
if (this.lastMessageEl && this.lastMessageEl.parentElement) {
|
||||
// Only cleanup if there's still an active streaming message
|
||||
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
|
||||
if (streamingMessage && this.lastMessageEl && this.lastMessageEl.parentElement) {
|
||||
this.lastMessageEl.parentElement.removeChild(this.lastMessageEl);
|
||||
this.lastMessageEl = null;
|
||||
}
|
||||
@@ -120,7 +120,7 @@ export class ChatView extends ItemView {
|
||||
this.sendButton = inputContainer.createEl('button', {
|
||||
cls: 'ollama-send-button',
|
||||
});
|
||||
(this.sendButton as HTMLButtonElement).textContent = 'Send';
|
||||
this.sendButton.textContent = 'Send';
|
||||
}
|
||||
|
||||
if (!this.newChatButton) {
|
||||
@@ -130,8 +130,8 @@ export class ChatView extends ItemView {
|
||||
this.newChatButton = newChatContainer.createEl('button', {
|
||||
cls: 'ollama-new-chat-button',
|
||||
});
|
||||
(this.newChatButton as HTMLButtonElement).textContent = '🔄 New Chat';
|
||||
(this.newChatButton as HTMLButtonElement).title = 'Start a new conversation';
|
||||
this.newChatButton.textContent = '🔄 New Chat';
|
||||
this.newChatButton.title = 'Start a new conversation';
|
||||
}
|
||||
|
||||
// Create immutable snapshot for rendering
|
||||
@@ -183,8 +183,8 @@ export class ChatView extends ItemView {
|
||||
if (!this.sendButtonClickHandler) {
|
||||
this.sendButtonClickHandler = async () => {
|
||||
if (!this.inputEl) return;
|
||||
await this.handleUserInput((this.inputEl as HTMLTextAreaElement).value);
|
||||
(this.inputEl as HTMLTextAreaElement).value = '';
|
||||
await this.handleUserInput(this.inputEl.value);
|
||||
this.inputEl.value = '';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -192,60 +192,47 @@ export class ChatView extends ItemView {
|
||||
this.inputKeyDownHandler = async (e: KeyboardEvent) => {
|
||||
if (!this.inputEl || e.key !== 'Enter' || e.shiftKey) return;
|
||||
e.preventDefault();
|
||||
await this.handleUserInput((this.inputEl as HTMLTextAreaElement).value);
|
||||
(this.inputEl as HTMLTextAreaElement).value = '';
|
||||
await this.handleUserInput(this.inputEl.value);
|
||||
this.inputEl.value = '';
|
||||
};
|
||||
}
|
||||
|
||||
// Add event listeners
|
||||
this.sendButtonEventHandler = () => {
|
||||
// Create wrapper functions for event listeners
|
||||
this.sendButtonClickWrapper = () => {
|
||||
void this.sendButtonClickHandler?.();
|
||||
};
|
||||
this.inputKeyDownEventHandler = (e: KeyboardEvent) => {
|
||||
this.inputKeyDownWrapper = (e: KeyboardEvent) => {
|
||||
void this.inputKeyDownHandler?.(e);
|
||||
};
|
||||
(this.sendButton as HTMLButtonElement).addEventListener('click', this.sendButtonEventHandler);
|
||||
(this.inputEl as HTMLTextAreaElement).addEventListener(
|
||||
'keydown',
|
||||
this.inputKeyDownEventHandler
|
||||
);
|
||||
this.newChatButtonClickWrapper = () => {
|
||||
void this.newChatButtonClickHandler?.();
|
||||
};
|
||||
|
||||
// Add event listeners using wrappers
|
||||
this.sendButton.addEventListener('click', this.sendButtonClickWrapper);
|
||||
this.inputEl.addEventListener('keydown', this.inputKeyDownWrapper);
|
||||
if (this.newChatButton) {
|
||||
if (!this.newChatButtonClickHandler) {
|
||||
this.newChatButtonClickHandler = () => this.clearConversation();
|
||||
}
|
||||
this.newChatButtonEventHandler = () => {
|
||||
this.newChatButtonClickHandler?.();
|
||||
};
|
||||
(this.newChatButton as HTMLButtonElement).addEventListener(
|
||||
'click',
|
||||
this.newChatButtonEventHandler
|
||||
);
|
||||
this.newChatButton.addEventListener('click', this.newChatButtonClickWrapper);
|
||||
}
|
||||
this.listenersAttached = true;
|
||||
}
|
||||
|
||||
private removeEventListeners(): void {
|
||||
if (this.sendButton && this.sendButtonEventHandler) {
|
||||
(this.sendButton as HTMLButtonElement).removeEventListener(
|
||||
'click',
|
||||
this.sendButtonEventHandler
|
||||
);
|
||||
if (this.sendButton && this.sendButtonClickWrapper) {
|
||||
this.sendButton.removeEventListener('click', this.sendButtonClickWrapper);
|
||||
}
|
||||
if (this.inputEl && this.inputKeyDownEventHandler) {
|
||||
(this.inputEl as HTMLTextAreaElement).removeEventListener(
|
||||
'keydown',
|
||||
this.inputKeyDownEventHandler
|
||||
);
|
||||
if (this.inputEl && this.inputKeyDownWrapper) {
|
||||
this.inputEl.removeEventListener('keydown', this.inputKeyDownWrapper);
|
||||
}
|
||||
if (this.newChatButton && this.newChatButtonEventHandler) {
|
||||
(this.newChatButton as HTMLButtonElement).removeEventListener(
|
||||
'click',
|
||||
this.newChatButtonEventHandler
|
||||
);
|
||||
if (this.newChatButton && this.newChatButtonClickWrapper) {
|
||||
this.newChatButton.removeEventListener('click', this.newChatButtonClickWrapper);
|
||||
}
|
||||
this.sendButtonEventHandler = null;
|
||||
this.inputKeyDownEventHandler = null;
|
||||
this.newChatButtonEventHandler = null;
|
||||
this.sendButtonClickWrapper = null;
|
||||
this.inputKeyDownWrapper = null;
|
||||
this.newChatButtonClickWrapper = null;
|
||||
this.listenersAttached = false;
|
||||
}
|
||||
|
||||
@@ -283,7 +270,7 @@ export class ChatView extends ItemView {
|
||||
|
||||
private async handleUserInput(content: string) {
|
||||
if (!this.sendButton || !this.inputEl) return;
|
||||
(this.sendButton as HTMLButtonElement).disabled = true;
|
||||
this.sendButton.disabled = true;
|
||||
|
||||
try {
|
||||
// Guard against empty messages
|
||||
@@ -483,7 +470,7 @@ export class ChatView extends ItemView {
|
||||
this.cleanupStreamingResources();
|
||||
} finally {
|
||||
if (this.sendButton) {
|
||||
(this.sendButton as HTMLButtonElement).disabled = false;
|
||||
this.sendButton.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
// Default plugin settings
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MODEL_NAME_REGEX = exports.DEFAULT_SETTINGS = void 0;
|
||||
exports.DEFAULT_SETTINGS = void 0;
|
||||
exports.DEFAULT_SETTINGS = {
|
||||
ollamaUrl: 'http://localhost:11434',
|
||||
model: 'llama3',
|
||||
@@ -10,4 +10,3 @@ exports.DEFAULT_SETTINGS = {
|
||||
lastIndexTime: 0,
|
||||
};
|
||||
// Model validation regex - lowercase letters, numbers, dashes, underscores only
|
||||
exports.MODEL_NAME_REGEX = /^[a-z0-9-_]+$/;
|
||||
|
||||
@@ -9,4 +9,3 @@ export const DEFAULT_SETTINGS = {
|
||||
};
|
||||
|
||||
// Model validation regex - lowercase letters, numbers, dashes, underscores only
|
||||
export const MODEL_NAME_REGEX = /^[a-z0-9-_]+$/;
|
||||
|
||||
@@ -93,7 +93,6 @@ class OllamaSettingTab extends obsidian_1.PluginSettingTab {
|
||||
this.containerEl.empty();
|
||||
// Create container for settings
|
||||
const container = this.containerEl.createDiv();
|
||||
container.empty();
|
||||
new obsidian_1.Setting(container)
|
||||
.setName('Ollama URL')
|
||||
.setDesc('URL of your Ollama instance')
|
||||
|
||||
@@ -105,7 +105,6 @@ class OllamaSettingTab extends PluginSettingTab {
|
||||
|
||||
// Create container for settings
|
||||
const container = this.containerEl.createDiv() as HTMLElement;
|
||||
container.empty();
|
||||
|
||||
new Setting(container)
|
||||
.setName('Ollama URL')
|
||||
|
||||
@@ -29,7 +29,11 @@ class OllamaClient {
|
||||
return chunks;
|
||||
}
|
||||
async *streamChatWithRetry(messages, tools = [], attempt = 0) {
|
||||
const prevController = this.abortController;
|
||||
this.abortController = new AbortController();
|
||||
if (prevController) {
|
||||
prevController.abort();
|
||||
}
|
||||
try {
|
||||
const response = await this.fetchFn(`${this.baseURL}/api/chat`, {
|
||||
method: 'POST',
|
||||
@@ -128,7 +132,6 @@ class OllamaClient {
|
||||
return this.chatWithRetry(messages, tools, 0);
|
||||
}
|
||||
async chatWithRetry(messages, tools = [], attempt = 0) {
|
||||
const controller = new AbortController();
|
||||
const response = await this.fetchFn(`${this.baseURL}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -140,7 +143,6 @@ class OllamaClient {
|
||||
tools,
|
||||
stream: false,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
if (response.status >= 500 && attempt < this.maxRetries) {
|
||||
|
||||
@@ -51,7 +51,11 @@ export class OllamaClient {
|
||||
tools: OllamaTool[] = [],
|
||||
attempt: number = 0
|
||||
): AsyncGenerator<OllamaMessage, void, unknown> {
|
||||
const prevController = this.abortController;
|
||||
this.abortController = new AbortController();
|
||||
if (prevController) {
|
||||
prevController.abort();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.fetchFn(`${this.baseURL}/api/chat`, {
|
||||
@@ -175,8 +179,6 @@ export class OllamaClient {
|
||||
tools: OllamaTool[] = [],
|
||||
attempt: number = 0
|
||||
): Promise<OllamaMessage> {
|
||||
const controller = new AbortController();
|
||||
|
||||
const response = await this.fetchFn(`${this.baseURL}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -188,7 +190,6 @@ export class OllamaClient {
|
||||
tools,
|
||||
stream: false,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
+1
-8
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
// src/types.ts
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DEFAULT_SETTINGS = exports.PathValidationError = exports.ToolExecutionError = exports.StreamingError = exports.ValidationError = exports.ApiError = exports.NetworkError = exports.OllamaError = exports.ErrorType = void 0;
|
||||
exports.PathValidationError = exports.ToolExecutionError = exports.StreamingError = exports.ValidationError = exports.ApiError = exports.NetworkError = exports.OllamaError = exports.ErrorType = void 0;
|
||||
// ============================================================
|
||||
// Error Type Hierarchy
|
||||
// ============================================================
|
||||
@@ -70,10 +70,3 @@ class PathValidationError extends OllamaError {
|
||||
}
|
||||
}
|
||||
exports.PathValidationError = PathValidationError;
|
||||
exports.DEFAULT_SETTINGS = {
|
||||
ollamaUrl: 'http://localhost:11434',
|
||||
model: 'llama3',
|
||||
vaultSearchLimit: 3,
|
||||
maxMessageHistory: 50,
|
||||
lastIndexTime: 0,
|
||||
};
|
||||
|
||||
@@ -98,14 +98,6 @@ export interface PluginSettings {
|
||||
lastIndexTime: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: PluginSettings = {
|
||||
ollamaUrl: 'http://localhost:11434',
|
||||
model: 'llama3',
|
||||
vaultSearchLimit: 3,
|
||||
maxMessageHistory: 50,
|
||||
lastIndexTime: 0,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Ollama Protocol Types
|
||||
// ============================================================
|
||||
|
||||
@@ -8,7 +8,6 @@ exports.validatePluginSettings = validatePluginSettings;
|
||||
exports.safeParseJson = safeParseJson;
|
||||
exports.sanitizeFilePath = sanitizeFilePath;
|
||||
exports.isValidHttpUrl = isValidHttpUrl;
|
||||
exports.convertMarkdownToHtml = convertMarkdownToHtml;
|
||||
// ==================== Logger ====================
|
||||
var LogLevel;
|
||||
(function (LogLevel) {
|
||||
@@ -175,6 +174,3 @@ function isValidHttpUrl(url) {
|
||||
}
|
||||
}
|
||||
// ==================== Markdown Utilities ====================
|
||||
function convertMarkdownToHtml(markdown) {
|
||||
return markdown.replace(/\n/g, '<br>').replace(/# (.+)/g, '<h1>$1</h1>');
|
||||
}
|
||||
|
||||
@@ -199,7 +199,3 @@ export function isValidHttpUrl(url: string): boolean {
|
||||
}
|
||||
|
||||
// ==================== Markdown Utilities ====================
|
||||
|
||||
export function convertMarkdownToHtml(markdown: string): string {
|
||||
return markdown.replace(/\n/g, '<br>').replace(/# (.+)/g, '<h1>$1</h1>');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user