Skip to content

Recall sent prompts with the arrow keys in the chat composer - #915

Merged
realtonyyoung merged 2 commits into
mainfrom
capacitor/agent-459e29e77b2445
Sep 12, 2026
Merged

realtonyyoung merged 2 commits into
mainfrom
capacitor/agent-459e29e77b2445

Conversation

@realtonyyoung

Copy link
Copy Markdown
Collaborator

AI-2731 (no GitHub issue exists for this one; the Linear issue is the tracker record)

What & why

In the desktop app's chat tab, ↑ in an empty composer recalls the most recent prompt sent from that tab, repeated ↑ walks older, and ↓ walks newer until it restores the empty draft, the way Claude Desktop and most chat apps behave. History is in-memory, per tab, for this app run; consecutive duplicates collapse and it is capped at 100 entries. A prompt the server rejected is not recorded.

Where to look

Recall starts only from a blank composer and continues only while the box still shows the recalled text unchanged; any edit turns it back into an ordinary draft. Inside a multi-line recall, ↑/↓ stay the TextBox's own caret moves until the caret sits on the first/last line, so long prompts remain editable.

Verification

dotnet run --project test/Capacitor.App.Tests.Unit -- --treenode-filter '/*/*/ComposerHistoryTests|ChatTabViewModelTests|ChatTabViewSmokeTests/*'
Test run summary: Passed!  total: 96  failed: 0  succeeded: 96

🤖 Generated with Claude Code

Recall starts only from a blank box and steps only while the caret sits on the first or last line, so a draft being typed and the caret moves inside a multi-line recall keep their TextBox meaning.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add arrow-key prompt recall to the chat composer

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Adds per-tab, in-memory recall for up to 100 non-rejected sent prompts.
• Collapses consecutive duplicates while preserving drafts and multiline caret navigation.
• Adds unit and UI smoke coverage for history, outcomes, and keyboard behavior.
Diagram

graph TD
  Keys["Arrow Keys"] --> View["Chat View"] --> Boundary{"Caret Boundary?"} -->|Yes| Recall["Recall Methods"] --> History["Composer History"] --> Composer["Composer Text"]
  Boundary -->|No| Native["Native Caret"]
  Outcome{"Send Outcome"} -->|Not rejected| History
Loading
High-Level Assessment

The dedicated per-view-model history helper is the best fit for per-tab, app-lifetime state and keeps keyboard-specific caret logic in the view. Shared or persistent history services were considered but would conflict with the stated isolation and lifetime requirements.

Files changed (6) +265 / -1

Enhancement (3) +76 / -1
ChatTabViewModel.csIntegrate per-tab prompt history with sending and recall +13/-0

Integrate per-tab prompt history with sending and recall

• Adds older and newer recall operations backed by a composer-specific history. Records accepted and unconfirmed sends while excluding rejected prompts.

src/Capacitor.App/ViewModels/ChatTabViewModel.cs

ComposerHistory.csImplement bounded composer history navigation +49/-0

Implement bounded composer history navigation

• Introduces a 100-entry in-memory history with consecutive duplicate suppression, draft restoration, and edit-sensitive navigation. Recall begins only from blank composers and stops when recalled text changes.

src/Capacitor.App/ViewModels/ComposerHistory.cs

ChatTabView.axaml.csHandle arrow-key recall at multiline boundaries +14/-1

Handle arrow-key recall at multiline boundaries

• Routes unmodified Up and Down keys to history recall only from the first or last line. Otherwise, the TextBox retains its native caret behavior and recalled text places the caret at the end.

src/Capacitor.App/Views/ChatTabView.axaml.cs

Tests (3) +189 / -0
ChatTabViewModelTests.csVerify send outcomes populate recall history correctly +39/-0

Verify send outcomes populate recall history correctly

• Tests backward and forward recall across accepted and unconfirmed sends. Confirms rejected sends are omitted from history.

test/Capacitor.App.Tests.Unit/ChatTabViewModelTests.cs

ChatTabViewSmokeTests.csExercise arrow-key recall through the Avalonia UI +55/-0

Exercise arrow-key recall through the Avalonia UI

• Adds keyboard and send helpers plus an end-to-end smoke test for multiline caret boundaries, draft protection, history limits, and empty-draft restoration.

test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs

ComposerHistoryTests.csCover composer history navigation rules +95/-0

Cover composer history navigation rules

• Tests older and newer traversal, draft preservation, edit cancellation, duplicate collapse, navigation reset, empty history, and capacity eviction.

test/Capacitor.App.Tests.Unit/ComposerHistoryTests.cs

@qodo-code-review

qodo-code-review Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Wrapped prompts recall too early ✓ Resolved 🐞 Bug ≡ Correctness
Description
OnComposerKeyDown identifies first and last lines only by searching for \n, even though
ComposerInput has TextWrapping="Wrap". With at least two history entries, pressing Up at the end
of a long soft-wrapped recall immediately replaces it with the older prompt instead of moving the
caret to the preceding visual line, and Down similarly skips caret movement.
Code

src/Capacitor.App/Views/ChatTabView.axaml.cs[R81-83]

+            var recalled = e.Key == Key.Up
+                ? text.IndexOf('\n', 0, caret) < 0 && tab.RecallOlder()
+                : text.IndexOf('\n', caret) < 0 && tab.RecallNewer();
Relevance

●●● Strong

Visual wrapping makes newline-only edge detection incorrect; accepted UI interaction fixes favor
correcting this behavior.

PR-#766

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The composer explicitly enables visual wrapping, while the new tunnel handler considers only newline
characters before deciding to consume the arrow and navigate history. Because this handler runs
before the TextBox class handler, successful recall prevents the native vertical caret move.

src/Capacitor.App/Views/ChatTabView.axaml[181-187]
src/Capacitor.App/Views/ChatTabView.axaml.cs[30-35]
src/Capacitor.App/Views/ChatTabView.axaml.cs[77-86]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Arrow recall treats only explicit newline characters as line boundaries, so soft-wrapped composer rows are incorrectly considered the first and last line.

## Fix Focus Areas
- src/Capacitor.App/Views/ChatTabView.axaml.cs[77-87]
- test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[442-477]

## Recommended Fix
Determine whether the caret is on the first or last rendered visual line, rather than searching only for newline characters. Let the TextBox process Up or Down while another wrapped visual line exists, and add smoke coverage using a prompt long enough to wrap without explicit newlines.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Reverted edits still navigate history ✓ Resolved 🐞 Bug ≡ Correctness
Description
ComposerHistory.IsShowing treats matching text as proof that no edit occurred and ignores the
existing _composerEdits revision. If a user changes recalled text and then undoes or deletes back
to its original value, the next arrow key continues to another history entry instead of preserving
the resulting draft.
Code

src/Capacitor.App/ViewModels/ComposerHistory.cs[43]

+    bool IsShowing(string current) => _shown is not null && string.Equals(_shown, current, StringComparison.Ordinal);
Relevance

●●● Strong

This is a concrete state-tracking correctness bug; recent App findings fixing analogous state errors
were accepted.

PR-#889

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The composer already increments an edit counter for every text change because identical final text
cannot prove that no edit happened, but the new history calls pass only the text. IsShowing
therefore resumes navigation whenever the final string equals _shown, regardless of intervening
edits.

src/Capacitor.App/ViewModels/ComposerHistory.cs[34-43]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[134-154]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[328-335]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
History navigation uses text equality to infer that recalled content was not edited, which fails when edits return the composer to the same string.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/ComposerHistory.cs[22-47]
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[134-154]
- test/Capacitor.App.Tests.Unit/ComposerHistoryTests.cs[55-62]

## Recommended Fix
Associate active history navigation with the composer's edit revision. Capture the revision after programmatically applying recalled text, invalidate navigation when the revision changes, and add a test that edits a recall away from and back to its original value before pressing an arrow key.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Commit subject omits GitHub issue ✗ Dismissed 📘 Rule violation ⚙ Maintainability
Description
The commit subject Recall sent prompts with the arrow keys in the chat composer does not end with
the required (#<digits>) reference. Because this is the sole commit listed for the branch, its
subject carries no explicit GitHub issue linkage.
Code

src/Capacitor.App/ViewModels/ComposerHistory.cs[1]

+namespace Capacitor.App.ViewModels;
Relevance

●●● Strong

Recent compliance precedent enforces explicit GitHub references in repository workflow guidance.

PR-#659

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2897961 requires every commit subject to end with a GitHub issue reference, while
the supplied commit subject contains no such reference.

Rule 2897961: Enforce single-clause imperative commit subject with GitHub issue reference and 80-char limit


4. PR reference line omits GitHub issue ✗ Dismissed 📘 Rule violation § Compliance
Description
The PR description starts with `AI-2731 (no GitHub issue exists for this one; the Linear issue is
the tracker record)` instead of a line containing both a GitHub closing reference and the Linear
key. The description therefore lacks the required Closes #<digits> AI-2731-style reference line
connecting both trackers.
Code

src/Capacitor.App/ViewModels/ComposerHistory.cs[1]

+namespace Capacitor.App.ViewModels;
Relevance

●●● Strong

Recent repository guidance explicitly requires PR descriptions to link both GitHub and Linear
issues.

PR-#659

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2897991 requires one reference line containing exactly one GitHub closing reference
and at least one Linear issue key; the supplied description explicitly provides only the Linear key.

Rule 2897991: PR description must contain both GitHub and Linear issue references; PR title must not contain issue IDs


View medium (2)
5. Arrow recall replaces selected text ✓ Resolved 🐞 Bug ≡ Correctness
Description
OnComposerKeyDown decides recall from CaretIndex without checking SelectionStart and
SelectionEnd. When recalled text is selected and the active caret is on an edge line, Up or Down
runs history navigation on the tunnel route before the TextBox can collapse or move the selection.
Code

src/Capacitor.App/Views/ChatTabView.axaml.cs[R79-80]

+            var text = ComposerInput.Text ?? "";
+            var caret = Math.Clamp(ComposerInput.CaretIndex, 0, text.Length);
Relevance

●●● Strong

Selection handling is a concrete keyboard interaction bug, consistent with accepted UI behavior
corrections.

PR-#766

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The handler is registered on the tunnel route and bases its decision solely on text and caret
position, so it runs before native TextBox processing and never accounts for a selection. Newer
can then return the saved draft and replace the entire selected value.

src/Capacitor.App/Views/ChatTabView.axaml.cs[30-35]
src/Capacitor.App/Views/ChatTabView.axaml.cs[77-86]
src/Capacitor.App/ViewModels/ComposerHistory.cs[34-40]
src/Capacitor.App/Views/ChatTabView.axaml[181-187]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The arrow-key tunnel handler can navigate history while the composer has a selection, replacing recalled text before the TextBox handles that selection.

## Fix Focus Areas
- src/Capacitor.App/Views/ChatTabView.axaml.cs[77-87]
- test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[442-477]

## Recommended Fix
Skip recall whenever the composer has a nonempty selection and leave the event unhandled so the TextBox performs its normal arrow-key selection behavior. Add smoke tests for Up and Down with forward and backward selections in recalled text.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Failed recall checks leak a live window ✗ Dismissed 🐞 Bug ☼ Reliability
Description
Up_recalls_sent_prompts_and_down_returns_to_the_empty_draft creates a Host without a finally,
while CloseAsync runs only after every assertion succeeds. If a new text or caret assertion fails,
the shown window, chat timer, subscriptions, and terminal resources remain alive in the
process-global Avalonia test session.
Code

test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[449]

+            var host = new Host();
Relevance

●●● Strong

A recent Avalonia smoke-test precedent accepted requiring cleanup despite assertion failures and
process-global resources.

PR-#858

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Host shows a real window during construction, and its explicit CloseAsync method is what closes
that window and tears down both view models. The added test reaches that cleanup only after all
assertions, matching a previously accepted leak pattern for Avalonia smoke tests.

test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[85-116]
test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[190-195]
test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[447-478]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[628-640]
PR-#858

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new smoke test tears down its live Avalonia host only on the successful path, leaking resources when an assertion or setup step throws.

## Fix Focus Areas
- test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[447-478]
- test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs[190-195]

## Recommended Fix
Wrap the host's setup and assertions in `try`/`finally` and call `await host.CloseAsync()` from the `finally` block. Alternatively, make `Host` asynchronously disposable and use `await using` so teardown executes on every exit path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 64 rules
✅ Cross-repo context — repo relationships
Review mode: ⚖️ Balanced: This is a localized but behavior-changing UI/view-model feature involving stateful history, keyboard navigation, send outcomes, and multiline caret semantics; it warrants a complete single-pass review, but is not broad or dense enough to justify extended review.

Grey Divider

Tip of the day
💡 Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.App/ViewModels/ComposerHistory.cs
Comment thread src/Capacitor.App/ViewModels/ComposerHistory.cs
Comment thread src/Capacitor.App/Views/ChatTabView.axaml.cs Outdated
Comment thread src/Capacitor.App/Views/ChatTabView.axaml.cs Outdated
Comment thread src/Capacitor.App/ViewModels/ComposerHistory.cs
Comment thread test/Capacitor.App.Tests.Unit/ChatTabViewSmokeTests.cs
The composer wraps, so a newline search misreads a long single-line prompt as one row; a selection makes the arrows the TextBox's own collapse-and-move; and text equality cannot tell an untouched recall from one edited and restored by hand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

Re the Qodo findings:

  1. Wrapped prompts recall too early — fixed in ae8e052. The view now reads the caret's row and the row count from the TextPresenter.TextLayout, with the newline count as the fallback before a presenter exists. Smoke test added for a 60-word prompt with no newline.
  2. Commit subject omits GitHub issue / 3. PR reference line omits GitHub issue — not changed. No GitHub issue exists for this work; AI-2731 in Linear is the tracker record. AGENTS.md says the reference goes in only when context already gives it and never to invent one, and the PR template allows dropping the half that does not exist when the description says which. It does.
  3. Arrow recall replaces selected text — fixed in ae8e052. A non-empty selection leaves ↑/↓ to the TextBox. Smoke tests added for forward and backward selections.
  4. Reverted edits still navigate history — fixed in ae8e052. Recall records the composer's edit count after it sets the text, and any later mismatch ends navigation before the history is consulted. View-model test added for edit-then-restore.
  5. Failed recall checks leak a live window — not changed. Every test in ChatTabViewSmokeTests closes its Host the same way, without a finally; a per-test deviation would leave the file with two teardown patterns. Moving the whole file to await using is a separate change.

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.

1 participant