Skip to content

Commit 3e774c5

Browse files
authored
🤖 Add up arrow to edit last user message (#272)
Pressing up arrow on an empty input now edits the last user message, providing a quick way to iterate without clicking edit. ## Changes - **ChatInput**: Added keyboard handler for up arrow when input is empty - **AIView**: Added `handleEditLastUserMessage` callback that finds last user message ## Cleanup While implementing this, also cleaned up related code: - Fixed placeholder to use `CANCEL_EDIT` keybind instead of `CANCEL` - Removed redundant empty `CANCEL` keybind handler - Removed redundant `NEW_LINE` handler (`matchesKeybind` already handles Shift+Enter) ## Testing Manually test: 1. Send a message 2. Clear input and press up arrow → should edit last message 3. Type something and press up arrow → should move cursor normally 4. Press Shift+Enter → should still insert newline --- _Generated with `cmux`_
1 parent 9ed91d6 commit 3e774c5

File tree

2 files changed

+45
-19
lines changed

2 files changed

+45
-19
lines changed

src/components/AIView.tsx

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,26 @@ const AIViewInner: React.FC<AIViewProps> = ({
256256
setEditingMessage({ id: messageId, content });
257257
}, []);
258258

259+
const handleEditLastUserMessage = useCallback(() => {
260+
if (!workspaceState) return;
261+
const mergedMessages = mergeConsecutiveStreamErrors(workspaceState.messages);
262+
const lastUserMessage = [...mergedMessages]
263+
.reverse()
264+
.find((msg): msg is Extract<DisplayedMessage, { type: "user" }> => msg.type === "user");
265+
if (lastUserMessage) {
266+
setEditingMessage({ id: lastUserMessage.historyId, content: lastUserMessage.content });
267+
setAutoScroll(false); // Show jump-to-bottom indicator
268+
269+
// Scroll to the message being edited
270+
requestAnimationFrame(() => {
271+
const element = contentRef.current?.querySelector(
272+
`[data-message-id="${lastUserMessage.historyId}"]`
273+
);
274+
element?.scrollIntoView({ behavior: "smooth", block: "center" });
275+
});
276+
}
277+
}, [workspaceState, contentRef, setAutoScroll]);
278+
259279
const handleCancelEdit = useCallback(() => {
260280
setEditingMessage(undefined);
261281
}, []);
@@ -464,12 +484,18 @@ const AIViewInner: React.FC<AIViewProps> = ({
464484

465485
return (
466486
<React.Fragment key={msg.id}>
467-
<MessageRenderer
468-
message={msg}
469-
onEditUserMessage={handleEditUserMessage}
470-
workspaceId={workspaceId}
471-
isCompacting={isCompacting}
472-
/>
487+
<div
488+
data-message-id={
489+
msg.type !== "history-hidden" ? msg.historyId : undefined
490+
}
491+
>
492+
<MessageRenderer
493+
message={msg}
494+
onEditUserMessage={handleEditUserMessage}
495+
workspaceId={workspaceId}
496+
isCompacting={isCompacting}
497+
/>
498+
</div>
473499
{isAtCutoff && (
474500
<EditBarrier>
475501
⚠️ Messages below this line will be removed when you submit the edit
@@ -532,6 +558,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
532558
isCompacting={isCompacting}
533559
editingMessage={editingMessage}
534560
onCancelEdit={handleCancelEdit}
561+
onEditLastUserMessage={handleEditLastUserMessage}
535562
canInterrupt={canInterrupt}
536563
onReady={handleChatInputReady}
537564
/>

src/components/ChatInput.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export interface ChatInputProps {
129129
isCompacting?: boolean;
130130
editingMessage?: { id: string; content: string };
131131
onCancelEdit?: () => void;
132+
onEditLastUserMessage?: () => void;
132133
canInterrupt?: boolean; // Whether Esc can be used to interrupt streaming
133134
onReady?: (api: ChatInputAPI) => void; // Callback with focus method
134135
}
@@ -335,6 +336,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
335336
isCompacting = false,
336337
editingMessage,
337338
onCancelEdit,
339+
onEditLastUserMessage,
338340
canInterrupt = false,
339341
onReady,
340342
}) => {
@@ -809,13 +811,16 @@ export const ChatInput: React.FC<ChatInputProps> = ({
809811
}
810812
}
811813

812-
// Handle escape - let VimTextArea handle it (for Vim mode transitions)
813-
// Edit canceling is handled by Ctrl+Q above
814-
// Stream interruption is handled by Ctrl+C (INTERRUPT_STREAM keybind)
815-
if (matchesKeybind(e, KEYBINDS.CANCEL)) {
816-
// Do not preventDefault here: allow VimTextArea or other handlers (like suggestions) to process ESC
814+
// Handle up arrow on empty input - edit last user message
815+
if (e.key === "ArrowUp" && !editingMessage && input.trim() === "" && onEditLastUserMessage) {
816+
e.preventDefault();
817+
onEditLastUserMessage();
818+
return;
817819
}
818820

821+
// Note: ESC handled by VimTextArea (for mode transitions) and CommandSuggestions (for dismissal)
822+
// Edit canceling is Ctrl+Q, stream interruption is Ctrl+C
823+
819824
// Don't handle keys if command suggestions are visible
820825
if (
821826
showCommandSuggestions &&
@@ -825,13 +830,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
825830
return; // Let CommandSuggestions handle it
826831
}
827832

828-
// Handle newline
829-
if (matchesKeybind(e, KEYBINDS.NEW_LINE)) {
830-
// Allow newline (default behavior)
831-
return;
832-
}
833-
834-
// Handle send message
833+
// Handle send message (Shift+Enter for newline is default behavior)
835834
if (matchesKeybind(e, KEYBINDS.SEND_MESSAGE)) {
836835
e.preventDefault();
837836
void handleSend();
@@ -841,7 +840,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
841840
// Build placeholder text based on current state
842841
const placeholder = (() => {
843842
if (editingMessage) {
844-
return `Edit your message... (${formatKeybind(KEYBINDS.CANCEL)} to cancel edit, ${formatKeybind(KEYBINDS.SEND_MESSAGE)} to send)`;
843+
return `Edit your message... (${formatKeybind(KEYBINDS.CANCEL_EDIT)} to cancel, ${formatKeybind(KEYBINDS.SEND_MESSAGE)} to send)`;
845844
}
846845
if (isCompacting) {
847846
return `Compacting... (${formatKeybind(KEYBINDS.INTERRUPT_STREAM)} to cancel)`;

0 commit comments

Comments
 (0)