mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
feat(desktop): integrate arrow history with the message queue
Builds on @naqerl's arrow up/down history (previous commit), making ArrowUp do the right thing when a queue exists. ArrowUp/ArrowDown priority: 1. Editing a queued turn → walk older/newer through queued entries, saving each edit; ArrowDown past the newest exits and restores the pre-edit draft. 2. Empty composer + queued turns → ArrowUp opens the newest queued entry for editing (the row's pencil), so Enter saves it back to the queue instead of firing a new message — the gap the history nav had alone. 3. Otherwise → sent-message history recall (unchanged). Also: Esc cancels an in-progress queue edit (else interrupts). Cleanups on the integrated code: fold the browse-state reset into the existing session-change effect (drop the duplicate ref+effect); reuse loadIntoComposer for history recall; sort imports; add curly braces + the runDrain sessionId dep (lint).
This commit is contained in:
parent
f94363d1f0
commit
ce50030634
2 changed files with 97 additions and 52 deletions
|
|
@ -24,6 +24,13 @@ import { DATA_IMAGE_URL_RE } from '@/lib/embedded-images'
|
|||
import { triggerHaptic } from '@/lib/haptics'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { $composerAttachments, clearComposerAttachments, type ComposerAttachment } from '@/store/composer'
|
||||
import {
|
||||
browseBackward,
|
||||
browseForward,
|
||||
deriveUserHistory,
|
||||
isBrowsingHistory,
|
||||
resetBrowseState
|
||||
} from '@/store/composer-input-history'
|
||||
import {
|
||||
$queuedPromptsBySession,
|
||||
enqueueQueuedPrompt,
|
||||
|
|
@ -33,13 +40,6 @@ import {
|
|||
shouldAutoDrainOnSettle,
|
||||
updateQueuedPrompt
|
||||
} from '@/store/composer-queue'
|
||||
import {
|
||||
browseBackward,
|
||||
browseForward,
|
||||
deriveUserHistory,
|
||||
isBrowsingHistory,
|
||||
resetBrowseState
|
||||
} from '@/store/composer-input-history'
|
||||
import { $gatewayState, $messages } from '@/store/session'
|
||||
import { $threadScrolledUp } from '@/store/thread-scroll'
|
||||
|
||||
|
|
@ -201,6 +201,7 @@ export function ChatBar({
|
|||
return
|
||||
}
|
||||
|
||||
resetBrowseState(prev)
|
||||
setRestingPlaceholder(pickPlaceholder(sessionId ? followUpPlaceholders : newSessionPlaceholders))
|
||||
}, [followUpPlaceholders, newSessionPlaceholders, sessionId])
|
||||
|
||||
|
|
@ -255,16 +256,6 @@ export function ChatBar({
|
|||
}
|
||||
}, [disabled, focusInput, focusKey, focusRequestId])
|
||||
|
||||
// Reset input history browse state when the active session changes.
|
||||
const prevSessionRef = useRef(sessionId)
|
||||
|
||||
useEffect(() => {
|
||||
if (prevSessionRef.current !== sessionId) {
|
||||
prevSessionRef.current = sessionId
|
||||
resetBrowseState(sessionId)
|
||||
}
|
||||
}, [sessionId])
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
return undefined
|
||||
|
|
@ -733,14 +724,31 @@ export function ChatBar({
|
|||
}
|
||||
}
|
||||
|
||||
// ArrowUp/ArrowDown for input history navigation. The user-text ring is
|
||||
// derived from the live session messages on every press — no mirror, no
|
||||
// seeding, no dedup. Single source of truth: $messages.
|
||||
// ArrowUp/ArrowDown navigate, in priority order: the queue (edit entries in
|
||||
// place) then sent-message history. The history ring is derived from live
|
||||
// session messages each press — single source of truth, no mirror.
|
||||
if (event.key === 'ArrowUp') {
|
||||
const currentDraft = draftRef.current
|
||||
|
||||
// Don't hijack Arrow Up when the user has an unsent draft and isn't
|
||||
// already browsing — they'd lose what they typed.
|
||||
// Editing a queued turn → walk to the older entry.
|
||||
if (queueEdit && stepQueuedEdit(-1)) {
|
||||
event.preventDefault()
|
||||
triggerKeyConsumedRef.current = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Empty composer + a queued turn → open the newest queued entry for edit
|
||||
// (the row's pencil), not a text recall. Enter saves it back to the queue.
|
||||
if (!currentDraft.trim() && !queueEdit && queuedPrompts.length > 0) {
|
||||
event.preventDefault()
|
||||
triggerKeyConsumedRef.current = true
|
||||
beginQueuedEdit(queuedPrompts[queuedPrompts.length - 1]!)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Don't hijack a typed draft unless already browsing — they'd lose it.
|
||||
if (currentDraft.trim() && !isBrowsingHistory(sessionId)) {
|
||||
return
|
||||
}
|
||||
|
|
@ -752,21 +760,23 @@ export function ChatBar({
|
|||
const entry = browseBackward(sessionId, currentDraft, history)
|
||||
|
||||
if (entry !== null) {
|
||||
const editor = editorRef.current
|
||||
|
||||
draftRef.current = entry
|
||||
aui.composer().setText(entry)
|
||||
|
||||
if (editor) {
|
||||
renderComposerContents(editor, entry)
|
||||
placeCaretEnd(editor)
|
||||
}
|
||||
loadIntoComposer(entry, $composerAttachments.get())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
// Editing a queued turn → walk to the newer entry (past the newest exits).
|
||||
if (queueEdit) {
|
||||
event.preventDefault()
|
||||
triggerKeyConsumedRef.current = true
|
||||
stepQueuedEdit(1)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Browsing sent history → step toward the present, restoring the draft.
|
||||
if (isBrowsingHistory(sessionId)) {
|
||||
event.preventDefault()
|
||||
triggerKeyConsumedRef.current = true
|
||||
|
|
@ -775,21 +785,10 @@ export function ChatBar({
|
|||
const result = browseForward(sessionId, history)
|
||||
|
||||
if (result !== null) {
|
||||
const editor = editorRef.current
|
||||
|
||||
draftRef.current = result.text
|
||||
aui.composer().setText(result.text)
|
||||
|
||||
if (editor) {
|
||||
renderComposerContents(editor, result.text)
|
||||
placeCaretEnd(editor)
|
||||
}
|
||||
loadIntoComposer(result.text, $composerAttachments.get())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Not browsing — let the browser handle default cursor movement.
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -813,11 +812,21 @@ export function ChatBar({
|
|||
return
|
||||
}
|
||||
|
||||
// Esc interrupts the running turn (Stop-button parity).
|
||||
if (event.key === 'Escape' && busy) {
|
||||
event.preventDefault()
|
||||
triggerHaptic('cancel')
|
||||
void Promise.resolve(onCancel())
|
||||
if (event.key === 'Escape') {
|
||||
// Editing a queued turn → Esc cancels the edit, restoring the prior draft.
|
||||
if (queueEdit) {
|
||||
event.preventDefault()
|
||||
exitQueuedEdit('cancel')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise Esc interrupts the running turn (Stop-button parity).
|
||||
if (busy) {
|
||||
event.preventDefault()
|
||||
triggerHaptic('cancel')
|
||||
void Promise.resolve(onCancel())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -983,6 +992,42 @@ export function ChatBar({
|
|||
focusInput()
|
||||
}
|
||||
|
||||
// Walk queued entries while editing (ArrowUp = older, ArrowDown = newer),
|
||||
// saving the in-progress edit on each step. Stepping newer past the last
|
||||
// entry exits edit mode and restores the pre-edit draft.
|
||||
const stepQueuedEdit = (direction: -1 | 1) => {
|
||||
if (!queueEdit) {
|
||||
return false
|
||||
}
|
||||
|
||||
const index = queuedPrompts.findIndex(e => e.id === queueEdit.entryId)
|
||||
const target = index + direction
|
||||
|
||||
if (index < 0 || target < 0) {
|
||||
return index >= 0 // at the oldest: swallow; missing entry: let it fall through
|
||||
}
|
||||
|
||||
const saved = updateQueuedPrompt(queueEdit.sessionKey, queueEdit.entryId, {
|
||||
attachments: cloneAttachments($composerAttachments.get()),
|
||||
text: draftRef.current
|
||||
})
|
||||
|
||||
const next = queuedPrompts[target]
|
||||
|
||||
if (next) {
|
||||
setQueueEdit({ ...queueEdit, entryId: next.id })
|
||||
loadIntoComposer(next.text, next.attachments)
|
||||
} else {
|
||||
setQueueEdit(null)
|
||||
loadIntoComposer(queueEdit.draft, queueEdit.attachments)
|
||||
}
|
||||
|
||||
triggerHaptic(saved ? 'success' : 'selection')
|
||||
focusInput()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const exitQueuedEdit = (action: 'cancel' | 'save'): boolean => {
|
||||
if (!queueEdit) {
|
||||
return false
|
||||
|
|
@ -1058,7 +1103,7 @@ export function ChatBar({
|
|||
drainingQueueRef.current = false
|
||||
}
|
||||
},
|
||||
[activeQueueSessionKey, onSubmit, queuedPrompts]
|
||||
[activeQueueSessionKey, onSubmit, queuedPrompts, sessionId]
|
||||
)
|
||||
|
||||
const drainNextQueued = useCallback(
|
||||
|
|
|
|||
|
|
@ -55,11 +55,11 @@ export function deriveUserHistory<T extends { role: string }>(
|
|||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const m = messages[i]!
|
||||
|
||||
if (m.role !== 'user') continue
|
||||
if (m.role !== 'user') {continue}
|
||||
|
||||
const t = getText(m).trim()
|
||||
|
||||
if (t) out.push(t)
|
||||
if (t) {out.push(t)}
|
||||
}
|
||||
|
||||
return out
|
||||
|
|
@ -138,7 +138,7 @@ export function resetBrowseState(sessionId: string | null | undefined) {
|
|||
const all = { ...$perSessionBrowse.get() }
|
||||
const existing = all[sessionId]
|
||||
|
||||
if (!existing) return
|
||||
if (!existing) {return}
|
||||
|
||||
all[sessionId] = { cursor: -1, draftSnapshot: '' }
|
||||
$perSessionBrowse.set(all)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue