Skip to content

feat: add Agent prompt history navigation - #784

Merged
marksftw merged 3 commits into
masterfrom
codex-maple-prompt-history-maple
Aug 18, 2026
Merged

feat: add Agent prompt history navigation#784
marksftw merged 3 commits into
masterfrom
codex-maple-prompt-history-maple

Conversation

@marksftw

Copy link
Copy Markdown
Contributor

Summary

  • Add Terminal-style Arrow Up/Arrow Down prompt recall to both Agent Mode composer placements.
  • Derive recall entries from the selected task's canonical user-message timeline, preserving order, duplicates, multiline text, and Unicode without creating a second history store.
  • Anchor active navigation against live timeline updates and reset it on edits, pointer movement, sends, scope changes, and authoritative history replacement.
  • Keep Chat Mode unchanged.

Why

Agent users often reuse an earlier instruction with small edits. The prompts already exist in the account- and task-scoped Agent timeline, but the composer had no navigation state for recalling them.

Validation

  • nix develop .#ci -c ./scripts/ci/frontend.sh — passed formatting, TypeScript, and all 666 tests; ESLint completed with 0 errors and 13 existing warnings.
  • Pre-commit production frontend build passed and the hook reran all 666 tests successfully.
  • Focused Agent prompt-history tests cover filtering, failed/cancelled runs, duplicates, no-wrap navigation, modifier/selection/IME guards, multiline recall, and stable snapshots.
  • Unsigned macOS QA app build and exact-bundle launch succeeded using the managed workspace overlay.

Closes #782

@marksftw

Copy link
Copy Markdown
Contributor Author

Manual QA was successful. Agent Mode allows Arrow Up and Arrow Down to navigate prompt history. Chat Mode does not allow prompt-history navigation, confirming it remains unchanged.

@marksftw

Copy link
Copy Markdown
Contributor Author

Code review report

One issue is worth fixing before merge:

P2 — routine run completion exits active prompt-history navigation

replaceSessionTimeline in frontend/src/components/AgentMode.tsx resets prompt-history navigation whenever it installs an active-session snapshot. A normal runFinished event asynchronously reloads the persisted timeline through that same path.

The composer remains enabled while a run is active, so this sequence is possible:

  1. Recall a prompt with Arrow Up while the Agent run is still active.
  2. The run finishes and its routine timeline reload succeeds.
  3. The recalled text remains in the composer, but navigation state is cleared.
  4. Subsequent Arrow Up/Arrow Down no longer navigate because history navigation can only begin from an empty composer.

This violates the requirement that live assistant/tool events not change the selected history entry. The narrow fix is to preserve navigation during routine run-finish reconciliation. True historyReplaced, task/account changes, sends, edits, and pointer movement already have explicit reset paths. A focused regression test at the run-finish boundary would be high-value.

Advisory only — not necessary to fix

The keydown handler already returns during IME composition, while the pure history helper also checks isComposing. That is minor defensive duplication, but it is harmless and keeps the helper self-contained. I do not recommend changing it for this PR.

Validation

  • Three independent reviews converged on the P2 finding.
  • Full frontend gate passed: formatting, TypeScript, 0 lint errors, and all 666 tests.
  • Focused prompt-history suite passed: 6 tests, 26 assertions.
  • git diff --check passed.
  • No other validated correctness, account-isolation, complexity, intermediary-code, comment, or low-value-test issues were found.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 15, 2026

Copy link
Copy Markdown

Deploying maple with  Cloudflare Pages  Cloudflare Pages

Latest commit: 10feca6
Status: ✅  Deploy successful!
Preview URL: https://2d86ff89.maple-ca8.pages.dev
Branch Preview URL: https://codex-maple-prompt-history-m.maple-ca8.pages.dev

View logs

@marksftw

Copy link
Copy Markdown
Contributor Author

Addressed in commit 75e2723.

I agree with the P2 finding. A routine runFinished reconciliation is not the semantic historyReplaced event, so installing that persisted timeline now refreshes entries for the next history-navigation session without clearing the active snapshot. The explicit exit paths remain intact for edits, sends, pointer movement, task/account changes, deletion, and true historyReplaced events.

I kept the defensive IME check unchanged, as recommended.

I intentionally did not add a synthetic run-finish policy hook solely for a test: the existing pure snapshot test cannot catch this component-side reset, while threading prompt-history-specific policy through the thought-run helper would couple unrelated layers. The production fix remains the narrow semantic change.

Validation:

  • Focused Agent tests: 10 passed, 64 assertions.
  • Complete frontend gate: formatting, typecheck, 0 lint errors, and 666 tests passed.
  • Pre-commit production frontend build and repeated 666-test suite passed.
  • git diff --check passed.

@marksftw

Copy link
Copy Markdown
Contributor Author

Final manual QA after code review

Manual QA passed on the post-review desktop build:

  • Created a new Agent task and submitted an initial prompt.
  • After the response completed, pressed the Up arrow and confirmed the first prompt appeared in prompt history.
  • Submitted additional prompts and used the Up and Down arrows to cycle through the history.
  • Reused a previous prompt recalled from history.
  • Recalled a previous prompt, edited it, and submitted the edited version. Confirmed the original prompt remained unchanged in its earlier history position and the edited prompt became the newest entry in the history stack.

Result: Passed.

@marksftw

Copy link
Copy Markdown
Contributor Author

QA UI Automation Report — PASS

PR: #784 — feat: add Agent prompt history navigation
Tested head: 75e2723
Date: 2026-08-15
Overall result: PASS — no blocking or non-blocking prompt-history defects found in the exercised desktop scope.

Environment and build verification

  • Authenticated macOS Maple desktop app, version 3.3.4.
  • The running process was verified to come from this workspace's exact unsigned QA bundle at frontend/src-tauri/target/release/bundle/macos/Maple.app, not from another worktree.
  • Local checkout was clean and exactly synchronized with the PR head (+0/-0 at 75e2723).
  • The bundle was built after the tested commit and immediately before launch. The app does not embed a source commit SHA, so source identity cannot be proven from the binary alone; exact worktree path, clean checkout, and build timing provide strong evidence that the tested app contains this head.
  • Desktop environment used the managed shared-development OpenSecret, billing, and feature-flag endpoints.
  • Automation drove the real desktop UI through its accessibility surface and keyboard events. Agent messages were sent to the authenticated server-side account.

UI results

Area Scenario Result
Agent history order From an existing task containing first, second, third, duplicate second, and fourth: blank + Up recalled fourth, then the later second, third, the earlier second, and first. PASS
Agent boundaries Additional Up at the oldest entry stayed on the oldest entry. Down walked forward through every entry, including both duplicate positions; Down past newest restored an empty composer and an additional Down remained empty. PASS
Duplicate preservation The two identical second prompt entries occupied distinct history positions and were traversed independently. PASS
New-task composer A fresh Agent task did not recall prompts from the previously selected task after its carried draft was cleared. PASS
Draft protection Up in a non-empty manually typed draft left the draft unchanged. PASS
Selection guard Up while recalled text was selected did not navigate to an older history entry. PASS
Modifier guards Shift+Up, Option+Up, Control+Up, and Command+Up from an empty Agent composer did not start history recall. A following unmodified Up still recalled the newest prompt. PASS
Multiline and Unicode Submitted and recalled exact text containing a newline, Greek characters, emoji, CJK text, and an em dash: QA-PH-B-20260815T1751 αβ 👩‍💻, followed by line-2 中文 — reply exactly B on the second line. PASS
Focus/caret behavior Recall retained composer focus. Typing immediately after recall appended at the end of the multiline prompt, confirming the caret was placed at the end. Recall did not auto-submit. PASS
Edit and reuse Recalled the multiline prompt, appended [EDITED], and verified subsequent Up did not traverse while the edited draft remained non-empty. Submitting it created one new newest entry while the original remained unchanged in its prior position. PASS
Pointer exit Clicking inside a recalled prompt exited navigation; a subsequent Up did not replace the non-empty composer with an older entry. PASS
Task isolation Task A recalled only its own QA-PH-* entries. Task B recalled its own newest fourth prompt. Switching back restored Task A's newest entry. PASS
Mode isolation Switching Agent → Chat → Agent preserved the selected Agent task's history. Chat never recalled an Agent prompt. PASS
Reload persistence Reloaded the desktop webview with the Agent composer empty; Up recalled the persisted newest accepted/cancelled Agent prompt. PASS
Accepted cancellation Sent QA-PH-D-CANCEL-20260815T1802..., cancelled the active run after the user message was accepted, and confirmed it remained the newest recallable prompt. PASS
Desktop key regressions Enter continued to submit in Agent mode. Shift+Enter continued to create a newline in the composer. PASS
Non-text state Across recall operations, visible model, permission mode, project root, and MCP selection remained GLM 5.2, Read only, test1, and 0 MCP servers; recall changed only the composer text. PASS

Critical run-completion regression test

This specifically exercised the behavior fixed by 75e2723:

  1. Sent a deliberately longer Agent prompt, QA-PH-C-LIVE-20260815T1755..., which requested a 30-point response.
  2. While the run was visibly active and the composer was empty, pressed Up and recalled the in-flight C prompt.
  3. Pressed Up again to move to the prior edited B prompt.
  4. The Agent run completed during this navigation boundary and the authoritative timeline reconciled.
  5. The composer remained on B instead of being reset or jumping back to C.
  6. Down then returned to C, and another Down restored the empty composer.

Result: PASS. Routine assistant streaming and run completion did not disturb or shift the active history snapshot.

Chat negative controls

Tested both Chat composer placements:

  • New-conversation centered composer: blank Up and Down remained blank.
  • Existing conversation footer composer containing prior user history: blank Up and Down remained blank.
  • A typed QA-CHAT-DRAFT-UNCHANGED draft remained unchanged after Up.
  • No Agent prompt leaked into Chat, and no Chat prompt-history behavior was introduced.

Result: PASS — history behavior is Agent-only as intended.

Focused automated validation

All commands ran in Maple's pinned .#ci Nix environment:

nix develop .#ci -c bash -lc 'cd frontend && bun --no-env-file test ./src/components/agent/agentPromptHistory.test.ts'
nix develop .#ci -c bash -lc 'cd frontend && bun --no-env-file test ./src/components/AgentMode.test.ts'
nix develop .#ci -c bash -lc 'cd frontend && bun --no-env-file test ./src/components/chatComposerListContinuation.test.ts'

Results:

  • Agent prompt history: 6 tests passed, 26 assertions
  • AgentMode run-finish helpers: 4 tests passed, 38 assertions
  • Composer list continuation regressions: 29 tests passed, 31 assertions
  • Total: 39 tests passed, 95 assertions, 0 failures

These focused suites complement the rendered desktop testing; the pure helper tests alone do not prove the component-side run-completion behavior, which is why that boundary was also exercised directly in the live UI.

Side effects and cleanup state

  • Created one Agent task titled “Exact Reply Protocol Test” containing the uniquely labeled QA-PH-* evidence prompts.
  • The task includes one intentionally cancelled accepted run.
  • No Chat messages were sent.
  • No account, model, project, MCP, or permission settings were changed.
  • The final Agent composer was left blank.
  • Worktree remained clean after testing.
  • No errors correlated with prompt-history interactions were observed in the desktop app log.

Not manually exercised

  • Live IME composition behavior (the pure controller test covers the IME guard).
  • Logout/login or switching to a second account.
  • Permanent task deletion or Agent-history clearing.
  • Pre-acceptance rejection/failure where no canonical user message is created.
  • Compact/mobile layout behavior.
  • Authoritative history replacement caused by compaction or external synchronization.

Within the requested desktop Agent-versus-Chat scope, the PR behaves as specified and the high-risk run-completion regression is resolved.

@marksftw
marksftw marked this pull request as ready for review August 15, 2026 23:30
@marksftw

Copy link
Copy Markdown
Contributor Author

Already rebased on latest master. Ready for review.

@AnthonyRonning

Copy link
Copy Markdown
Contributor

Review notes

Two items from a local review of 75e2723. Everything else in the prompt-history helper and AgentMode wiring looked sound: snapshot navigation, the runFinished preserve fix, both composer placements, Chat Mode isolation, and the usual send/edit/pointer/task/account exits.

historyReplaced can leave recall empty until the next successful timeline replace

Not a merge blocker.

On historyReplaced, AgentMode resets navigation, sets promptHistoryReplacementSessionRef for the active session, and wipes promptHistoryEntriesRef to [] before reloading. The marker is only cleared inside a successful replaceSessionTimeline, after the revision fence. If loadSession throws, the catch only surfaces an error. If a live timelineItem bumps the revision during that await — plausible when Goose compact/rewrite happens mid-run and then continues streaming — the replace returns false and the marker stays armed. While it equals the active session, the layout effect will not copy live promptHistoryEntries back into the ref, so Up/Down silently no-ops.

This is not a leak of another task or account, and it is not the earlier runFinished reset. Routine run completion still goes through replaceSessionTimeline and should heal recall for the common case. The bad window is mostly “compaction mid-run, recall dead until the run finishes,” and it only sticks if that terminal reload also fails. Fine to land as-is. If you touch this path later, clear the marker on the failed/raced replace without a session-id-only finally that could dismiss a newer replacement, and consider dropping the eager [] wipe so a failed rewrite degrades to the timeline still on screen.

ACP / CRLF vs textarea value (needs your call)

Needs a bit of investigation from you before deciding whether to fix.

Continuation stays in history mode only while event.currentTarget.value === navigation.entries[index]. Maple-typed prompts should be fine: the composer already lives in \n, and send_message just trims. ACP prompt_text joins text blocks with \n\n but does not normalize \r / \r\n inside a block. A persisted ACP prompt can therefore keep Windows line endings. On first Up, React sets that canonical string; the textarea’s .value getter then normalizes line endings to \n (HTML textarea). The next Up/Down sees a mismatch, treats it as an edit, and exits. The recalled text stays in the box; further arrows only move the caret.

ACP is not on for users yet, but it is close. Issue #782 says ACP-submitted prompts in the same task are eligible for recall, so this is on the intended contract even if desktop-only QA would not hit it. Worth checking whether any ACP client you are about to ship can persist \r\n (or \r) in a user-visible prompt, and how cheap a compare-time newline normalize would be — keep the stored canonical value, compare on \n-normalized strings, place the caret from the DOM value. If that is a small local change and ACP might land on this before a follow-up, I would take it. If you are confident the ACP prompt path already cannot retain \r, or you would rather handle newline canonicalization on the ACP ingest side, this does not need to block the PR.

@AnthonyRonning AnthonyRonning left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other two comments are likely small and not blocking but I ran through QA myself and history navigation doesn't seem to work in a not-new chat unless you click into the box again. You can type into that box immediately after sending the previous message, but the arrow navigation doesn't work unless you click into that box again.

This may or may not be an existing issue that never surfaced until this. The sus behavior is that the orange cursor remains in the text box after hitting enter to send. But the text box does not have an active orange border like it usually does when it is selected for text.

@marksftw

Copy link
Copy Markdown
Contributor Author

Working on these code review fixes. And yes I see the focus thing you're talking about. It pre-dates this PR but should be fixed, I'll do it here.

@marksftw

Copy link
Copy Markdown
Contributor Author

Addressed Anthony’s three review findings in commit 10feca6.

  1. Post-send focus: I agree this was a real, reachable issue. Enter submission briefly disables the textarea, and the first send can also replace the centered composer with the footer composer. Enter-origin sends now arm a scoped focus handoff that waits for submission to unlock and, when needed, for the accepted timeline row/composer swap. It is cancelled by task interaction, pointer focus elsewhere, window blur, or capture-phase keyboard focus moving to another control, so it does not steal deliberate focus. Button-origin sends remain unchanged.

  2. historyReplaced recovery: I agree with the failure/race concern. Replacement loads now use identity-scoped attempts with the prior eligible entries as a fallback. A failed load or revision-rejected replace restores that fallback only when the same attempt still owns the active task; a newer attempt, task/account switch, deletion, or successful authoritative replacement invalidates it. This avoids both a stranded empty recall list and cross-task restoration.

  3. ACP CR/LF: I confirmed that ACP text blocks can preserve internal \r\n or bare \r through persistence. History continuation now applies textarea-equivalent line-ending normalization only for the comparison, while preserving the canonical stored/recalled prompt unchanged. Deferred caret placement uses the DOM textarea value length.

Chat Mode remains untouched.

Validation on 10feca6:

  • Production frontend build passed.
  • Complete frontend gate passed: 680 tests, 0 failures, 2,152 assertions.
  • Focused Agent suites passed: 24 tests, 108 assertions.
  • Prettier, TypeScript, ESLint (0 errors), and git diff --check passed.

@AnthonyRonning AnthonyRonning left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works really well and code is great! Merge whenever.

@marksftw
marksftw merged commit 16d9ed7 into master Aug 18, 2026
19 checks passed
@marksftw
marksftw deleted the codex-maple-prompt-history-maple branch August 18, 2026 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use arrow keys up and down in the input box to access prompt history

2 participants