Skip to content

Show Claude's usage-limit question in the desktop chat - #1140

Merged
alexeyzimarev merged 4 commits into
mainfrom
nortonandreev/ai-3151-show-a-vendor-usage-limit-in-the-desktop-app
Sep 24, 2026
Merged

alexeyzimarev merged 4 commits into
mainfrom
nortonandreev/ai-3151-show-a-vendor-usage-limit-in-the-desktop-app

Conversation

@nortonandreev

@nortonandreev nortonandreev commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

No GitHub issue — AI-3151

What & why

Claude's session-limit prompt exists only as a terminal menu, so the desktop session stays running and a chat message is pasted into that menu. The daemon reads the live screen and the chat shows the same question. A choice sends the digit Claude printed. Sending a message is refused until the menu leaves the screen.

The option set depends on the account (stop and wait, wait and continue, ask an admin, add funds, upgrade), so the card uses the lines on screen rather than a fixed list.

Where to look

A match is the limit line above the title, plus two known choices, on the current screen. Cursor movement overwrites cells, so a stripped scrollback would keep a menu that is already gone. Other harnesses do not ask this question; the notice stays empty for them.

Note: The daemon running this build has to be the one attached. An older daemon never publishes the notice, so the chat still looks like an ordinary session while the menu is up.

Verification

dotnet run --project test/Capacitor.Cli.Daemon.Tests.Unit -- --treenode-filter "/*/*/ClaudeUsageLimitDetectorTests/*" — 7 passed.

dotnet run --project test/Capacitor.Cli.Core.Tests.Unit -- --treenode-filter "/*/*/StatusIpcJsonTests/*" — 21 passed.

dotnet run --project test/Capacitor.App.Tests.Unit -- --treenode-filter "/*/*/ChatTabViewModelTests/A_usage_limit*" — 2 passed. A_send_still_uploading_when_the_usage_limit_appears_is_not_pasted_into_the_menu — 1 passed. The smoke test of the usage-limit card — 1 passed.

TrayViewModelTests — 60 passed. DesktopNotificationCoordinatorTests — 32 passed. SessionStatusDotsTests — 10 passed. ChatComposerTests — 10 passed.

The menu is terminal chrome and the choices depend on the account, so the daemon reads the live screen. A choice sends the printed digit, and a chat message is refused while the menu is up.
@linear-code

linear-code Bot commented Sep 24, 2026

Copy link
Copy Markdown

AI-3151

@nortonandreev nortonandreev self-assigned this Sep 24, 2026
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Surface Claude usage-limit choices in desktop chat

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Detect Claude usage-limit menus from live ANSI terminal state and publish them over status IPC.
• Render dynamic choices in desktop chat, attention surfaces, and native question notifications.
• Block chat messages during menus and send selected options as raw digit keystrokes.
Diagram

graph TD
  PTY["Claude PTY"] --> SCREEN["ANSI Screen"] --> DETECTOR["Limit Detector"] --> DAEMON["Agent Orchestrator"] --> IPC["Status IPC"] --> APP["Desktop Surfaces"] --> INPUT["Raw Key Input"] --> PTY
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use vendor-native structured events
  • ➕ Avoids maintaining an ANSI terminal emulator and text-match heuristics.
  • ➕ Provides typed usage-limit states if Claude exposes them.
  • ➖ Claude currently presents this interaction only as terminal chrome.
  • ➖ Would couple support to an unavailable or unstable vendor-specific event contract.
2. Parse stripped terminal scrollback
  • ➕ Requires substantially less terminal-state implementation.
  • ➕ Can reuse conventional ANSI stripping utilities.
  • ➖ Retains menus after cursor-based redraws remove them.
  • ➖ Could keep chat blocked or notify users after the question disappears.
3. Render a fixed choice list
  • ➕ Simplifies detection and desktop presentation.
  • ➕ Avoids parsing numbered option labels.
  • ➖ Available actions vary by account, plan, and funding state.
  • ➖ Risks sending a digit whose meaning differs from the displayed menu.

Recommendation: Keep the PR's live-screen detection and dynamic option extraction. A structured vendor event would be preferable if Claude exposes a stable one later, but reconstructing the active viewport is currently the safest way to avoid stale menus, while preserving Claude's account-specific choices.

Files changed (21) +792 / -44

Enhancement (16) +531 / -23
AgentRow.csCarry usage-limit notices into local agent rows +4/-2

Carry usage-limit notices into local agent rows

• Adds the local daemon's usage-limit notice to merged agent rows while leaving remote and pending rows unaffected.

src/Capacitor.App/Services/AgentRow.cs

DesktopNotificationCoordinator.csNotify users about new usage-limit questions +47/-3

Notify users about new usage-limit questions

• Tracks active and previously seen usage-limit menus, emits question notifications, and withdraws them when the menu changes or disappears. Notification preference and foreground rules align with existing question handling.

src/Capacitor.App/Services/Notifications/DesktopNotificationCoordinator.cs

ChatInput.csAdd a raw single-key chat input operation +2/-0

Add a raw single-key chat input operation

• Defines an input abstraction for sending one unpasted key, allowing terminal menus to receive digits without bracketed-paste text.

src/Capacitor.App/ViewModels/ChatInput.cs

ChatSessionInfo.csPropagate usage-limit state into chat sessions +5/-2

Propagate usage-limit state into chat sessions

• Carries local usage-limit notices from daemon status snapshots into the chat session model.

src/Capacitor.App/ViewModels/ChatSessionInfo.cs

RailSessionViewModel.csShow usage-limit summaries in rail tooltips +3/-1

Show usage-limit summaries in rail tooltips

• Adds the active usage-limit summary to local session rail tooltips.

src/Capacitor.App/ViewModels/RailSessionViewModel.cs

SessionStatusDots.csTreat usage-limit questions as attention states +12/-6

Treat usage-limit questions as attention states

• Updates status dots, labels, and attention predicates so blocking usage-limit menus consistently appear as waiting for the user.

src/Capacitor.App/ViewModels/SessionStatusDots.cs

TerminalChatInput.csForward raw menu keys through terminal chat input +1/-0

Forward raw menu keys through terminal chat input

• Implements the new single-key input operation by delegating to the attached terminal view model.

src/Capacitor.App/ViewModels/TerminalChatInput.cs

TerminalTabViewModel.csSend isolated raw bytes to attached terminals +19/-0

Send isolated raw bytes to attached terminals

• Adds guarded raw-key delivery that waits for pending text delivery, validates the current terminal attachment, and handles cancellation or failures.

src/Capacitor.App/ViewModels/TerminalTabViewModel.cs

TrayViewModel.csInclude usage limits in tray attention state +11/-6

Include usage limits in tray attention state

• Counts blocking local usage-limit questions, includes them in tray attention summaries, and labels affected agent entries.

src/Capacitor.App/ViewModels/TrayViewModel.cs

UsageLimitChoiceViewModel.csModel an actionable usage-limit choice +17/-0

Model an actionable usage-limit choice

• Introduces a view model containing the vendor-displayed index, label, and selection command for each menu option.

src/Capacitor.App/ViewModels/UsageLimitChoiceViewModel.cs

ChatTabView.axamlRender the usage-limit choice card +30/-1

Render the usage-limit choice card

• Adds a warning card with dynamic choice buttons and delivery errors. The message composer is disabled while the blocking menu is active.

src/Capacitor.App/Views/ChatTabView.axaml

StatusIpc.csDefine vendor-neutral usage-limit IPC contracts +55/-1

Define vendor-neutral usage-limit IPC contracts

• Extends agent status with usage-limit notices and introduces kinds, numbered options, question detection, and value equality. The nullable trailing field preserves compatibility with older daemons.

src/Capacitor.Cli.Core/LocalIpc/StatusIpc.cs

AgentOrchestrator.LocalIpc.csPublish live usage limits in daemon status +2/-1

Publish live usage limits in daemon status

• Includes a running agent's detected usage-limit notice in local IPC status snapshots and clears it for non-running states.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.LocalIpc.cs

AgentOrchestrator.csDetect usage-limit changes in Claude output +18/-0

Detect usage-limit changes in Claude output

• Attaches the Claude detector to eligible PTY read loops, stores changed notices on agent state, and pulses status subscribers only when the live menu changes.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs

AnsiScreen.csReconstruct a live ANSI terminal viewport +214/-0

Reconstruct a live ANSI terminal viewport

• Introduces a fixed-size UTF-8 terminal screen supporting cursor movement, erasure, scrolling, save/restore, CSI, and OSC handling. This prevents overwritten menus from surviving as stale scrollback matches.

src/Capacitor.Cli.Daemon/Services/AnsiScreen.cs

ClaudeUsageLimitDetector.csParse Claude usage-limit menus from the live screen +91/-0

Parse Claude usage-limit menus from the live screen

• Recognizes Claude's prompt only when at least two known numbered choices are present, extracts account-specific labels, and derives a nearby usage summary.

src/Capacitor.Cli.Daemon/Services/ClaudeUsageLimitDetector.cs

Bug fix (1) +66 / -5
ChatTabViewModel.csManage usage-limit questions and choices in chat +66/-5

Manage usage-limit questions and choices in chat

• Projects notices into actionable choice models, marks the session as waiting, and blocks normal message sends while a question is active. Selecting an option sends its printed digit and reports terminal attachment failures.

src/Capacitor.App/ViewModels/ChatTabViewModel.cs

Tests (4) +195 / -16
ChatTabViewModelTests.csTest chat behavior during usage-limit questions +43/-1

Test chat behavior during usage-limit questions

• Verifies choice projection, composer blocking, raw digit delivery, and restoration of normal messaging after the notice clears.

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

ChatTabViewSmokeTests.csSmoke-test usage-limit card rendering and input +39/-0

Smoke-test usage-limit card rendering and input

• Confirms the card and all choices render, the composer becomes disabled, and selecting a choice sends exactly one digit to the terminal.

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

StatusIpcJsonTests.csPin usage-limit IPC serialization compatibility +34/-15

Pin usage-limit IPC serialization compatibility

• Updates expected agent JSON for the new nullable field and verifies notices and numbered options round-trip correctly. Older payloads remain supported with a null notice.

test/Capacitor.Cli.Core.Tests.Unit/LocalIpc/StatusIpcJsonTests.cs

ClaudeUsageLimitDetectorTests.csTest live-screen usage-limit detection +79/-0

Test live-screen usage-limit detection

• Covers complete and chunked menus, ANSI decoration, screen clearing, and false-positive rejection for prose or incomplete choice sets.

test/Capacitor.Cli.Daemon.Tests.Unit/Services/ClaudeUsageLimitDetectorTests.cs

@qodo-code-review

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Old menu clicks send digits 🐞 Bug ≡ Correctness
Description
ChooseUsageLimitAsync receives only an option index and sends it without checking that the notice
and option which created the command are still current. A status update can clear or replace the
menu after a click is dispatched, allowing that stale callback to write its digit into the
replacement menu or ordinary terminal input.
Code

src/Capacitor.App/ViewModels/ChatTabViewModel.cs[R669-674]

+    async Task ChooseUsageLimitAsync(int index) {
+        if (index is < 1 or > 9) return;
+        UsageLimitError = "";
+        var sent = await _input.SendKeyAsync((byte)('0' + index), _lifetimeToken);
+        if (!sent && !_lifetimeToken.IsCancellationRequested)
+            UsageLimitError = "The terminal is not attached, so that choice was not sent.";
Relevance

●●● Strong

Recent precedent accepts guarding delayed callbacks against stale UI state and navigation.

PR-#1101
PR-#790

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Applying a new status disposes old commands and replaces the choices, but commands capture only
option.Index; the send method has no current-notice or option-identity validation. Terminal raw
sending remains available whenever the terminal is attached, so replacement or removal of the notice
does not prevent a previously initiated callback from sending.

src/Capacitor.App/ViewModels/ChatTabViewModel.cs[654-667]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[669-675]
src/Capacitor.App/ViewModels/TerminalChatInput.cs[53-56]
src/Capacitor.App/ViewModels/TerminalTabViewModel.cs[218-226]

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 choice callback can outlive the usage-limit menu that created it and write a stale digit to the terminal.

Fix Focus Areas
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[654-675]
- src/Capacitor.App/ViewModels/TerminalTabViewModel.cs[218-226]

Recommended Fix
Associate every choice command with a menu generation or immutable notice snapshot. Cancel its token when `ApplyUsageLimit` replaces or clears that snapshot, and verify the generation immediately before raw input is sent, including after waiting for an in-flight delivery.

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


2. Closed menus keep blocking chat ✗ Dismissed 🐞 Bug ≡ Correctness
Description
AnsiScreen.Csi discards every private control sequence, including alternate-screen exit, so the
detector retains the usage menu after the terminal has restored its main buffer. When Claude leaves
an alternate-screen menu with CSI ?1049l, the stale notice continues disabling chat and its
choices can send digits into the restored terminal.
Code

src/Capacitor.Cli.Daemon/Services/AnsiScreen.cs[R137-138]

+        if (!_private) Execute(c);
+        _mode = Mode.Ground;
Relevance

●●● Strong

Terminal parser state bugs affecting live UI detection are reliability defects; historical parser
correctness fixes were accepted.

PR-#362
PR-#149

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The emulator has only one _cells buffer and skips Execute whenever a CSI sequence is private,
while the detector parses that buffer after every output chunk. The repository's captured terminal
test explicitly exercises ?1049h and ?1049l and verifies that alternate-buffer text must not
remain after returning to the main screen.

src/Capacitor.Cli.Daemon/Services/AnsiScreen.cs[9-24]
src/Capacitor.Cli.Daemon/Services/AnsiScreen.cs[127-138]
src/Capacitor.Cli.Daemon/Services/ClaudeUsageLimitDetector.cs[15-17]
test/Capacitor.App.Tests.Unit/TerminalTranscriptTests.cs[28-33]
test/Capacitor.App.Tests.Unit/TerminalTranscriptTests.cs[62-68]

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

## Issue description
`AnsiScreen` ignores private alternate-screen switching sequences, leaving menu cells visible after the real terminal restores its main buffer.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/AnsiScreen.cs[11-24]
- src/Capacitor.Cli.Daemon/Services/AnsiScreen.cs[127-138]

## Recommended Fix
Implement alternate-buffer handling for the private screen modes used by terminal applications, including `?1049`, `?1047`, and `?47`. Preserve and restore the main cells and cursor as appropriate, and add a detector test proving that a menu written between alternate-screen entry and exit disappears after exit.

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


3. Fake menus send unintended input ✓ Resolved 🐞 Bug ⛨ Security
Description
ClaudeUsageLimitDetector.Parse treats any PTY screen with the prompt and two familiar option
labels as a blocked Claude menu, without establishing that the screen belongs to Claude's genuine
usage-limit UI. Model- or tool-generated terminal output can therefore create a card whose button
sends its parsed digit directly to the attached terminal.
Code

src/Capacitor.Cli.Daemon/Services/ClaudeUsageLimitDetector.cs[R36-39]

+            if (IsKnown(label)) known++;
+            options.Add(new UsageLimitOptionDto(index, label));
+        }
+        if (known < 2) return null;
Relevance

●●● Strong

Security findings involving untrusted tool or terminal-derived input crossing into action sinks are
consistently accepted.

PR-#304
PR-#839

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Every Claude PTY chunk is parsed as screen content, and the parser accepts the menu solely after
finding two known labels. The resulting choice callback dispatches the derived index as a raw byte
to the attached terminal, so arbitrary displayed text crosses from an untrusted-output channel into
an input sink.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[3043-3065]
src/Capacitor.Cli.Daemon/Services/ClaudeUsageLimitDetector.cs[15-53]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[654-675]
src/Capacitor.App/ViewModels/TerminalTabViewModel.cs[218-232]

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 detector treats unauthenticated PTY text as authority to send an input byte to the terminal.

Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/ClaudeUsageLimitDetector.cs[20-53]
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[669-675]
- src/Capacitor.App/ViewModels/TerminalTabViewModel.cs[218-232]

Recommended Fix
Do not use a menu inferred from arbitrary terminal output as authorization to write a raw digit. Keep the notice informational and direct the user to the terminal, or introduce a trusted vendor signal that can authenticate the menu before enabling a choice that writes to the PTY.

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



Remediation recommended

4. Usage menus can be missed ✗ Dismissed 🐞 Bug ≡ Correctness
Description
AnsiScreen.EraseDisplay moves the cursor to the top-left when it processes CSI 2J or CSI 3J,
although erase-display clears cells without changing the cursor position. When Claude clears the
display without a subsequent cursor-position sequence, later PTY output is written into the wrong
simulated cells and the live menu may not be detected.
Code

src/Capacitor.Cli.Daemon/Services/AnsiScreen.cs[R185-188]

+        if (mode is 2 or 3) {
+            Array.Fill(_cells, ' ');
+            _row = 0;
+            _col = 0;
Relevance

●●● Strong

Incorrect terminal emulation semantics can miss live prompts; prior PTY rendering correctness fixes
were accepted.

PR-#149
PR-#244

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The parser already handles cursor positioning as separate H and f commands, but its
erase-display handling additionally resets both coordinates for modes 2 and 3. Because the detector
retains this screen state across all PTY chunks, that incorrect state changes the text subsequently
parsed as the active terminal screen.

src/Capacitor.Cli.Daemon/Services/AnsiScreen.cs[154-172]
src/Capacitor.Cli.Daemon/Services/AnsiScreen.cs[184-193]
src/Capacitor.Cli.Daemon/Services/ClaudeUsageLimitDetector.cs[15-18]

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 screen emulator changes cursor position for an ANSI display-erase operation, causing later output to be placed incorrectly.

Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/AnsiScreen.cs[154-172]
- src/Capacitor.Cli.Daemon/Services/AnsiScreen.cs[184-193]

Recommended Fix
Make `EraseDisplay` clear only the requested cells. Preserve `_row` and `_col` for all erase-display modes, leaving cursor movement exclusively to the explicit cursor-position commands already handled by the parser; add a test that erases mid-screen and then writes the menu.

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


5. Claude parser sits outside its harness 📘 Rule violation ⌂ Architecture
Description
ClaudeUsageLimitDetector embeds Claude-only prompts and choices while declaring the generic
Capacitor.Cli.Daemon.Services namespace. The detector is instantiated only for Claude terminal
agents, while analogous vendor parsers live under Harness/<Vendor>, so future vendor-specific
changes are split across two locations.
Code

src/Capacitor.Cli.Daemon/Services/ClaudeUsageLimitDetector.cs[10]

+internal sealed class ClaudeUsageLimitDetector {
Relevance

●●● Strong

Recent precedent accepts removing vendor-specific coupling from shared daemon services and placing
logic behind harness boundaries.

PR-#631
PR-#612

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2762984 requires vendor-specific daemon harness logic under Harness/<Vendor>. The detector
recognizes Claude-specific terminal prompts and choices, is constructed exclusively for Claude
agents, yet is added under the generic Services directory and namespace.

Rule 2762984: Place vendor-specific harness code only in the correct Harness/&lt;Vendor&gt;/ assembly and directory
src/Capacitor.Cli.Daemon/Services/ClaudeUsageLimitDetector.cs[3-10]
src/Capacitor.Cli.Daemon/Services/ClaudeUsageLimitDetector.cs[69-78]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[3043-3045]

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

## Issue description
`ClaudeUsageLimitDetector` contains Claude-specific terminal parsing but is located in the generic daemon services directory, contrary to the required vendor harness organization.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/ClaudeUsageLimitDetector.cs[1-91]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[3043-3045]

## Recommended Fix
Move the detector to `src/Capacitor.Cli.Daemon/Harness/Claude/ClaudeUsageLimitDetector.cs`, change its namespace to `Capacitor.Cli.Daemon.Harness.Claude`, and update the orchestrator and tests to import the new namespace.

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


6. Repeated choices send extra digits ✓ Resolved 🐞 Bug ☼ Reliability
Description
ChooseUsageLimitAsync sends a digit on every invocation without sharing an in-flight or
already-selected guard across the choice commands. Until the daemon publishes the menu redraw,
repeated or competing button clicks can send another digit after the first selection, reaching the
next terminal prompt as unintended input.
Code

src/Capacitor.App/ViewModels/ChatTabViewModel.cs[R669-672]

+    async Task ChooseUsageLimitAsync(int index) {
+        if (index is < 1 or > 9) return;
+        UsageLimitError = "";
+        var sent = await _input.SendKeyAsync((byte)('0' + index), _lifetimeToken);
Relevance

●●● Strong

Recent precedents accept fixes for overlapping async operations and duplicate user actions.

PR-#1069
PR-#766

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Each option receives a separate command that calls the same unguarded send method, and every call
forwards directly to SendRawAsync. The terminal method waits only for normal text delivery and
then sends its byte; it does not serialize usage-limit selections or prevent a second selection
after the first send completes.

src/Capacitor.App/ViewModels/ChatTabViewModel.cs[663-675]
src/Capacitor.App/ViewModels/TerminalChatInput.cs[53-56]
src/Capacitor.App/ViewModels/TerminalTabViewModel.cs[218-226]
src/Capacitor.App/Views/ChatTabView.axaml[296-307]

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

## Issue description
Every usage-limit command can independently send its digit while the same menu remains displayed, allowing multiple selections to reach the terminal.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[654-675]
- src/Capacitor.App/Views/ChatTabView.axaml[296-307]

## Recommended Fix
Add shared selection state for the current usage-limit notice and use it to disable every choice while a send is running. Keep the choices locked after a successful send until the notice changes or disappears, but unlock them after a failed send so the user can retry; add a test that concurrent or repeated commands produce only one raw digit.

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



Informational

7. Three usage-limit types share one file 📘 Rule violation ⚙ Maintainability
Description
StatusIpc.cs adds UsageLimitKinds, UsageLimitOptionDto, and UsageLimitNoticeDto as separate
public top-level types beside the existing status payload types. None is an allowed extension, tiny
internal hierarchy, or registry companion, so later changes have no filename matching these primary
types.
Code

src/Capacitor.Cli.Core/LocalIpc/StatusIpc.cs[R108-110]

+/// Wire tokens for <see cref="UsageLimitNoticeDto.Kind"/>.
+public static class UsageLimitKinds {
+    /// The vendor is waiting on a choice before the turn can continue. Chat send is refused.
Relevance

● Weak

Recent, closely matching one-type-per-file findings were explicitly rejected, including multi-type
IPC files.

PR-#873
PR-#1117

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3162234 requires one primary top-level type per matching file except for narrow extension,
hierarchy, or registry patterns. The added region declares three independent public top-level
usage-limit types in the already multi-purpose StatusIpc.cs file.

Rule 3162234: One primary type per file, with only narrow documented exceptions
src/Capacitor.Cli.Core/LocalIpc/StatusIpc.cs[108-125]
src/Capacitor.Cli.Core/LocalIpc/StatusIpc.cs[125-156]

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

## Issue description
Three new public top-level usage-limit types are added to `StatusIpc.cs`, leaving none of them in a file whose name matches its primary type.

## Fix Focus Areas
- src/Capacitor.Cli.Core/LocalIpc/StatusIpc.cs[108-156]

## Recommended Fix
Move `UsageLimitKinds`, `UsageLimitOptionDto`, and `UsageLimitNoticeDto` into separately named files under `LocalIpc`, preserving their namespace and public API.

ⓘ 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: 🧠 Deep: This is a behaviorally dense, cross-layer feature spanning terminal ANSI parsing, daemon state/IPC contracts, desktop notifications, view models, raw input delivery, UI rendering, and multiple code paths where independent subtle defects are plausible.

Grey Divider

Tip of the day
💡 Did you know, you can choose which labels appear on a finding, and whether they show icons or text

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

/// the option set depends on the account, and the transcript never records it. A match is the
/// title plus at least two of the choices the CLI actually offers, so a sentence that merely
/// quotes one of them does not raise a question.
internal sealed class ClaudeUsageLimitDetector {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Claude parser sits outside its harness 📘 Rule violation ⌂ Architecture

ClaudeUsageLimitDetector embeds Claude-only prompts and choices while declaring the generic
Capacitor.Cli.Daemon.Services namespace. The detector is instantiated only for Claude terminal
agents, while analogous vendor parsers live under Harness/<Vendor>, so future vendor-specific
changes are split across two locations.
Agent Prompt
## Issue description
`ClaudeUsageLimitDetector` contains Claude-specific terminal parsing but is located in the generic daemon services directory, contrary to the required vendor harness organization.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/ClaudeUsageLimitDetector.cs[1-91]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[3043-3045]

## Recommended Fix
Move the detector to `src/Capacitor.Cli.Daemon/Harness/Claude/ClaudeUsageLimitDetector.cs`, change its namespace to `Capacitor.Cli.Daemon.Harness.Claude`, and update the orchestrator and tests to import the new namespace.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This scraper is called from the orchestrator read loop, next to ConsentDialogDetector, which already reads the same Claude PTY from Services. Harness/Claude is the launcher and the policies. Moving only this type would split the two scrapers.

Comment thread src/Capacitor.Cli.Daemon/Services/AnsiScreen.cs
Comment thread src/Capacitor.App/ViewModels/ChatTabViewModel.cs
Comment thread src/Capacitor.Cli.Daemon/Services/ClaudeUsageLimitDetector.cs
Comment on lines +669 to +674
async Task ChooseUsageLimitAsync(int index) {
if (index is < 1 or > 9) return;
UsageLimitError = "";
var sent = await _input.SendKeyAsync((byte)('0' + index), _lifetimeToken);
if (!sent && !_lifetimeToken.IsCancellationRequested)
UsageLimitError = "The terminal is not attached, so that choice was not sent.";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Old menu clicks send digits 🐞 Bug ≡ Correctness

ChooseUsageLimitAsync receives only an option index and sends it without checking that the notice
and option which created the command are still current. A status update can clear or replace the
menu after a click is dispatched, allowing that stale callback to write its digit into the
replacement menu or ordinary terminal input.
Agent Prompt
Issue description
A choice callback can outlive the usage-limit menu that created it and write a stale digit to the terminal.

Fix Focus Areas
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[654-675]
- src/Capacitor.App/ViewModels/TerminalTabViewModel.cs[218-226]

Recommended Fix
Associate every choice command with a menu generation or immutable notice snapshot. Cancel its token when `ApplyUsageLimit` replaces or clears that snapshot, and verify the generation immediately before raw input is sent, including after waiting for an in-flight delivery.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A second click on the same notice no longer sends: the choice stays taken until the notice changes, including across the wait for an in-flight paste. A click that starts after the notice has been replaced is a new command. A send that has already passed that check is not cancelled if the menu disappears while a paste is draining. Escape has the same shape.

Comment thread src/Capacitor.Cli.Daemon/Services/AnsiScreen.cs
The digit is written before the menu leaves the screen, so another click would land on the next prompt.
@nortonandreev

Copy link
Copy Markdown
Contributor Author

@codex review

@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: 9d115cd7d0

ℹ️ 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 +519 to +520
this.WhenAnyValue(x => x.HasUsageLimitQuestion),
(text, can, readOnly, limit) => can && !readOnly && !limit && !string.IsNullOrWhiteSpace(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.

P1 Badge Recheck the usage-limit gate after attachment uploads

The new limit check only participates in CanExecute at command start. If a send with attachments is already awaiting UploadAsync when Claude renders the usage-limit menu, execution resumes and calls _input.SendAsync without checking HasUsageLimitQuestion again; for a terminal-backed chat that pastes the original message into the live Claude menu instead of sending a chat turn. Recheck the limit immediately before dispatching the prompt (and leave the draft/chips staged) after any await.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The send now checks the menu again after the upload, before the text is dispatched. The draft and the chips stay staged. A_send_still_uploading_when_the_usage_limit_appears_is_not_pasted_into_the_menu holds the upload, raises the menu, then releases it.

}
}

return new UsageLimitNoticeDto(UsageLimitKinds.Blocked, summary ?? "Usage limit reached", Prompt, options);

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 Require an actual limit banner before surfacing choices

A normal Claude response that happens to print What do you want to do? followed by two numbered known choices is accepted as a blocking menu even if no usage/limit/reset banner is present: summary is optional and this fallback creates a usage-limit notice. This can occur when a user asks Claude to reproduce or explain the menu, and it disables the composer and routes clicks as raw menu keys until the screen changes. Require the adjacent limit banner (or another menu-specific terminal marker) before returning a notice.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A notice now requires the limit line above the title. The title and two known choices without that line are not a question, including when the choice text itself contains "limit". A_title_and_two_choices_without_a_limit_line_is_not_a_question pins it.

The upload can finish after the menu is on screen, and the text would be typed into it. The draft and the chips stay put.
The title and two known choices are also what a session prints when asked to show the menu. The limit line above the title is what makes it the real prompt.
@alexeyzimarev
alexeyzimarev merged commit e2d8fd5 into main Sep 24, 2026
8 checks passed
@alexeyzimarev
alexeyzimarev deleted the nortonandreev/ai-3151-show-a-vendor-usage-limit-in-the-desktop-app branch September 24, 2026 13:59
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.

2 participants