Commit d32842f
feat(site): cycle prompt history with up/down arrows (#25004)
Fixes
[CODAGT-319](https://linear.app/codercom/issue/CODAGT-319/support-prompt-history-cycling-with-up-arrow).
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
- **Up** when composer is empty: snapshot the (empty) draft and load the
most recent user prompt; subsequent **Up** presses load older prompts.
Clamp at oldest, no wrap.
- **Up** while non-empty and not yet cycling: pass through (caret
movement preserved).
- Once cycling, **Up / Down** are intercepted unconditionally because
the cycle text fully replaces editor contents. Exit explicitly via
Escape, by sending, or by typing.
- **Down** while cycling: load the next-newer prompt, or restore the
saved draft when past newest.
- **Escape** while cycling: exit cycle and restore the saved draft. This
also applies during streaming; the same keypress is stopped before it
reaches the interrupt handler, and a second Escape interrupts as before.
- **Typing / paste / drop / attach / send / `remountKey` change**: exit
cycle mode and clear the snapshot.
- Cycling is suppressed while `isEditingHistoryMessage`,
`editingQueuedMessageID !== null`, or the input is `disabled` /
`isLoading`.
- Empty `userPromptHistory` makes Up a no-op (no destructive fallback).
## Out of scope (filed as follow-ups if needed)
- Restoring file-reference chips / attachments on cycled messages — v1
cycles plain text only, matching the existing per-message destructive
Edit's `text` payload.
- `^N` / `^P` keybindings (per Cian's note in the Linear thread).
- Per-user "enable/disable history cycling" preference (per Rowan's
note).
- Cross-chat history; cycling is per-chat.
## 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 `useImperativeHandle` to delegate to `internalRef.current`
lazily 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 of `ChatMessageInputRef`.
Verified locally:
```
pnpm format
pnpm check
pnpm test:storybook src/pages/AgentsPage/components/AgentChatInput.stories.tsx # 41 passed
pnpm lint
```
## 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.
<details>
<summary>Implementation plan and decision log</summary>
The complete plan that drove this PR, including design alternatives
considered and edge cases:
```md
# CODAGT-319 — Up-arrow prompt history cycling
## Goal
Pressing the up-arrow key in the agent chat composer should cycle through the
user's previously-sent prompts in the current chat, terminal/Discord/iTerm2
style. Down-arrow steps forward; Escape exits cycle mode and restores the
in-progress draft. Cycling is non-destructive — it only populates the
composer with text the user can choose to resend, edit, or discard.
## Today's behaviour (and the regression)
- `ChatMessageInput` is a Lexical-based plain-text editor used inside `AgentChatInput.tsx`.
- `AgentChatInput.tsx` already wires an `ArrowUp` handler. When the composer is empty and not already editing, it calls `onEditLastUserMessage`.
- `onEditLastUserMessage` puts the user into a destructive "edit history" mode that warns "Editing will delete all subsequent messages and restart the conversation here.".
- Danielle's regression report ("shows me as editing but the input box is empty") indicates the destructive flow has a bug in addition to being the wrong UX for the request. We're replacing that path on the up-arrow, not patching it. The destructive edit remains accessible via the per-message hover Edit button.
## Design
### Behaviour
- Up when composer is empty: snapshot the (empty) draft and load the most recent user prompt. Subsequent Up loads older prompts, clamping at oldest. No wrap.
- Up while non-empty and not yet cycling: pass through. Matches existing gating.
- Once cycling, Up/Down are intercepted unconditionally. Exit via Escape, send, or typing.
- Down while cycling: next-newer or restore draft past newest.
- Escape while cycling: restore draft. During streaming, stop propagation so the same keypress does not interrupt; a second Escape interrupts as before.
- Typing/paste/drop/attach/send: exit cycle.
- Suppressed while isEditingHistoryMessage, editingQueuedMessageID !== null, or disabled/isLoading.
- No history => Up is a no-op.
### State
Local to `AgentChatInput.tsx`:
- `cycleIndex: number | null` — null means not cycling. 0 = newest user prompt.
- `cycleSavedDraft: string | null` — text restored on dismiss.
No localStorage persistence — refresh is a clean exit signal and the chat already has history server-side.
### Wiring
- New prop `userPromptHistory: readonly string[]` on `AgentChatInput`, newest-first.
- Removed `onEditLastUserMessage` prop entirely (its single call-site is being replaced). Removed dead `onEditUserMessage` prop on `ChatPageInput` (no longer needed since the destructive last-message shortcut is gone; the destructive Edit button uses a separate prop chain through `ChatPageTimeline`).
- `ChatPageContent.tsx` derives `userPromptHistory` from existing message store, filtered to `role === "user"` with non-empty `getEditableUserMessagePayload(message).text.trim()`.
### Reset triggers
`cycleIndex` and `cycleSavedDraft` reset on:
1. New `remountKey` (chat change, edit start/cancel).
2. Successful send.
3. Paste, drop, file attach.
4. User typing (detected via `handleContentChange` by comparing the incoming content to `currentCycleValueRef`, the last value applied programmatically).
### Out of scope
- Chip/attachment cycling.
- ^N/^P (Cian's note).
- Per-user toggle (Rowan's note).
- Cross-chat history.
```
</details>
---
> [!NOTE]
> This PR was created on behalf of @ibetitsmike by Coder Agents.
---------
Co-authored-by: Coder Agents <noreply@coder.com>1 parent ffe2595 commit d32842f
4 files changed
Lines changed: 292 additions & 37 deletions
File tree
- site/src/pages/AgentsPage
- components
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
556 | 556 | | |
557 | 557 | | |
558 | 558 | | |
559 | | - | |
560 | 559 | | |
561 | 560 | | |
562 | 561 | | |
| |||
Lines changed: 127 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
47 | 62 | | |
48 | 63 | | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
49 | 176 | | |
50 | 177 | | |
51 | 178 | | |
| |||
0 commit comments