feat(dashboard): add input history navigation with up/down arrows
Add shell-style up/down arrow navigation through past messages in the SimpleInput component. History is derived from the conversation data already parsed from session logs — no new state management needed. dashboard/components/SessionCard.js: - Pass `conversation` prop through to SimpleInput (line 170) - Prop chain verified: App -> SessionCard -> SimpleInput, including the Modal/enlarged path (Modal.js:69 already passes conversation) dashboard/components/SimpleInput.js: - Accept `conversation` prop and derive `userHistory` via useMemo, filtering for role === 'user' messages and mapping to content - Add historyIndexRef (-1 = not browsing) and draftRef (preserves in-progress text when entering history mode) - ArrowUp: intercepts only when cursor at position 0 and autocomplete closed, walks backward through history (newest to oldest) - ArrowDown: only when already browsing history, walks forward; past newest entry restores saved draft and exits history mode - Bounds clamp on ArrowUp prevents undefined array access if userHistory shrinks between navigations (SSE update edge case) - Reset historyIndexRef on submit (line 110) and manual input (line 141) - Textarea height recalculated after setting history text via setTimeout to run after Preact commits the state update Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -167,6 +167,7 @@ export function SessionCard({ session, onClick, conversation, onFetchConversatio
|
||||
status=${session.status}
|
||||
onRespond=${onRespond}
|
||||
autocompleteConfig=${autocompleteConfig}
|
||||
conversation=${conversation}
|
||||
/>
|
||||
`}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { html, useState, useRef, useCallback, useMemo, useEffect } from '../lib/
|
||||
import { getStatusMeta } from '../utils/status.js';
|
||||
import { getTriggerInfo as _getTriggerInfo, filteredSkills as _filteredSkills } from '../utils/autocomplete.js';
|
||||
|
||||
export function SimpleInput({ sessionId, status, onRespond, autocompleteConfig = null }) {
|
||||
export function SimpleInput({ sessionId, status, onRespond, autocompleteConfig = null, conversation }) {
|
||||
const [text, setText] = useState('');
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
@@ -12,8 +12,15 @@ export function SimpleInput({ sessionId, status, onRespond, autocompleteConfig =
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const textareaRef = useRef(null);
|
||||
const autocompleteRef = useRef(null);
|
||||
const historyIndexRef = useRef(-1);
|
||||
const draftRef = useRef('');
|
||||
const meta = getStatusMeta(status);
|
||||
|
||||
const userHistory = useMemo(
|
||||
() => (conversation || []).filter(m => m.role === 'user').map(m => m.content),
|
||||
[conversation]
|
||||
);
|
||||
|
||||
const getTriggerInfo = useCallback((value, cursorPos) => {
|
||||
return _getTriggerInfo(value, cursorPos, autocompleteConfig);
|
||||
}, [autocompleteConfig]);
|
||||
@@ -100,6 +107,7 @@ export function SimpleInput({ sessionId, status, onRespond, autocompleteConfig =
|
||||
try {
|
||||
await onRespond(sessionId, text.trim(), true, 0);
|
||||
setText('');
|
||||
historyIndexRef.current = -1;
|
||||
} catch (err) {
|
||||
setError('Failed to send message');
|
||||
console.error('SimpleInput send error:', err);
|
||||
@@ -130,6 +138,7 @@ export function SimpleInput({ sessionId, status, onRespond, autocompleteConfig =
|
||||
const value = e.target.value;
|
||||
const cursorPos = e.target.selectionStart;
|
||||
setText(value);
|
||||
historyIndexRef.current = -1;
|
||||
setTriggerInfo(getTriggerInfo(value, cursorPos));
|
||||
e.target.style.height = 'auto';
|
||||
e.target.style.height = e.target.scrollHeight + 'px';
|
||||
@@ -169,6 +178,57 @@ export function SimpleInput({ sessionId, status, onRespond, autocompleteConfig =
|
||||
}
|
||||
}
|
||||
|
||||
// History navigation (only when autocomplete is closed)
|
||||
if (e.key === 'ArrowUp' && !showAutocomplete &&
|
||||
e.target.selectionStart === 0 && e.target.selectionEnd === 0 &&
|
||||
userHistory.length > 0) {
|
||||
e.preventDefault();
|
||||
if (historyIndexRef.current === -1) {
|
||||
draftRef.current = text;
|
||||
historyIndexRef.current = userHistory.length - 1;
|
||||
} else if (historyIndexRef.current > 0) {
|
||||
historyIndexRef.current -= 1;
|
||||
}
|
||||
// Clamp if history shrank since last navigation
|
||||
if (historyIndexRef.current >= userHistory.length) {
|
||||
historyIndexRef.current = userHistory.length - 1;
|
||||
}
|
||||
const historyText = userHistory[historyIndexRef.current];
|
||||
setText(historyText);
|
||||
setTimeout(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.selectionStart = historyText.length;
|
||||
textareaRef.current.selectionEnd = historyText.length;
|
||||
textareaRef.current.style.height = 'auto';
|
||||
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowDown' && !showAutocomplete &&
|
||||
historyIndexRef.current !== -1) {
|
||||
e.preventDefault();
|
||||
historyIndexRef.current += 1;
|
||||
let newText;
|
||||
if (historyIndexRef.current >= userHistory.length) {
|
||||
historyIndexRef.current = -1;
|
||||
newText = draftRef.current;
|
||||
} else {
|
||||
newText = userHistory[historyIndexRef.current];
|
||||
}
|
||||
setText(newText);
|
||||
setTimeout(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.selectionStart = newText.length;
|
||||
textareaRef.current.selectionEnd = newText.length;
|
||||
textareaRef.current.style.height = 'auto';
|
||||
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal Enter-to-submit (only when dropdown is closed)
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user