Skip to content

Improve desktop chat activity, interruption, and queued messages - #889

Merged
alexeyzimarev merged 2 commits into
mainfrom
capacitor/agent-e4fb05a9dbe547
Sep 11, 2026
Merged

alexeyzimarev merged 2 commits into
mainfrom
capacitor/agent-e4fb05a9dbe547

Conversation

@alexeyzimarev

@alexeyzimarev alexeyzimarev commented Sep 11, 2026 •

Copy link
Copy Markdown
Member

No linked GitHub or Linear issue.

What & why

Desktop chat shows zzz for idle agents, a live Working for Xm Xs line while busy, and outgoing messages above the composer. Esc interrupts terminal-backed agents while preserving the draft. The clock pauses for pending questions and permissions.

Where to look

Uncertain deliveries remain visible until a fresh transcript receipt confirms them. Path changes, truncation, and partial historical lines cannot falsely acknowledge a send; hidden slash commands can confirm delivery. A late receipt preserves edited drafts and newer delivery notices.

Verification

  • dotnet build for Capacitor.App.Tests.Unit and Capacitor.Cli.Core.Tests.Unit: zero warnings/errors, including a full desktop rebuild.
  • Focused TUnit runs: 164 desktop tests and 45 transcript tests passed, including composer key events, queue rendering, resets, lost acknowledgments, and command receipts.
  • dotnet publish src/Capacitor.Cli/Capacitor.Cli.csproj -c Release: passed without trimming/AOT warnings. Local Homebrew libraries emitted macOS deployment-target linker warnings.
  • git diff --check and bash scripts/check-linear-ids.sh passed.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-11T12:21:10.719779Z 5b51d58 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Improve desktop chat activity, interruption, and queued messages

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Shows idle badges, live busy duration, and queued messages across desktop chat.
• Sends Escape interrupts without clearing drafts or targeting stale terminal connections.
• Correlates transcript echoes by byte offset and preserves unresolved attention states.
Diagram

graph TD
  Composer["Chat composer"] -->|"send / Escape"| ChatVM["Chat tab VM"] -->|"text / Escape"| Terminal["Terminal channel"] -->|"message echo"| Transcript[("JSONL transcript")] --> Tail["JSONL tail"] -->|"offset events"| ChatVM
  ChatVM -->|"track sends"| Queue["Queued messages"]
  Tail -->|"acknowledge"| Queue
  Status["Agent status"] -->|"busy state"| ChatVM
  Status -->|"attention state"| Rail["Session rail"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. End-to-end message correlation IDs
  • ➕ Acknowledges repeated messages without relying on text normalization or file offsets
  • ➕ Provides explicit delivery correlation across transcript resets and path changes
  • ➖ Requires protocol, daemon, projection, and vendor-transcript changes
  • ➖ May be impossible for externally owned vendor transcript formats
  • ➖ Substantially increases scope for a desktop-only usability improvement

Recommendation: Keep the PR’s byte-offset and normalized-text correlation approach. It provides reliable one-at-a-time acknowledgement for repeated messages while remaining compatible with existing vendor transcripts; explicit correlation IDs are preferable only if transcript producers later support them end to end.

Files changed (17) +366 / -56

Enhancement (10) +162 / -42
ChatInput.csAdd an optional chat interruption contract +2/-0

Add an optional chat interruption contract

• Adds default 'CanInterrupt' and 'InterruptAsync' members so chat inputs can expose interruption without requiring every transport to implement it.

src/Capacitor.App/ViewModels/ChatInput.cs

ChatTabViewModel.csCoordinate activity timing, interruption, and queued messages +70/-33

Coordinate activity timing, interruption, and queued messages

• Tracks outgoing messages until matching transcript echoes arrive, using transcript byte offsets to reject older identical entries. Adds a gated interrupt command and continuously refreshed busy-duration text while preserving pending-card suppression.

src/Capacitor.App/ViewModels/ChatTabViewModel.cs

QueuedChatMessage.csModel transcript-acknowledged outgoing messages +24/-0

Model transcript-acknowledged outgoing messages

• Introduces a queued-message model that captures transcript path and file length at send time. Matching normalizes line endings and requires a newer offset when reading the same transcript.

src/Capacitor.App/ViewModels/QueuedChatMessage.cs

RailSessionViewModel.csDifferentiate idle and attention session badges +7/-0

Differentiate idle and attention session badges

• Derives 'zzz' for sessions waiting on user input and '!' for failures or pending requests, retaining attention priority when states overlap.

src/Capacitor.App/ViewModels/RailSessionViewModel.cs

RailWorktreeViewModel.csAggregate worktree status badge content +10/-0

Aggregate worktree status badge content

• Computes a worktree-level '!' badge when any session failed or has pending input, otherwise showing 'zzz' when any session waits on the user.

src/Capacitor.App/ViewModels/RailWorktreeViewModel.cs

TerminalChatInput.csExpose terminal Escape interruption through chat input +4/-0

Expose terminal Escape interruption through chat input

• Projects terminal interrupt availability and forwards interrupt requests to the terminal view model when the attached session is writable.

src/Capacitor.App/ViewModels/TerminalChatInput.cs

ChatTabView.axamlRender queued messages above the composer +22/-2

Render queued messages above the composer

• Adds a scrollable banner showing the queue summary and truncated outgoing message text until transcript acknowledgement.

src/Capacitor.App/Views/ChatTabView.axaml

ChatTabView.axaml.csBind unmodified Escape to chat interruption +6/-0

Bind unmodified Escape to chat interruption

• Handles Escape in the composer by executing the interrupt command while leaving the existing draft untouched.

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

SessionRailView.axamlDisplay dynamic rail badge text +2/-2

Display dynamic rail badge text

• Replaces hard-coded exclamation marks with the session or worktree 'StatusBadge', enabling both attention and idle indicators.

src/Capacitor.App/Views/SessionRailView.axaml

JsonlTail.csReport absolute byte offsets for tailed lines +15/-5

Report absolute byte offsets for tailed lines

• Extends tail reads with each nonblank line’s absolute ending byte offset. Offsets account for UTF-8 bytes, skipped blank lines, CRLF input, and incremental cursor positions.

src/Capacitor.Cli.Core/JsonlTail.cs

Bug fix (1) +17 / -0
TerminalTabViewModel.csSafely deliver Escape to writable terminal sessions +17/-0

Safely deliver Escape to writable terminal sessions

• Adds terminal interruption capability that waits for any paste-and-submit delivery to finish before sending Escape. Revalidates connection ownership and writability to prevent control input reaching detached, replaced, or read-only clients.

src/Capacitor.App/ViewModels/TerminalTabViewModel.cs

Tests (6) +187 / -14
ChatComposerTests.csVerify unavailable chats cannot interrupt +1/-0

Verify unavailable chats cannot interrupt

• Asserts that the interrupt command remains disabled when the chat input cannot accept interaction.

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

ChatTabViewModelTests.csCover activity timers and queue acknowledgement +94/-14

Cover activity timers and queue acknowledgement

• Updates activity expectations for persistent elapsed-time reporting and adds timer lifecycle coverage. Tests repeated queued text, stale transcript echoes, send acknowledgements, refusals, and draft preservation.

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

ChatTabViewSmokeTests.csExercise Escape and queue-banner UI behavior +35/-0

Exercise Escape and queue-banner UI behavior

• Adds headless UI coverage confirming Escape sends one control byte without clearing the composer and queued text remains visible until echoed.

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

RailSessionViewModelTests.csVerify idle and attention badge precedence +10/-0

Verify idle and attention badge precedence

• Covers 'zzz' for waiting sessions, blank badges for working sessions, and '!' precedence for failures or pending requests.

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

TerminalChatInputTests.csCover interrupt ordering and stale-client protection +37/-0

Cover interrupt ordering and stale-client protection

• Verifies Escape waits behind submit delivery and does not add another paste or submit. Also confirms detachment prevents a waiting Escape from reaching the retired client.

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

JsonlTailTests.csValidate JSONL line byte offsets +10/-0

Validate JSONL line byte offsets

• Adds assertions for CRLF, blank-line skipping, multibyte UTF-8 content, and absolute offsets across incremental reads.

test/Capacitor.Cli.Core.Tests.Unit/JsonlTailTests.cs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5b51d58555

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

Comment on lines +19 to +21
internal bool Matches(string text, string? path, long offset) =>
(TranscriptPath != path || offset > TranscriptOffset)
&& Normalize(Text) == Normalize(text);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rebase queued offsets when the transcript resets

When a transcript is truncated after a send, JsonlTail rereads it from offset zero with TailStatus.Reset, but the queued message retains its pre-truncation TranscriptOffset. A matching echo in the reset file will therefore have offset <= TranscriptOffset and can never acknowledge the message, leaving the banner permanently queued or unconfirmed. Reset or generation-scope queued offsets when Apply handles the tail reset.

Useful? React with 👍 / 👎.

Comment on lines +166 to +169
void RefreshActivityNote() {
var working = _status == "Running" && _awaitingInput == false;
if (working) _workingSince ??= _time.GetTimestamp();
else _workingSince = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop the working clock while a pending card blocks the turn

When a permission or question card is pending, the activity line is hidden but _workingSince remains unchanged because working ignores HasPendingCards. After the user spends time answering and the card disappears, the UI resumes with that blocked interval included—for example, a ten-minute approval wait immediately displays Working for 10m ... even though the agent was not working. Treat a pending card as non-working when updating the clock, not only when rendering the note.

Useful? React with 👍 / 👎.

@qodo-code-review

qodo-code-review Bot commented Sep 11, 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. Reset transcripts leave messages queued forever ✓ Resolved 🐞 Bug ≡ Correctness
Description
QueuedChatMessage retains the file length captured before a reset and Matches requires a
same-path echo to end after that stale offset. When JsonlTail detects truncation and rereads the
replacement content from byte zero, a new matching transcript entry at a lower offset cannot clear
the queued message.
Code

src/Capacitor.App/ViewModels/QueuedChatMessage.cs[R19-21]

+    internal bool Matches(string text, string? path, long offset) =>
+        (TranscriptPath != path || offset > TranscriptOffset)
+        && Normalize(Text) == Normalize(text);
Relevance

●●● Strong

Reset replay starts offsets at zero, so stale baselines prevent legitimate acknowledgements after
truncation.

PR-#730
PR-#831

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new queue model stores the physical file length at send time. The tail explicitly resets its
origin to zero on length regression, and the chat reset handler clears projection state without
updating queued entries before applying newly projected user messages through the stale offset
matcher.

src/Capacitor.App/ViewModels/QueuedChatMessage.cs[10-21]
src/Capacitor.Cli.Core/JsonlTail.cs[22-49]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[418-436]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[447-477]

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

### Issue description
A queued message records the transcript's pre-reset byte length, but a truncation reset rereads the replacement file from offset zero. The current same-path comparison rejects every matching entry before the old length, so a valid post-reset echo can leave the banner queued indefinitely.

### Fix Focus Areas
- src/Capacitor.App/ViewModels/QueuedChatMessage.cs[19-21]
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[418-421]
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[457-477]

### Recommended Fix
Handle `TailStatus.Reset` as a new transcript epoch for queued messages. Rebase their acknowledgement offsets to the reset file's origin, or explicitly clear/mark them unconfirmed according to the desired delivery policy, so a subsequently read matching user entry can resolve the queue.

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


2. Historical messages can hide a new send ✓ Resolved 🐞 Bug ≡ Correctness
Description
QueuedChatMessage.Matches treats equal text from a different transcript path as sufficient
acknowledgement regardless of TranscriptOffset, while its path-and-file-length baseline remains
unchanged across same-path tail resets. When ChatTabViewModel switches, replaces, or resets a
transcript and replays content containing the same user text, an unrelated historical entry can
remove the queued banner before the new send’s actual echo is observed.
Code

src/Capacitor.App/ViewModels/QueuedChatMessage.cs[R19-21]

+    internal bool Matches(string text, string? path, long offset) =>
+        (TranscriptPath != path || offset > TranscriptOffset)
+        && Normalize(Text) == Normalize(text);
Relevance

●●● Strong

Path changes invalidate offset-based identity, allowing historical identical text to acknowledge a
new send prematurely.

PR-#730
PR-#831

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The queue item captures only the transcript path and file length at send time, while transcript
switches clear rendered/projection state but preserve the queue before replaying the replacement
file. Each replayed user event is passed to Matches, whose path-mismatch condition bypasses the
captured offset; tail resets likewise preserve queued entries while restarting offsets against
replacement content, so neither flow maintains a reliable transcript identity for acknowledgement.

src/Capacitor.App/ViewModels/ChatTabViewModel.cs[324-339]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[383-401]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[473-479]
src/Capacitor.App/ViewModels/QueuedChatMessage.cs[10-21]
src/Capacitor.Cli.Core/JsonlTail.cs[46-49]
src/Capacitor.App/ViewModels/QueuedChatMessage.cs[7-21]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[327-329]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[388-401]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[447-477]
src/Capacitor.Cli.Core/JsonlTail.cs[22-49]

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

## Issue description

`QueuedChatMessage.Matches` does not preserve reliable transcript identity across path switches, transcript replacements, or same-path tail resets. A path mismatch bypasses the captured-offset check, while reset offsets can be compared against a stale baseline, allowing replayed historical same-text user messages to acknowledge a newly queued send without observing its actual echo.

## Fix Focus Areas

- src/Capacitor.App/ViewModels/QueuedChatMessage.cs[7-21]
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[388-401]
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[447-477]

## Recommended Fix

Capture a stable transcript lease or generation identifier with each queued message and require acknowledgements to come from that exact transcript generation after the captured offset; do not allow a path mismatch to bypass this check. Advance the generation on both path switches and tail resets, and explicitly choose how pending entries are handled when correlation is no longer safe—retain and rebase them, or mark them unconfirmed or unconfirmable—instead of matching them against replayed or replacement transcript history.

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



Remediation recommended

3. Slash commands stay queued forever ✓ Resolved 🐞 Bug ≡ Correctness
Description
Apply acknowledges queued text only from user-message envelopes after transcript display
projection, but Claude’s projection strips slash-command wrapper blocks and drops the envelope when
no display text remains. A command represented entirely by those injected wrappers never reaches
QueuedChatMessage.Matches, leaving its queue banner stale after the transcript has recorded it.
Code

src/Capacitor.App/ViewModels/ChatTabViewModel.cs[R475-477]

                case AcpEventKind.UserMessage:
+                    var acknowledged = _queuedMessages.FirstOrDefault(q => q.Matches(e.Text ?? "", _path, offset));
+                    if (acknowledged is not null) _queuedMessages.Remove(acknowledged);
Relevance

●●● Strong

Acknowledgement must use the recorded envelope, not only rendered display text; this is a concrete
correctness gap.

PR-#790
PR-#884

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The chat associates offsets only with envelopes returned by projection.Project and matches queues
against their transformed Text. Claude’s display rules remove command wrapper blocks and return
null for an empty result, which prevents any acknowledgement attempt for wrapper-only records.

src/Capacitor.App/ViewModels/ChatTabViewModel.cs[426-432]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[473-477]
src/Capacitor.Cli.Core/Harness/Claude/ClaudeChatRules.cs[6-24]
src/Capacitor.Cli.Core/Harness/Claude/ClaudeChatRules.cs[47-61]

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

## Issue description
Queued messages are correlated against display-filtered envelopes, so transcript records removed or rewritten by vendor display rules cannot acknowledge the original send.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[426-432]
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[473-477]
- src/Capacitor.Cli.Core/Harness/Claude/ClaudeChatRules.cs[14-24]

## Recommended Fix
Expose acknowledgement metadata or canonical submitted text separately from display envelopes and perform queue correlation before vendor display filtering. Preserve the byte offset while allowing wrapper-only slash-command records to acknowledge the corresponding submitted command even when no visible user row is emitted.

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


4. Unconfirmed sends vanish from the queue ✓ Resolved 🐞 Bug ≡ Correctness
Description
SendCommand removes its QueuedChatMessage for every false result even though
LocalFrameChatInput.SendAsync uses false for transport failures whose delivery outcome is
explicitly unknown. When an acknowledgement is lost after daemon acceptance, the message disappears
before a later transcript echo can confirm it, despite the input warning the user to check the chat.
Code

src/Capacitor.App/ViewModels/ChatTabViewModel.cs[R333-337]

+            finally {
+                // Without a transcript there is no later echo to observe; the channel's ack is
+                // the only delivery evidence available. Refusals keep the original draft below.
+                if (!committed || _projection is null) _queuedMessages.Remove(queued);
+                RefreshQueue();
Relevance

●●● Strong

Directly conflicts with the stated queue contract; accepted history favors preserving uncertain
delivery state.

PR-#730
PR-#831

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The local input labels cancellation and transport failures as unconfirmed but returns false for
them. The new command cleanup removes every false-result queue entry, so the queue cannot represent
exactly the uncertain delivery state the channel exposes.

src/Capacitor.App/ViewModels/LocalFrameChatInput.cs[9-13]
src/Capacitor.App/ViewModels/LocalFrameChatInput.cs[65-89]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[324-339]

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 boolean send result conflates confirmed rejection with an unknown transport outcome, causing potentially delivered messages to be removed from the queue.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/ChatInput.cs[11-15]
- src/Capacitor.App/ViewModels/LocalFrameChatInput.cs[65-96]
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[324-339]

## Recommended Fix
Replace the boolean result with an outcome that distinguishes accepted, rejected, and unconfirmed delivery. Remove queue entries immediately only for confirmed rejection, retain unconfirmed entries for transcript acknowledgement, and clear the composer only for accepted or transcript-confirmed delivery.

ⓘ 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
  Explored: repo: kurrent-io/kurrent-agents (sha: 8bf78d4f)
  Explored: repo: kurrent-io/kcap-server (sha: e6c10c5a)
Review mode: 🧠 Deep: This is a bug-dense behavioral change spanning chat state, terminal interruption, transcript byte-offset correlation, UI bindings, and rail status logic, with many independent code paths where a redundant review could catch subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.App/ViewModels/ChatTabViewModel.cs Outdated
Comment thread src/Capacitor.App/ViewModels/ChatTabViewModel.cs Outdated
Comment thread src/Capacitor.App/ViewModels/QueuedChatMessage.cs Outdated
Comment thread src/Capacitor.App/ViewModels/QueuedChatMessage.cs Outdated
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