feat(site): cycle prompt history with up/down arrows#25004
Conversation
Pressing up-arrow in the agent chat composer now cycles through prior user prompts in the chat (terminal/Discord/iTerm2 style). Down-arrow steps forward, Escape exits cycling and restores the in-progress draft. Cycling is non-destructive: the per-message hover Edit button is still the destructive truncate-and-edit path. Replaces the previous up-arrow shortcut that immediately entered destructive history-edit mode (CODAGT-319). Co-authored-by: Coder Agents <noreply@coder.com>
💡 Codex Reviewcoder/site/src/pages/AgentsPage/components/ChatPageContent.tsx Lines 286 to 288 in 1fdcfe4 Building ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
Code Review
Reviewed the prompt history cycling implementation. The feature is well-designed and fixes both the regression (empty editing state) and the UX issue with the destructive up-arrow shortcut. The Storybook play functions are thorough and cover the important scenarios. A few things worth addressing:
Missing resetPromptCycle() in handleStartRecording (line 721): handleStartRecording is the only user-action path that does not explicitly call resetPromptCycle(). Speech input does implicitly exit cycling because the speech useEffect calls editor.clear() + editor.insertText(), which fires handleContentChange with different content, triggering the cycle reset. But relying on that indirection makes the intent harder to audit. Every other exit path (file select, paste, drop, send, + menu file pick) calls resetPromptCycle() explicitly. Adding it to handleStartRecording would be consistent.
…cling - Drop redundant isApplyingCycleValueRef/applyingCycleValueRef pair; the one-shot flag had a subtle fragility where a Lexical normalisation change before the main onChange would consume it early. The remaining currentCycleValueRef compares on every change, which is what we want. - Add explicit e.stopPropagation() on the cycle-mode Escape handler so intent does not depend on the outer composer Escape coincidentally being a no-op in this state. - Reset the cycle on speech-recording start for consistency with every other user-action path that exits cycling. - Preserve original message text in userPromptHistory; trim only for the emptiness check so cycling and re-sending reproduces what the user actually sent (boundary whitespace can be intentional). - Comment the newest-first invariant on the userPromptHistory loop.
There was a problem hiding this comment.
The cycling state machine is well-designed. Entry, clamping, draft restoration, and exit-on-typing are all covered by real assertions against the Lexical DOM. The removal of onEditLastUserMessage and onEditUserMessage on ChatPageInput is clean; the destructive Edit button continues to work through ConversationTimeline. Pariston traced every branch of the state machine and could not construct a simpler alternative.
Severity count: 2 P2, 7 P3, 3 Nit.
The two P2s are about unrelated diff changes (DEREM-5) and an Escape trap during streaming (DEREM-4). The P3s cluster into three themes: (1) Escape exit from cycling is blocked or misdirected in edge states (DEREM-6, related to DEREM-4), (2) the three-ref cycle-detection protocol is correct but undocumented, flagged independently by four reviewers (DEREM-7), and (3) test coverage gaps for suppression conditions and the streaming Escape path (DEREM-1, DEREM-11).
Pariston: "I tried to build a case that a simpler fix exists and could not. The cycling state is local, the reset triggers are comprehensive, and the tests cover the main interaction paths."
Ging-ts, Ging-react, Robin, and Luffy returned no findings.
site/src/pages/AgentsPage/components/AgentChatInput.tsx:721
P3 [DEREM-10] handleStartRecording captures the editor's current value as preRecordingValue. When cycling, that value is the historical prompt, not the user's actual draft.
As speech.transcript changes, the effect at line 398 builds preRecordingValue + transcript, embedding the historical prompt in the result. The user's speech input is silently prepended with text they didn't dictate. Clicking the microphone does not call resetPromptCycle() or restoreCycleDraft().
"The interaction (cycle through history, then click mic) is unlikely. Fix: call
restoreCycleDraft()whencycleIndex !== nullat the top ofhandleStartRecording, so the editor shows the saved draft before capturing it." (Pariston)
🤖
🤖 This review was automatically generated with Coder Agents.
There was a problem hiding this comment.
Code Review
Reviewed the prompt history cycling implementation (4 files). The design is solid: replacing the buggy destructive ArrowUp shortcut with non-destructive terminal-style cycling is the right call. The state machine (cycleIndex + cycleSavedDraft + currentCycleValueRef) handles the key edge cases correctly, and the Storybook play functions cover the main scenarios well.
A few things worth addressing:
restoreCycleDraftleavescurrentCycleValueRef.currentnon-null (set to draft value instead of cleared).resetPromptCyclebody is copy-pasted into theuseEffect, creating a maintenance hazard.- The
isStreamingomission fromisPromptCyclingSuppresseddeserves a short explanatory comment. PromptHistoryDemohas noplayfunction — intentional but deviates from convention.- Minor:
messages[index]guard inChatPageContent.tsxis always-truthy at the TypeScript level.
- Move Escape-exits-cycle handler above isPromptCyclingSuppressed so the user can always exit cycling, even if isDisabled or isLoading flips true mid-cycle (DEREM-6). - Escape during streaming now exits cycling cleanly without also interrupting the stream; stopPropagation keeps the outer composer Escape handler from firing on the same keypress. A subsequent Escape (no longer cycling) interrupts as before (DEREM-4). - Snapshot userPromptHistory at cycle entry so a new message arriving mid-cycle does not silently shift the indexed prompt (DEREM-8). - Add three new tests: PromptHistorySuppressedWhileDisabled, PromptHistorySuppressedWhileLoading, and an extension to the typing-exit story that verifies the second cycle's draft snapshot is the new empty state, not the previous prompt. - Use messages.at(index) in ChatPageContent for self-documenting T | undefined typing. - Add explanatory comments above the cycle-detection protocol and the isStreaming exclusion in isPromptCyclingSuppressed. - Revert pre-existing emdash and non-null cleanup unrelated to the feature (and their accidental comma splices); remove the no-play-function PromptHistoryDemo story that supported the manual-UAT walkthrough.
There was a problem hiding this comment.
Code Review
Reviewed PR #25004: prompt history cycling with up/down arrows in the agent chat composer. The implementation is well-structured and the Storybook test coverage is thorough. Found 2 nitpick-level issues.
Overall Assessment
The core logic is sound:
- The
cycleHistorySnapshotRefsnapshot correctly prevents out-of-bounds access if new messages arrive during cycling. - The content-comparison trick in
handleContentChange(comparing againstcurrentCycleValueRef.current) correctly distinguishes programmaticsetValuecalls from user input without relying on committed React state. - Escape propagation is correctly stopped to avoid accidentally interrupting a stream with the first Escape press.
- The
useImperativeHandlerewrite from eager capture to lazy delegation is a legitimate fix for the stale-ref crash after remounts.
There was a problem hiding this comment.
Code Review
Reviewed PR #25004 at updated commit 5232a255. The onChange comment has been corrected per the previous review — the React-batching explanation is now accurate. One nitpick remains.
Summary
Implementation is solid. The cycling state machine, snapshot-based history isolation, and ref-comparison trick in handleContentChange all look correct. Storybook coverage is comprehensive.
There was a problem hiding this comment.
Code Review
Reviewed prompt history cycling implementation (CODAGT-319). Found 2 nitpicks.
The overall design is solid. Key things done well:
cycleHistorySnapshotRefcorrectly prevents a new incoming message from shifting the indexed prompt mid-cycle.- The cycle-detection protocol in
handleContentChange(comparingcontentagainstcurrentCycleValueRef.current) is correct; setting the ref before callingeditor.setValue()inapplyCycleValueis essential and the ordering is right. - Placing the Escape-exits-cycle handler before
isPromptCyclingSuppressedso the user can always exit cycling even whenisDisabled/isLoadingflips true mid-cycle is the right call. - The
useImperativeHandlelazy-delegation rewrite is a real bug fix for stale refs after remounts. - Storybook coverage is thorough.
- handleDrop reset moved past both the no-onAttach/empty-files guard and the no-attachable-files filter so cycling only exits when the drop actually mutates the composer. - Add a 'Keep in sync with resetPromptCycle above.' comment so the inlined remountKey effect body stays coupled to the helper.
There was a problem hiding this comment.
Code Review
Reviewed prompt history cycling with up/down arrows. The implementation is well-structured with thorough test coverage via Storybook play functions. The useImperativeHandle fix is a solid bug fix. Found 2 nitpicks.
Overall: LGTM with minor notes.
|
@codex review |
|
Codex Review: Didn't find any major issues. More of your lovely PRs please. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Fixes CODAGT-319.
Pressing the up-arrow key in the agent chat composer now cycles through prior user prompts in the chat (terminal/Discord/iTerm2 style). Down-arrow steps forward, Escape exits cycling and restores the in-progress draft. Cycling is non-destructive: the per-message hover Edit button is still the destructive truncate-and-edit path.
Replaces the previous up-arrow shortcut that immediately entered destructive history-edit mode (and which had a regression where the composer rendered as "editing" with an empty input box).
Behaviour
remountKeychange: exit cycle mode and clear the snapshot.isEditingHistoryMessage,editingQueuedMessageID !== null, or the input isdisabled/isLoading.userPromptHistorymakes Up a no-op (no destructive fallback).Out of scope (filed as follow-ups if needed)
textpayload.^N/^Pkeybindings (per Cian's note in the Linear thread).Tests
New Storybook play functions in
AgentChatInput.stories.tsx:PromptHistoryCycling— Up cycles older, clamps at oldest; Down returns to newer / draft; Escape restores draft.PromptHistoryCyclingExitsOnTyping— typing exits cycle mode; subsequent Up snapshots the fresh empty draft and Down restores it.NoPromptHistoryUpArrowIsNoOp— empty history → Up is a no-op.PromptHistorySuppressedWhileEditingHistoryMessage— cycling does not engage while history-editing.PromptHistorySuppressedWhileDisabled— cycling does not engage while disabled.PromptHistorySuppressedWhileLoading— cycling does not engage while loading.Implementation notes
Also rewrites
useImperativeHandleto delegate tointernalRef.currentlazily on every call instead of capturing it eagerly at factory time. The old code crashed when methods were called after a remount because the captured ref was stale; the new wrapper sees the current Lexical instance. Behavior changes from throw-on-null to silent no-op, which matches every other consumer ofChatMessageInputRef.Verified locally:
Manual UAT
A 13-case manual UAT covering cycle entry/exit, clamping, draft restoration, no-history no-op, suppression while editing a history message, and the send-button enable state — all PASS. Spec lives at the deleted artifact branch; happy to re-attach if reviewers want it.
Implementation plan and decision log
The complete plan that drove this PR, including design alternatives considered and edge cases:
Note
This PR was created on behalf of @ibetitsmike by Coder Agents.