From 425f6cb94c05489739c1b1bff6a85e2b929c49e0 Mon Sep 17 00:00:00 2001 From: han <475166676@qq.com> Date: Sat, 14 Mar 2026 10:27:48 +0800 Subject: [PATCH 1/6] fix(chat): simplify send history navigation --- src/components/chat/message-input.tsx | 157 +++++++++++++++++++++++++- 1 file changed, 156 insertions(+), 1 deletion(-) diff --git a/src/components/chat/message-input.tsx b/src/components/chat/message-input.tsx index 01540b9c..5661d985 100644 --- a/src/components/chat/message-input.tsx +++ b/src/components/chat/message-input.tsx @@ -130,6 +130,42 @@ const MIME_BY_EXT: Record = { svg: "image/svg+xml", } +const MESSAGE_INPUT_SEND_HISTORY_LIMIT = 50 + +function getMessageInputSendHistoryStorageKey(scopeKey: string | null): string { + return `codeg:messageInputSendHistory:${scopeKey ?? "global"}` +} + +function loadMessageInputSendHistory(scopeKey: string | null): string[] { + if (typeof window === "undefined") return [] + try { + const raw = window.localStorage.getItem( + getMessageInputSendHistoryStorageKey(scopeKey) + ) + if (!raw) return [] + const parsed: unknown = JSON.parse(raw) + if (!Array.isArray(parsed)) return [] + return parsed + .filter((item): item is string => typeof item === "string") + .map((item) => item.trim()) + .filter((item) => item.length > 0) + } catch { + return [] + } +} + +function saveMessageInputSendHistory(scopeKey: string | null, items: string[]) { + if (typeof window === "undefined") return + try { + window.localStorage.setItem( + getMessageInputSendHistoryStorageKey(scopeKey), + JSON.stringify(items) + ) + } catch { + // ignore + } +} + function fileNameFromPath(path: string): string { return path.split(/[/\\]/).pop() || path } @@ -299,6 +335,8 @@ export function MessageInput({ const textRef = useRef(text) const disabledRef = useRef(disabled) const isPromptingRef = useRef(isPrompting) + const sendHistoryRef = useRef([]) + const [sendHistoryIndex, setSendHistoryIndex] = useState(null) useEffect(() => { if (isActive && !disabled && !isPrompting) { @@ -307,6 +345,13 @@ export function MessageInput({ }) } }, [isActive, disabled, isPrompting]) + + useEffect(() => { + sendHistoryRef.current = loadMessageInputSendHistory( + effectiveDraftStorageKey + ) + setSendHistoryIndex(null) + }, [effectiveDraftStorageKey]) const dragActiveRef = useRef(false) const canAttachImages = promptCapabilities.image @@ -726,6 +771,9 @@ export function MessageInput({ (e: React.ChangeEvent) => { const value = e.target.value setText(value) + if (value.length === 0 && sendHistoryIndex !== null) { + setSendHistoryIndex(null) + } if (slashCommands.length > 0 && /^\/(\S*)$/.test(value)) { setSlashSelectedIndex(0) setSlashMenuOpen(true) @@ -733,7 +781,26 @@ export function MessageInput({ setSlashMenuOpen(false) } }, - [slashCommands.length] + [sendHistoryIndex, slashCommands.length] + ) + + const pushMessageToSendHistory = useCallback( + (value: string) => { + const trimmed = value.trim() + if (!trimmed) return + + const prev = loadMessageInputSendHistory(effectiveDraftStorageKey) + const last = prev.length > 0 ? prev[prev.length - 1] : null + if (last === trimmed) { + sendHistoryRef.current = prev + return + } + + const next = [...prev, trimmed].slice(-MESSAGE_INPUT_SEND_HISTORY_LIMIT) + saveMessageInputSendHistory(effectiveDraftStorageKey, next) + sendHistoryRef.current = next + }, + [effectiveDraftStorageKey] ) const handlePickFiles = useCallback(async () => { @@ -926,6 +993,11 @@ export function MessageInput({ const draft = buildDraft() if (!draft) return + if (!isEditingQueueItem) { + pushMessageToSendHistory(textRef.current) + setSendHistoryIndex(null) + } + // Edit mode: save back to queue item if (isEditingQueueItem && onSaveQueueEdit) { onSaveQueueEdit(draft) @@ -951,6 +1023,7 @@ export function MessageInput({ }, [ buildDraft, isEditingQueueItem, + pushMessageToSendHistory, isPrompting, onSaveQueueEdit, onEnqueue, @@ -998,6 +1071,87 @@ export function MessageInput({ } } + if ( + !disabled && + !isEditingQueueItem && + !slashMenuOpen && + (e.key === "ArrowUp" || e.key === "ArrowDown") && + !e.metaKey && + !e.ctrlKey && + !e.altKey && + !e.shiftKey + ) { + const textarea = e.currentTarget as HTMLTextAreaElement + const value = textarea.value + const history = sendHistoryRef.current + + if (history.length > 0) { + const applyHistoryValue = ( + nextText: string, + nextIndex: number | null + ) => { + e.preventDefault() + setSendHistoryIndex(nextIndex) + setText(nextText) + requestAnimationFrame(() => { + const el = textareaRef.current + if (!el) return + const pos = el.value.length + el.selectionStart = el.selectionEnd = pos + }) + } + + const atStart = + textarea.selectionStart === 0 && textarea.selectionEnd === 0 + const atEnd = + textarea.selectionStart === value.length && + textarea.selectionEnd === value.length + + if (e.key === "ArrowUp" && atStart) { + if (sendHistoryIndex === null) { + if (value.length === 0) { + const nextIndex = history.length - 1 + applyHistoryValue(history[nextIndex] ?? "", nextIndex) + return + } + } else { + if (value.length === 0) { + const nextIndex = history.length - 1 + applyHistoryValue(history[nextIndex] ?? "", nextIndex) + return + } + + const current = history[sendHistoryIndex] + if (current && value === current) { + const nextIndex = Math.max(0, sendHistoryIndex - 1) + applyHistoryValue(history[nextIndex] ?? "", nextIndex) + return + } + } + } + + if (e.key === "ArrowDown" && atEnd && sendHistoryIndex !== null) { + if (value.length === 0) { + setSendHistoryIndex(null) + return + } + + const current = history[sendHistoryIndex] + if (!current || value !== current) return + + const isLast = sendHistoryIndex >= history.length - 1 + if (isLast) { + applyHistoryValue("", null) + return + } + + const nextIndex = sendHistoryIndex + 1 + applyHistoryValue(history[nextIndex] ?? "", nextIndex) + return + } + } + } + if (isEditingQueueItem && e.key === "Escape") { e.preventDefault() onCancelQueueEdit?.() @@ -1031,6 +1185,7 @@ export function MessageInput({ filteredSlashCommands, slashSelectedIndex, handleSlashSelect, + sendHistoryIndex, ] ) From 28086c8509a841d32a3065a8a546258c3879ec56 Mon Sep 17 00:00:00 2001 From: han <475166676@qq.com> Date: Sat, 14 Mar 2026 10:37:45 +0800 Subject: [PATCH 2/6] fix(chat): allow history keys when disconnected --- src/components/chat/message-input.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/chat/message-input.tsx b/src/components/chat/message-input.tsx index 5661d985..bc0ec59d 100644 --- a/src/components/chat/message-input.tsx +++ b/src/components/chat/message-input.tsx @@ -1072,7 +1072,6 @@ export function MessageInput({ } if ( - !disabled && !isEditingQueueItem && !slashMenuOpen && (e.key === "ArrowUp" || e.key === "ArrowDown") && From 5567fd31d3473632a594f46a72b94208fdef6365 Mon Sep 17 00:00:00 2001 From: han <475166676@qq.com> Date: Sat, 14 Mar 2026 12:47:49 +0800 Subject: [PATCH 3/6] fix(chat): support webkit Up/Down key values --- src/components/chat/message-input.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/chat/message-input.tsx b/src/components/chat/message-input.tsx index bc0ec59d..ef0151d7 100644 --- a/src/components/chat/message-input.tsx +++ b/src/components/chat/message-input.tsx @@ -1044,15 +1044,20 @@ export function MessageInput({ return } + const isArrowUp = + e.key === "ArrowUp" || e.key === "Up" || e.keyCode === 38 + const isArrowDown = + e.key === "ArrowDown" || e.key === "Down" || e.keyCode === 40 + if (slashMenuOpen && filteredSlashCommands.length > 0) { - if (e.key === "ArrowDown") { + if (isArrowDown) { e.preventDefault() setSlashSelectedIndex((i) => i < filteredSlashCommands.length - 1 ? i + 1 : 0 ) return } - if (e.key === "ArrowUp") { + if (isArrowUp) { e.preventDefault() setSlashSelectedIndex((i) => i > 0 ? i - 1 : filteredSlashCommands.length - 1 @@ -1074,7 +1079,7 @@ export function MessageInput({ if ( !isEditingQueueItem && !slashMenuOpen && - (e.key === "ArrowUp" || e.key === "ArrowDown") && + (isArrowUp || isArrowDown) && !e.metaKey && !e.ctrlKey && !e.altKey && @@ -1106,7 +1111,7 @@ export function MessageInput({ textarea.selectionStart === value.length && textarea.selectionEnd === value.length - if (e.key === "ArrowUp" && atStart) { + if (isArrowUp && atStart) { if (sendHistoryIndex === null) { if (value.length === 0) { const nextIndex = history.length - 1 @@ -1129,7 +1134,7 @@ export function MessageInput({ } } - if (e.key === "ArrowDown" && atEnd && sendHistoryIndex !== null) { + if (isArrowDown && atEnd && sendHistoryIndex !== null) { if (value.length === 0) { setSendHistoryIndex(null) return From 9acf767de235e475be180cf5becba43726fabb30 Mon Sep 17 00:00:00 2001 From: han <475166676@qq.com> Date: Sat, 14 Mar 2026 13:01:53 +0800 Subject: [PATCH 4/6] fix(chat): seed history from conversation turns --- src/components/chat/message-input.tsx | 92 ++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 10 deletions(-) diff --git a/src/components/chat/message-input.tsx b/src/components/chat/message-input.tsx index ef0151d7..a783816c 100644 --- a/src/components/chat/message-input.tsx +++ b/src/components/chat/message-input.tsx @@ -26,10 +26,11 @@ import { import { cn } from "@/lib/utils" import { matchShortcutEvent } from "@/lib/keyboard-shortcuts" import { useShortcutSettings } from "@/hooks/use-shortcut-settings" -import { readFileBase64 } from "@/lib/tauri" +import { getFolderConversation, readFileBase64 } from "@/lib/tauri" import { disposeTauriListener } from "@/lib/tauri-listener" import type { AvailableCommandInfo, + ContentBlock, PromptCapabilitiesInfo, PromptDraft, PromptInputBlock, @@ -166,6 +167,27 @@ function saveMessageInputSendHistory(scopeKey: string | null, items: string[]) { } } +function parseConversationIdFromDraftStorageKey( + draftStorageKey: string | null +): number | null { + if (!draftStorageKey) return null + const match = /^conv:[^:]+:(\d+)$/.exec(draftStorageKey) + if (!match) return null + const id = Number(match[1]) + return Number.isFinite(id) ? id : null +} + +function textFromBlocks(blocks: ContentBlock[]): string | null { + const parts: string[] = [] + for (const block of blocks) { + if (block.type !== "text") continue + if (!block.text.trim()) continue + parts.push(block.text) + } + const joined = parts.join("\n").trim() + return joined.length > 0 ? joined : null +} + function fileNameFromPath(path: string): string { return path.split(/[/\\]/).pop() || path } @@ -352,6 +374,45 @@ export function MessageInput({ ) setSendHistoryIndex(null) }, [effectiveDraftStorageKey]) + + useEffect(() => { + if (!effectiveDraftStorageKey) return + if (sendHistoryRef.current.length > 0) return + const conversationId = parseConversationIdFromDraftStorageKey( + effectiveDraftStorageKey + ) + if (!conversationId) return + + let cancelled = false + getFolderConversation(conversationId) + .then((detail) => { + if (cancelled) return + if (sendHistoryRef.current.length > 0) return + + const seeded: string[] = [] + let last: string | null = null + for (const turn of detail.turns) { + if (turn.role !== "user") continue + const next = textFromBlocks(turn.blocks) + if (!next) continue + if (next === last) continue + seeded.push(next) + last = next + } + + if (seeded.length === 0) return + const limited = seeded.slice(-MESSAGE_INPUT_SEND_HISTORY_LIMIT) + saveMessageInputSendHistory(effectiveDraftStorageKey, limited) + sendHistoryRef.current = limited + }) + .catch(() => { + // ignore + }) + + return () => { + cancelled = true + } + }, [effectiveDraftStorageKey]) const dragActiveRef = useRef(false) const canAttachImages = promptCapabilities.image @@ -1035,20 +1096,31 @@ export function MessageInput({ const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { + const isArrowUp = + e.key === "ArrowUp" || + e.key === "Up" || + (e.nativeEvent as KeyboardEvent).code === "ArrowUp" || + e.code === "ArrowUp" || + e.keyCode === 38 + const isArrowDown = + e.key === "ArrowDown" || + e.key === "Down" || + (e.nativeEvent as KeyboardEvent).code === "ArrowDown" || + e.code === "ArrowDown" || + e.keyCode === 40 + + if (e.nativeEvent.isComposing || composingRef.current) { + return + } + if ( - e.nativeEvent.isComposing || - composingRef.current || - e.key === "Process" || - e.keyCode === 229 + (e.key === "Process" || e.keyCode === 229) && + !isArrowUp && + !isArrowDown ) { return } - const isArrowUp = - e.key === "ArrowUp" || e.key === "Up" || e.keyCode === 38 - const isArrowDown = - e.key === "ArrowDown" || e.key === "Down" || e.keyCode === 40 - if (slashMenuOpen && filteredSlashCommands.length > 0) { if (isArrowDown) { e.preventDefault() From c1f52b2069bc4231de36258eaa0b547d41dea3f8 Mon Sep 17 00:00:00 2001 From: han <475166676@qq.com> Date: Sat, 14 Mar 2026 13:09:45 +0800 Subject: [PATCH 5/6] fix(chat): codex-like history navigation for multiline --- src/components/chat/message-input.tsx | 59 ++++++++++++--------------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/src/components/chat/message-input.tsx b/src/components/chat/message-input.tsx index a783816c..c3488418 100644 --- a/src/components/chat/message-input.tsx +++ b/src/components/chat/message-input.tsx @@ -1162,6 +1162,7 @@ export function MessageInput({ const history = sendHistoryRef.current if (history.length > 0) { + const hasSelection = textarea.selectionStart !== textarea.selectionEnd const applyHistoryValue = ( nextText: string, nextIndex: number | null @@ -1177,53 +1178,43 @@ export function MessageInput({ }) } - const atStart = - textarea.selectionStart === 0 && textarea.selectionEnd === 0 - const atEnd = - textarea.selectionStart === value.length && - textarea.selectionEnd === value.length - - if (isArrowUp && atStart) { - if (sendHistoryIndex === null) { - if (value.length === 0) { - const nextIndex = history.length - 1 - applyHistoryValue(history[nextIndex] ?? "", nextIndex) - return - } - } else { - if (value.length === 0) { - const nextIndex = history.length - 1 + if (sendHistoryIndex !== null && !hasSelection) { + const current = history[sendHistoryIndex] + if (current && value === current) { + if (isArrowUp) { + const nextIndex = Math.max(0, sendHistoryIndex - 1) applyHistoryValue(history[nextIndex] ?? "", nextIndex) return } - const current = history[sendHistoryIndex] - if (current && value === current) { - const nextIndex = Math.max(0, sendHistoryIndex - 1) + if (isArrowDown) { + const isLast = sendHistoryIndex >= history.length - 1 + if (isLast) { + applyHistoryValue("", null) + return + } + + const nextIndex = sendHistoryIndex + 1 applyHistoryValue(history[nextIndex] ?? "", nextIndex) return } } } - if (isArrowDown && atEnd && sendHistoryIndex !== null) { - if (value.length === 0) { - setSendHistoryIndex(null) - return - } - - const current = history[sendHistoryIndex] - if (!current || value !== current) return + const atStart = + textarea.selectionStart === 0 && textarea.selectionEnd === 0 - const isLast = sendHistoryIndex >= history.length - 1 - if (isLast) { - applyHistoryValue("", null) + if ( + sendHistoryIndex === null && + !hasSelection && + isArrowUp && + atStart + ) { + if (value.length === 0) { + const nextIndex = history.length - 1 + applyHistoryValue(history[nextIndex] ?? "", nextIndex) return } - - const nextIndex = sendHistoryIndex + 1 - applyHistoryValue(history[nextIndex] ?? "", nextIndex) - return } } } From 8e7b6e500764c501b525ddf3f69653ca21b8327b Mon Sep 17 00:00:00 2001 From: han <475166676@qq.com> Date: Mon, 16 Mar 2026 10:25:30 +0800 Subject: [PATCH 6/6] fix(chat): preserve IME arrow navigation --- src/components/chat/message-input.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/chat/message-input.tsx b/src/components/chat/message-input.tsx index c3488418..73092831 100644 --- a/src/components/chat/message-input.tsx +++ b/src/components/chat/message-input.tsx @@ -1113,11 +1113,7 @@ export function MessageInput({ return } - if ( - (e.key === "Process" || e.keyCode === 229) && - !isArrowUp && - !isArrowDown - ) { + if (e.key === "Process" || e.keyCode === 229) { return }