mirror of
https://github.com/rait-09/obsidian-agent-client.git
synced 2026-07-22 06:43:37 +00:00
Add useInputHistory hook that allows navigating through previously sent messages using ArrowUp/ArrowDown keys. ArrowUp recalls the previous user message when the input is empty, and continued presses navigate further back. Editing the restored text or pressing ArrowLeft/ArrowRight exits history mode.
143 lines
3.9 KiB
TypeScript
143 lines
3.9 KiB
TypeScript
import { useCallback, useRef, useMemo } from "react";
|
|
import type { ChatMessage } from "../domain/models/chat-message";
|
|
|
|
export interface UseInputHistoryReturn {
|
|
/**
|
|
* Key handler for ArrowUp/ArrowDown history navigation.
|
|
* Returns true if the event was handled (caller should return early).
|
|
*/
|
|
handleHistoryKeyDown: (
|
|
e: React.KeyboardEvent,
|
|
textareaEl: HTMLTextAreaElement | null,
|
|
) => boolean;
|
|
|
|
/**
|
|
* Reset history navigation state. Call after sending a message.
|
|
*/
|
|
resetHistory: () => void;
|
|
}
|
|
|
|
/**
|
|
* Hook for navigating input history with ArrowUp/ArrowDown keys.
|
|
*
|
|
* - ArrowUp when input is empty recalls the previous user message.
|
|
* - Continued ArrowUp/ArrowDown navigates through history.
|
|
* - If the user edits a restored message, history navigation resets.
|
|
* - ArrowDown from the most recent entry returns to empty input.
|
|
*/
|
|
export function useInputHistory(
|
|
messages: ChatMessage[],
|
|
onInputChange: (value: string) => void,
|
|
): UseInputHistoryReturn {
|
|
// -1 = no history selected, 0 = most recent user message, 1 = second most recent, ...
|
|
const historyIndexRef = useRef(-1);
|
|
// The exact text placed into the input via history navigation.
|
|
// Used to detect if the user has edited it (which exits history mode).
|
|
const restoredTextRef = useRef<string | null>(null);
|
|
|
|
// Extract user message texts in chronological order
|
|
const userMessages = useMemo(() => {
|
|
return messages
|
|
.filter((m) => m.role === "user")
|
|
.map((m) => {
|
|
const textContent = m.content.find(
|
|
(c) => c.type === "text" || c.type === "text_with_context",
|
|
);
|
|
return textContent && "text" in textContent
|
|
? textContent.text
|
|
: "";
|
|
})
|
|
.filter((text) => text.trim() !== "");
|
|
}, [messages]);
|
|
|
|
const handleHistoryKeyDown = useCallback(
|
|
(
|
|
e: React.KeyboardEvent,
|
|
textareaEl: HTMLTextAreaElement | null,
|
|
): boolean => {
|
|
if (!textareaEl) return false;
|
|
if (e.nativeEvent.isComposing) return false;
|
|
if (userMessages.length === 0) return false;
|
|
|
|
// Exit history mode if user edited text or moved cursor
|
|
if (historyIndexRef.current !== -1) {
|
|
if (
|
|
e.key === "ArrowLeft" ||
|
|
e.key === "ArrowRight" ||
|
|
(restoredTextRef.current !== null &&
|
|
textareaEl.value !== restoredTextRef.current)
|
|
) {
|
|
historyIndexRef.current = -1;
|
|
restoredTextRef.current = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (e.key === "ArrowUp") {
|
|
// Allow when input is empty OR already navigating history
|
|
if (
|
|
textareaEl.value.trim() !== "" &&
|
|
historyIndexRef.current === -1
|
|
)
|
|
return false;
|
|
|
|
e.preventDefault();
|
|
|
|
const nextIndex = historyIndexRef.current + 1;
|
|
if (nextIndex >= userMessages.length) {
|
|
return true;
|
|
}
|
|
|
|
historyIndexRef.current = nextIndex;
|
|
const messageText =
|
|
userMessages[userMessages.length - 1 - nextIndex];
|
|
restoredTextRef.current = messageText;
|
|
onInputChange(messageText);
|
|
|
|
window.setTimeout(() => {
|
|
textareaEl.selectionStart = messageText.length;
|
|
textareaEl.selectionEnd = messageText.length;
|
|
}, 0);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (e.key === "ArrowDown") {
|
|
const currentIndex = historyIndexRef.current;
|
|
if (currentIndex === -1) return false;
|
|
|
|
e.preventDefault();
|
|
|
|
const nextIndex = currentIndex - 1;
|
|
historyIndexRef.current = nextIndex;
|
|
|
|
if (nextIndex === -1) {
|
|
restoredTextRef.current = null;
|
|
onInputChange("");
|
|
} else {
|
|
const messageText =
|
|
userMessages[userMessages.length - 1 - nextIndex];
|
|
restoredTextRef.current = messageText;
|
|
onInputChange(messageText);
|
|
|
|
window.setTimeout(() => {
|
|
textareaEl.selectionStart = messageText.length;
|
|
textareaEl.selectionEnd = messageText.length;
|
|
}, 0);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
[userMessages, onInputChange],
|
|
);
|
|
|
|
const resetHistory = useCallback(() => {
|
|
historyIndexRef.current = -1;
|
|
restoredTextRef.current = null;
|
|
}, []);
|
|
|
|
return { handleHistoryKeyDown, resetHistory };
|
|
}
|