Skip to content

Commit bf3ee7c

Browse files
committed
Add composer prompt history with arrow up/down on the web UI
Arrow keys previously only navigated the slash-command palette. Store recent sends in localStorage and recall them with ↑/↓ when the caret is on the first/last line, matching shell-style prompt history without breaking multi-line editing.
1 parent 3f859a5 commit bf3ee7c

1 file changed

Lines changed: 101 additions & 1 deletion

File tree

web/src/pages/ChatPage.tsx

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,42 @@ const SUGGESTION_KEYS: MessageKey[] = [
219219
'chat.suggest4',
220220
]
221221

222+
/** Composer ↑/↓ recall — most recent first, de-duped consecutive, capped. */
223+
const INPUT_HISTORY_KEY = 'antares:composer-history'
224+
const INPUT_HISTORY_MAX = 50
225+
226+
function loadInputHistory(): string[] {
227+
try {
228+
const raw = localStorage.getItem(INPUT_HISTORY_KEY)
229+
if (!raw) return []
230+
const parsed = JSON.parse(raw) as unknown
231+
if (!Array.isArray(parsed)) return []
232+
return parsed.filter((x): x is string => typeof x === 'string' && x.trim() !== '').slice(0, INPUT_HISTORY_MAX)
233+
} catch {
234+
return []
235+
}
236+
}
237+
238+
function pushInputHistory(entry: string, prev: string[]): string[] {
239+
const text = entry.trim()
240+
if (!text) return prev
241+
// Drop consecutive duplicate of the most recent entry.
242+
const next = prev[0] === text ? prev : [text, ...prev.filter((x) => x !== text)]
243+
return next.slice(0, INPUT_HISTORY_MAX)
244+
}
245+
246+
/** Caret is on the first visual line of a textarea (for shell-style history ↑). */
247+
function caretOnFirstLine(el: HTMLTextAreaElement): boolean {
248+
const pos = el.selectionStart ?? 0
249+
return !el.value.slice(0, pos).includes('\n')
250+
}
251+
252+
/** Caret is on the last visual line (for history ↓). */
253+
function caretOnLastLine(el: HTMLTextAreaElement): boolean {
254+
const pos = el.selectionStart ?? 0
255+
return !el.value.slice(pos).includes('\n')
256+
}
257+
222258
export default function ChatPage() {
223259
const { sessionId } = useParams<{ sessionId: string }>()
224260
const navigate = useNavigate()
@@ -238,6 +274,12 @@ export default function ChatPage() {
238274
notice?: string
239275
}>({ turn: 1 })
240276
const [input, setInput] = useState('')
277+
// Recent composer prompts (shell-style ↑/↓). Persisted across reloads.
278+
const [inputHistory, setInputHistory] = useState<string[]>(() => loadInputHistory())
279+
// -1 = editing a live draft (not browsing history). ≥0 = index into inputHistory
280+
// from the end (0 = most recent).
281+
const [historyPos, setHistoryPos] = useState(-1)
282+
const draftRef = useRef('') // draft saved when first leaving with ↑
241283
const [error, setError] = useState<string>()
242284
const [title, setTitle] = useState('')
243285
const [approvals, setApprovals] = useState<ApprovalView[]>([])
@@ -873,7 +915,18 @@ export default function ChatPage() {
873915
const text = raw.trim()
874916
if ((!text && attached.length === 0 && attachedDocs.length === 0) || streaming) return
875917
if (text.startsWith('/') && text.length > 1) {
918+
// Still record slash commands so ↑ recalls them.
919+
if (text) {
920+
setInputHistory((prev) => {
921+
const next = pushInputHistory(text, prev)
922+
localStorage.setItem(INPUT_HISTORY_KEY, JSON.stringify(next))
923+
return next
924+
})
925+
}
926+
setHistoryPos(-1)
927+
draftRef.current = ''
876928
void runCommand(text)
929+
setInput('')
877930
return
878931
}
879932

@@ -894,6 +947,16 @@ export default function ChatPage() {
894947
}
895948
const assistantId = `local_${Date.now()}_a`
896949
setMessages((prev) => [...prev, userMsg, { id: assistantId, role: 'assistant', content: '' }])
950+
// Remember what was sent for ↑/↓ (composer history).
951+
if (text) {
952+
setInputHistory((prev) => {
953+
const next = pushInputHistory(text, prev)
954+
localStorage.setItem(INPUT_HISTORY_KEY, JSON.stringify(next))
955+
return next
956+
})
957+
}
958+
setHistoryPos(-1)
959+
draftRef.current = ''
897960
setInput('')
898961
setImages([])
899962
setDocs([])
@@ -1138,12 +1201,49 @@ export default function ChatPage() {
11381201
}
11391202
}
11401203
}
1204+
// Shell-style prompt history: ↑ older, ↓ newer. Only when the caret is on
1205+
// the first/last line so multi-line editing still moves the cursor normally.
1206+
if (e.key === 'ArrowUp' && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
1207+
const el = e.currentTarget
1208+
if (inputHistory.length > 0 && caretOnFirstLine(el)) {
1209+
e.preventDefault()
1210+
if (historyPos === -1) draftRef.current = input
1211+
const idx = historyPos === -1 ? 0 : Math.min(historyPos + 1, inputHistory.length - 1)
1212+
setHistoryPos(idx)
1213+
setInput(inputHistory[idx] ?? '')
1214+
return
1215+
}
1216+
}
1217+
if (e.key === 'ArrowDown' && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
1218+
const el = e.currentTarget
1219+
if (historyPos >= 0 && caretOnLastLine(el)) {
1220+
e.preventDefault()
1221+
if (historyPos <= 0) {
1222+
setHistoryPos(-1)
1223+
setInput(draftRef.current)
1224+
} else {
1225+
const idx = historyPos - 1
1226+
setHistoryPos(idx)
1227+
setInput(inputHistory[idx] ?? '')
1228+
}
1229+
return
1230+
}
1231+
}
11411232
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
11421233
e.preventDefault()
11431234
send()
11441235
}
11451236
}
11461237

1238+
// Typing while browsing history leaves history mode (treat as new draft).
1239+
const onInputChange = useCallback((value: string) => {
1240+
if (historyPos !== -1) {
1241+
setHistoryPos(-1)
1242+
draftRef.current = ''
1243+
}
1244+
setInput(value)
1245+
}, [historyPos])
1246+
11471247
const newChat = () => {
11481248
stop()
11491249
localStorage.removeItem('antares:last-session')
@@ -1175,7 +1275,7 @@ export default function ChatPage() {
11751275
onRemoveImage={(i) => setImages((prev) => prev.filter((_, x) => x !== i))}
11761276
onRemoveDoc={(i) => setDocs((prev) => prev.filter((_, x) => x !== i))}
11771277
onPaste={onPaste}
1178-
onChange={setInput}
1278+
onChange={onInputChange}
11791279
onKeyDown={onKeyDown}
11801280
onSend={send}
11811281
onStop={stop}

0 commit comments

Comments
 (0)