Skip to content

Commit 2f58d4e

Browse files
committed
feat(chat): prompt history popup picker with insert/replace actions
Replace inline prompt history navigation with a popup picker triggered by Alt+PageUp. Shows past user messages in chronological order with search filtering and two actions per item: insert at cursor (default, Enter) and replace (Tab+Enter). Insert button is visually pre-highlighted when navigating the list. Add Alt+PgUp hint to textarea placeholder.
1 parent f77c104 commit 2f58d4e

2 files changed

Lines changed: 704 additions & 63 deletions

File tree

frontend/src/components/MessageInput.vue

Lines changed: 75 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import { useSettingsStore } from '../stores/settings'
77
import { sendWsMessage, notifyUserDraftUpdated } from '../composables/useWebSocket'
88
import { useVisualViewport } from '../composables/useVisualViewport'
99
import { isSupportedMimeType, MAX_FILE_SIZE, SUPPORTED_IMAGE_TYPES, draftMediaToMediaItem } from '../utils/fileUtils'
10-
import { getParsedContent } from '../utils/parsedContent'
1110
import { toast } from '../composables/useToast'
1211
import { PERMISSION_MODE, PERMISSION_MODE_LABELS, PERMISSION_MODE_DESCRIPTIONS, MODEL, MODEL_LABELS, EFFORT, EFFORT_LABELS, EFFORT_DISPLAY_LABELS, THINKING_LABELS, THINKING_DISPLAY_LABELS, CLAUDE_IN_CHROME_LABELS, CLAUDE_IN_CHROME_DISPLAY_LABELS } from '../constants'
1312
import MediaThumbnailGroup from './MediaThumbnailGroup.vue'
1413
import AppTooltip from './AppTooltip.vue'
1514
import FilePickerPopup from './FilePickerPopup.vue'
1615
import SlashCommandPickerPopup from './SlashCommandPickerPopup.vue'
16+
import PromptHistoryPickerPopup from './PromptHistoryPickerPopup.vue'
1717
1818
// Track visual viewport height for mobile keyboard handling
1919
useVisualViewport()
@@ -48,60 +48,8 @@ const messageText = ref('')
4848
const textareaRef = ref(null)
4949
const fileInputRef = ref(null)
5050
51-
// ── Prompt history navigation (PageUp / PageDown) ────────────────────────
52-
// Index into past user messages: -1 = not navigating, 0 = most recent, etc.
53-
const historyIndex = ref(-1)
54-
// Text saved before entering history navigation, restored when exiting
55-
let savedDraft = ''
56-
57-
/**
58-
* Get the list of past user message texts for the current session, most recent first.
59-
*/
60-
function getHistoryTexts() {
61-
const items = store.getSessionItems(props.sessionId)
62-
const texts = []
63-
for (let i = items.length - 1; i >= 0; i--) {
64-
if (items[i].kind !== 'user_message') continue
65-
const parsed = getParsedContent(items[i])
66-
const content = parsed?.message?.content
67-
if (!content) continue
68-
// content can be a plain string or an array of blocks
69-
if (typeof content === 'string') {
70-
if (content) texts.push(content)
71-
} else if (Array.isArray(content)) {
72-
const textBlock = content.find(b => b.type === 'text')
73-
if (textBlock?.text) texts.push(textBlock.text)
74-
}
75-
}
76-
return texts
77-
}
78-
79-
/**
80-
* Navigate prompt history. delta: -1 = older (PageUp), +1 = newer (PageDown).
81-
*/
82-
function navigateHistory(delta) {
83-
const history = getHistoryTexts()
84-
if (!history.length) return
85-
86-
const newIndex = historyIndex.value + delta
87-
88-
if (newIndex < -1) return // Can't go newer than draft
89-
if (newIndex >= history.length) return // Can't go older than oldest
90-
91-
if (historyIndex.value === -1) {
92-
// Entering history mode: save current text
93-
savedDraft = messageText.value
94-
}
95-
96-
historyIndex.value = newIndex
97-
98-
if (newIndex === -1) {
99-
// Back to draft
100-
messageText.value = savedDraft
101-
} else {
102-
messageText.value = history[newIndex]
103-
}
104-
}
51+
// ── Prompt history picker ─────────────────────────────────────────────────
52+
const historyPickerRef = ref(null)
10553
const attachButtonId = useId()
10654
const settingsButtonId = useId()
10755
const textareaAnchorId = useId()
@@ -250,7 +198,7 @@ const placeholderText = computed(() => {
250198
let text = 'Type your message... Use / for commands, @ for file paths'
251199
if (!settingsStore.isTouchDevice) {
252200
const keys = settingsStore.isMac ? '⌘↵ or Ctrl↵' : 'Ctrl↵ or Meta↵'
253-
text += `, ${keys} to send`
201+
text += `, Alt+PgUp for history, ${keys} to send`
254202
}
255203
return text
256204
})
@@ -493,6 +441,7 @@ watch(
493441
494442
// Restore draft message when session changes
495443
watch(() => props.sessionId, async (newId) => {
444+
496445
const draft = store.getDraftMessage(newId)
497446
messageText.value = draft?.message || ''
498447
// Adjust textarea height after the DOM updates with restored content
@@ -604,7 +553,6 @@ function onInput(event) {
604553
}
605554
606555
messageText.value = newText
607-
historyIndex.value = -1 // Exit history navigation on new typing
608556
adjustTextareaHeight()
609557
// Notify server that user is actively preparing a message (debounced)
610558
// This prevents auto-stop of the process due to inactivity timeout
@@ -685,7 +633,7 @@ function onSlashCommandPickerClose() {
685633
686634
/**
687635
* Handle keyboard shortcuts in textarea.
688-
* Cmd/Ctrl+Enter submits, PageUp/PageDown navigates prompt history.
636+
* Cmd/Ctrl+Enter submits, Alt+PageUp opens prompt history picker.
689637
*/
690638
function onKeydown(event) {
691639
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
@@ -694,19 +642,71 @@ function onKeydown(event) {
694642
return
695643
}
696644
697-
// PageUp: older prompt, PageDown: newer prompt
698-
if (event.key === 'PageUp') {
645+
// Alt+PageUp: open prompt history picker (or toggle to search if already open)
646+
if (event.altKey && event.key === 'PageUp') {
699647
event.preventDefault()
700-
navigateHistory(1) // 1 = older (higher index)
648+
historyPickerRef.value?.open() // open() handles already-open case (focuses search)
701649
return
702650
}
703-
if (event.key === 'PageDown') {
651+
652+
// Alt+PageDown: focus list in prompt history picker (if open)
653+
if (event.altKey && event.key === 'PageDown') {
704654
event.preventDefault()
705-
navigateHistory(-1) // -1 = newer (lower index)
655+
historyPickerRef.value?.focusList()
706656
return
707657
}
708658
}
709659
660+
/**
661+
* Handle prompt history replace: overwrite textarea content with selected message.
662+
*/
663+
async function onHistoryReplace(text) {
664+
messageText.value = text
665+
if (textareaRef.value) {
666+
textareaRef.value.value = text
667+
const inner = textareaRef.value.shadowRoot?.querySelector('textarea')
668+
if (inner) {
669+
inner.value = text
670+
const pos = text.length
671+
inner.setSelectionRange(pos, pos)
672+
}
673+
}
674+
await nextTick()
675+
textareaRef.value?.focus()
676+
adjustTextareaHeight()
677+
}
678+
679+
/**
680+
* Handle prompt history insert: insert selected message at cursor position.
681+
*/
682+
async function onHistoryInsert(text) {
683+
const inner = textareaRef.value?.shadowRoot?.querySelector('textarea')
684+
const pos = inner?.selectionStart ?? messageText.value.length
685+
const before = messageText.value.slice(0, pos)
686+
const after = messageText.value.slice(pos)
687+
const newText = before + text + after
688+
const newPos = pos + text.length
689+
690+
messageText.value = newText
691+
if (textareaRef.value) {
692+
textareaRef.value.value = newText
693+
if (inner) {
694+
inner.value = newText
695+
inner.setSelectionRange(newPos, newPos)
696+
}
697+
}
698+
await nextTick()
699+
textareaRef.value?.focus()
700+
adjustTextareaHeight()
701+
}
702+
703+
/**
704+
* Handle prompt history picker close: return focus to textarea.
705+
*/
706+
function onHistoryPickerClose() {
707+
textareaRef.value?.focus()
708+
}
709+
710710
/**
711711
* Handle paste event to capture images from clipboard.
712712
* Only processes image files from clipboard.
@@ -890,6 +890,7 @@ async function handleSend() {
890890
891891
// Clear draft message from store (and IndexedDB)
892892
store.clearDraftMessage(props.sessionId)
893+
893894
894895
// Clear attachments from store and IndexedDB
895896
if (attachmentCount.value > 0) {
@@ -943,6 +944,7 @@ function handleCancel() {
943944
* restore dropdowns to their active (server-side) values.
944945
*/
945946
async function handleReset() {
947+
946948
// Clear text if any
947949
if (messageText.value) {
948950
messageText.value = ''
@@ -1010,6 +1012,16 @@ async function handleReset() {
10101012
@close="onSlashCommandPickerClose"
10111013
/>
10121014
1015+
<!-- Prompt history picker popup triggered by Alt+PageUp -->
1016+
<PromptHistoryPickerPopup
1017+
ref="historyPickerRef"
1018+
:session-id="sessionId"
1019+
:anchor-id="textareaAnchorId"
1020+
@replace="onHistoryReplace"
1021+
@insert="onHistoryInsert"
1022+
@close="onHistoryPickerClose"
1023+
/>
1024+
10131025
<div class="message-input-toolbar">
10141026
<!-- Attachments row: button on left, thumbnails on right -->
10151027
<div class="message-input-attachments">

0 commit comments

Comments
 (0)