Add basic message history

This commit is contained in:
Barrca 2025-05-13 21:28:40 +02:00
parent 13d0a1ebf9
commit 031efb3ae4
3 changed files with 168 additions and 22 deletions

View file

@ -8,6 +8,14 @@ import { App, MarkdownView, Notice } from "obsidian";
import { EditorView } from "@codemirror/view";
import { setGeneratedResponseEffect } from "./modules/AIExtension";
import { parseCommand } from "./modules/commands/parser";
import { MessageQueue } from "./modules/messageHistory/queue";
const MESSAGE_HISTORY_LIMIT = 20;
export type HistoryMessage = {
mode: string;
userPrompt: string;
};
/**
* Class to manage interactions with different chat APIs.
@ -16,7 +24,7 @@ export class ChatApiManager {
private chatClient: ChatOpenAI | ChatOllama | ChatGoogleGenerativeAI | null;
private app: App;
private settings: InlineAISettings;
private messageHistory: MessageQueue<HistoryMessage>;
/**
* Initializes the ChatApiManager with the given settings.
* @param settings - Configuration settings for the chat API.
@ -26,6 +34,7 @@ export class ChatApiManager {
this.app = app;
this.chatClient = this.initializeChatClient(settings);
this.settings = settings;
this.messageHistory = new MessageQueue<HistoryMessage>(MESSAGE_HISTORY_LIMIT);
}
/**
@ -138,25 +147,14 @@ export class ChatApiManager {
return "⚠️ Failed to process request.";
}
}
/**
* Handles user input and generates a response using the cursor API.
* @param userRequest - The user's request to process.
* @returns The AI-generated response or an error message.
*/
public async callCursor(userRequest: string): Promise<string> {
const systemPrompt = "You are a helpful assistant. Please help the user with the following request:";
return this.handleEditorUpdate(systemPrompt, userRequest);
}
/**
/**
* Processes selected text using the specified prompt and transformation.
* @param prompt - The transformation prompt (e.g., "Add Emojis").
* @param userPrompt - The transformation prompt (e.g., "Add Emojis").
* @param selectedText - The selected text to transform.
* @returns The transformed text or an error message.
*/
public async callSelection(prompt: string, selectedText: string): Promise<string> {
prompt = parseCommand(prompt, this.settings.commandPrefix, this.settings.customCommands);
public async callSelection(userPrompt: string, selectedText: string): Promise<string> {
userPrompt = parseCommand(userPrompt, this.settings.commandPrefix, this.settings.customCommands);
let isCursor = false;
if (selectedText.trim().length === 0) {
@ -164,21 +162,23 @@ export class ChatApiManager {
}
const systemPrompt = isCursor ? this.settings.cursorPrompt : this.settings.selectionPrompt;
let userPrompt = ``;
let finalUserPrompt = ``;
const mode = isCursor ? "cursor" : "selection";
this.messageHistory.enqueue({ mode, userPrompt });
if (isCursor) {
userPrompt = `
**Task:** ${prompt}
finalUserPrompt = `
**Task:** ${userPrompt}
**Output:**`;
} else {
userPrompt = `
**Task:** ${prompt}
finalUserPrompt = `
**Task:** ${userPrompt}
**Input:**
${selectedText}
**Output:**`;
}
return this.handleEditorUpdate(systemPrompt, userPrompt);
return this.handleEditorUpdate(systemPrompt, finalUserPrompt);
}
/**
@ -193,4 +193,8 @@ export class ChatApiManager {
}
this.chatClient = newChatClient;
}
public getMessageHistory(): HistoryMessage[] {
return this.messageHistory.getItems();
}
}

View file

@ -47,6 +47,9 @@ class FloatingWidget extends WidgetType {
private acceptButton!: HTMLButtonElement;
private discardButton!: HTMLButtonElement;
// Track the current index in message history for up/down navigation
private messageHistoryIndex: number | null = null;
constructor(chatApiManager: ChatApiManager, selectionInfo: SelectionInfo | null, plugin: InlineAIChatPlugin) {
super();
this.chatApiManager = chatApiManager;
@ -111,6 +114,7 @@ class FloatingWidget extends WidgetType {
this.textFieldView = undefined;
this.outerEditorView = null;
this.messageHistoryIndex = null;
}
private dismissTooltip() {
@ -159,7 +163,81 @@ class FloatingWidget extends WidgetType {
},
preventDefault: true,
},
{
key: "ArrowUp",
run: () => {
const messageHistory = this.chatApiManager.getMessageHistory();
if (messageHistory.length === 0 || !this.textFieldView) {
return true;
}
// If not started, set to last index
if (this.messageHistoryIndex === null) {
this.messageHistoryIndex = messageHistory.length - 1;
} else if (this.messageHistoryIndex > 0) {
this.messageHistoryIndex--;
}
// Clamp to 0
if (this.messageHistoryIndex < 0) this.messageHistoryIndex = 0;
const msg = messageHistory[this.messageHistoryIndex].userPrompt;
this.textFieldView.dispatch({
changes: {
from: 0,
to: this.textFieldView.state.doc.length,
insert: msg,
}
});
// Move cursor to end
this.textFieldView.dispatch({
selection: { anchor: msg.length, head: msg.length }
});
return true;
},
preventDefault: true,
},
{
key: "ArrowDown",
run: () => {
const messageHistory = this.chatApiManager.getMessageHistory();
if (messageHistory.length === 0 || !this.textFieldView) {
return true;
}
if (this.messageHistoryIndex === null) {
// Do nothing if not in history navigation
return true;
}
if (this.messageHistoryIndex < messageHistory.length - 1) {
this.messageHistoryIndex++;
const msg = messageHistory[this.messageHistoryIndex].userPrompt;
this.textFieldView.dispatch({
changes: {
from: 0,
to: this.textFieldView.state.doc.length,
insert: msg,
}
});
// Move cursor to end
this.textFieldView.dispatch({
selection: { anchor: msg.length, head: msg.length }
});
} else {
// If at the end, clear the field and exit history navigation
this.messageHistoryIndex = null;
this.textFieldView.dispatch({
changes: {
from: 0,
to: this.textFieldView.state.doc.length,
insert: "",
}
});
}
return true;
},
preventDefault: true,
},
]),
// 3) Enable slash-command autocompletion
slashCommandAutocompletion({
prefix: this.plugin.settings.commandPrefix,
@ -174,6 +252,13 @@ class FloatingWidget extends WidgetType {
parent: editorDom,
})
// Reset messageHistoryIndex when user types or changes input
this.textFieldView.dom.addEventListener("input", () => {
// Only reset if the input is not the result of an up/down navigation
// (i.e., if the value doesn't match the current history entry)
// But for simplicity, always reset if user types
this.messageHistoryIndex = null;
});
}
private createSubmitButton() {
@ -223,6 +308,8 @@ class FloatingWidget extends WidgetType {
this.toggleLoading(false);
});
// Reset message history navigation after submit
this.messageHistoryIndex = null;
}
/**

View file

@ -0,0 +1,55 @@
/**
* MessageQueue is a generic fixed-length queue for storing recent chat messages.
* - When a new item is enqueued and the queue is full, the oldest item is removed.
* - Duplicate consecutive items are not added.
* - Provides standard queue operations: enqueue, dequeue, peek, size, isEmpty, and getItems.
*/
export class MessageQueue<T> {
private items: T[] = [];
private maxLength: number;
constructor(maxLength: number) {
this.maxLength = maxLength;
}
// Adds an item to the queue
enqueue(item: T): void {
// If the new item is exactly the same as the previous one, do not add it
if (this.items.length > 0) {
const lastItem = this.items[this.items.length - 1];
if (lastItem === item) {
return;
}
}
if (this.items.length >= this.maxLength) {
// Remove the oldest item if the queue is full
this.items.shift();
}
this.items.push(item);
}
// Removes and returns the item at the front of the queue
dequeue(): T | undefined {
return this.items.shift();
}
// Returns the item at the front of the queue without removing it
peek(): T | undefined {
return this.items[0];
}
// Returns the current size of the queue
size(): number {
return this.items.length;
}
// Checks if the queue is empty
isEmpty(): boolean {
return this.items.length === 0;
}
// Returns the items in the queue
getItems(): T[] {
return [...this.items];
}
}