feat: use Ctrl/Cmd + ↑/↓ to navigate message history (#1501)

This commit is contained in:
kang-square
2025-03-05 16:33:22 -05:00
committed by GitHub
parent 09cae6e83d
commit b8b5c9dd6e
2 changed files with 73 additions and 4 deletions

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, useMemo } from 'react';
import { getApiUrl } from '../config';
import { generateSessionId } from '../sessions';
import BottomMenu from './BottomMenu';
@@ -226,6 +226,20 @@ export default function ChatView({
return true;
};
const commandHistory = useMemo(() => {
return filteredMessages
.reduce<string[]>((history, message) => {
if (isUserMessage(message)) {
const text = message.content.find((c) => c.type === 'text')?.text?.trim();
if (text) {
history.push(text);
}
}
return history;
}, [])
.reverse();
}, [filteredMessages, isUserMessage]);
return (
<div className="flex flex-col w-full h-screen items-center justify-center">
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle border-b border-borderSubtle">
@@ -278,7 +292,12 @@ export default function ChatView({
<div className="relative">
{isLoading && <LoadingGoose />}
<Input handleSubmit={handleSubmit} isLoading={isLoading} onStop={onStopGoose} />
<Input
handleSubmit={handleSubmit}
isLoading={isLoading}
onStop={onStopGoose}
commandHistory={commandHistory}
/>
<BottomMenu hasMessages={hasMessages} setView={setView} />
</div>
</Card>

View File

@@ -7,12 +7,20 @@ interface InputProps {
handleSubmit: (e: React.FormEvent) => void;
isLoading?: boolean;
onStop?: () => void;
commandHistory?: string[];
}
export default function Input({ handleSubmit, isLoading = false, onStop }: InputProps) {
export default function Input({
handleSubmit,
isLoading = false,
onStop,
commandHistory = [],
}: InputProps) {
const [value, setValue] = useState('');
// State to track if the IME is composing (i.e., in the middle of Japanese IME input)
const [isComposing, setIsComposing] = useState(false);
const [historyIndex, setHistoryIndex] = useState(-1);
const [savedInput, setSavedInput] = useState('');
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
@@ -50,7 +58,45 @@ export default function Input({ handleSubmit, isLoading = false, onStop }: Input
setIsComposing(false);
};
const handleHistoryNavigation = (evt: React.KeyboardEvent<HTMLTextAreaElement>) => {
evt.preventDefault();
// Save current input if we're just starting to navigate history
if (historyIndex === -1) {
setSavedInput(value);
}
// Calculate new history index
let newIndex = historyIndex;
if (evt.key === 'ArrowUp') {
// Move backwards through history
if (historyIndex < commandHistory.length - 1) {
newIndex = historyIndex + 1;
}
} else {
// Move forwards through history
if (historyIndex > -1) {
newIndex = historyIndex - 1;
}
}
// Update index and value
setHistoryIndex(newIndex);
if (newIndex === -1) {
// Restore saved input when going past the end of history
setValue(savedInput);
} else {
setValue(commandHistory[newIndex] || '');
}
};
const handleKeyDown = (evt: React.KeyboardEvent<HTMLTextAreaElement>) => {
// Handle command history navigation
if ((evt.metaKey || evt.ctrlKey) && (evt.key === 'ArrowUp' || evt.key === 'ArrowDown')) {
handleHistoryNavigation(evt);
return;
}
if (evt.key === 'Enter') {
// should not trigger submit on Enter if it's composing (IME input in progress) or shift is pressed
if (evt.shiftKey || isComposing) {
@@ -66,6 +112,8 @@ export default function Input({ handleSubmit, isLoading = false, onStop }: Input
if (!isLoading && value.trim()) {
handleSubmit(new CustomEvent('submit', { detail: { value } }));
setValue('');
setHistoryIndex(-1);
setSavedInput('');
}
}
};
@@ -75,6 +123,8 @@ export default function Input({ handleSubmit, isLoading = false, onStop }: Input
if (value.trim() && !isLoading) {
handleSubmit(new CustomEvent('submit', { detail: { value } }));
setValue('');
setHistoryIndex(-1);
setSavedInput('');
}
};
@@ -94,7 +144,7 @@ export default function Input({ handleSubmit, isLoading = false, onStop }: Input
<textarea
autoFocus
id="dynamic-textarea"
placeholder="What can goose help with?"
placeholder="What can goose help with? (Cmd/Ctrl + ↑/↓ for history)"
value={value}
onChange={handleChange}
onCompositionStart={handleCompositionStart}