Skip to content

[AI-2156] kcap setup creates the flow and polls it to completion - #640

Open
George-Payne wants to merge 1 commit into
mainfrom
georgepayne/ai-2156-setup-creates-and-polls-flow
Open

[AI-2156] kcap setup creates the flow and polls it to completion#640
George-Payne wants to merge 1 commit into
mainfrom
georgepayne/ai-2156-setup-creates-and-polls-flow

Conversation

@George-Payne

Copy link
Copy Markdown
Member

Gives the server's two rendezvous routes a caller. They shipped with none, so nothing generated a flow id and the browser's claim-on-arrival was what established ownership - which is where it sat under the retired pairing, and is the one property of the design the server half could not realise alone.

The leg runs after login, since both routes are authenticated. It generates a 128-bit base64url id, creates the flow, opens {server}/setup?s=<id>, and polls until every step it knows has settled.

  • FirstRunFlowId - 16 CSPRNG bytes as base64url, 22 characters. The server's floor is what makes that the only shape that fits; it can check length and alphabet but never entropy, so the guarantee is the generator's alone.
  • FirstRunFlowClient - the two routes, degrading rather than throwing. Refusals are handled apart: 404/401/403/405 on the create mean the tenant does not serve the flow and say nothing to the user; 429 reports the server's own Retry-After rather than sleeping through ten minutes of it; 409 retries with a fresh id, since it means the id is taken rather than the credentials wrong.
  • FirstRunFlowPoll - the poll's decision, extracted so every branch is tested without a socket. 410 is a dead link, 404 a flow that will never be ours, 401 a re-login rather than a new link, and 5xx or a transport blip is another tick.
  • FirstRunFlowOutcomes - outcomes, never instructions. Step and status strings map onto closed local sets and an unrecognised member is dropped, because kcap setup writes Claude Code hooks and a hook entry is a command string Claude Code runs. Which steps are gates stays the server's to say, through can_finish, rather than being restated here where an old CLI could get it wrong.
  • BrowserFirstRunFlow - create, then open, then poll. The setup URL is composed locally, so unlike the pairing there is no server-supplied URL reaching a shell-executed open to validate.
  • SetupCommand - an unnumbered leg after login. Skipped on --no-prompt and the None provider. Headless deliberately is not a skip: the link is printed as well as opened, which is what keeps the screens available to the device-path population rather than designing it out of them.
  • Any key ends the wait. The 30-minute budget is the backstop for a terminal nobody is sitting at; a closed tab should not cost half an hour of dots.

Two things worth flagging for review. 401/403 on the create are read as "no flow here" even though the route is authenticated - a gateway answering them on a path it does not know is indistinguishable from the feature being off, and a login succeeded seconds earlier, so guessing wrong here silently skips an additive leg while guessing the other way prints an alarming auth failure on every tenant that has the flow off. And the leg reports, configuring nothing: the screens that would push configuration are their own tickets, so the terminal steps remain what wires the machine up, and which of the two renders a given step is a decision that belongs to neither.

Unblocks two things the server half deferred: refusing a flow no CLI created (and with it metering the claim path, which is unlimited today), and s being single-consumer.

Capacitor.Cli.Core.Tests.Unit and Capacitor.Cli.Tests.Unit are green apart from WriteAndBootstrap_writes_the_unit_and_bootstraps_without_a_leading_bootout, which fails identically on an unmodified tree - it refuses a group-writable temp directory, which is a devcontainer artefact rather than anything here. AOT publish is clean of IL2026/IL3050.

AI-2156

The server's two rendezvous routes shipped with no caller at all, so nothing
generated a flow id and the browser's claim-on-arrival was what established
ownership - which is where it sat under the retired pairing, and the one
property of the design the server half could not realise alone.

The leg runs after login, since both routes are authenticated. It generates a
128-bit base64url id, creates the flow, opens {server}/setup?s=<id>, and polls
until every step it knows has settled.

- Refusals are handled apart: 404/401/403/405 on the create mean the tenant
  does not serve the flow, and say nothing; 429 reports the server's own
  Retry-After rather than sleeping through it; 409 retries with a fresh id,
  since it means the id is taken rather than the credentials wrong.
- The poll's decision is extracted and unit-tested per branch. 410 is a dead
  link, 404 a flow that will never be ours, 401 a re-login rather than a new
  link, and 5xx or a transport blip is another tick.
- Outcomes, never instructions. Step and status strings map onto closed local
  sets and an unrecognised member is dropped, because kcap setup writes Claude
  Code hooks and a hook entry is a command string Claude Code runs. Which steps
  are gates stays the server's to say, via can_finish.
- The setup URL is composed locally, so unlike the pairing there is no
  server-supplied URL reaching a shell-executed open to validate.
- Any key ends the wait. The 30-minute budget is the backstop for a terminal
  nobody is sitting at; a closed tab should not cost half an hour of dots.
- Headless is deliberately not a skip - the link is printed as well as opened,
  which is what keeps the screens available to the device-path population.

The leg reports and configures nothing: the screens that would push
configuration are their own tickets, and the terminal steps remain what wires
the machine up.
@George-Payne George-Payne self-assigned this Aug 21, 2026
@linear-code

linear-code Bot commented Aug 21, 2026

Copy link
Copy Markdown

AI-2156

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Create and poll browser first-run setup flow during kcap setup

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Create an authenticated first-run flow ID before opening /setup, then poll until finished.
• Add resilient HTTP client + poll classification, including keypress escape hatch and rate-limit
 handling.
• Document the browser-finishing leg and add thorough unit tests for all branches.
Diagram

sequenceDiagram
  actor U as "User"
  participant SC as "SetupCommand"
  participant BF as "BrowserFirstRunFlow"
  participant FC as "FirstRunFlowClient"
  participant API as "Tenant API"
  participant B as "Browser"

  U->>SC: "kcap setup"
  SC->>BF: "RunAsync(serverUrl, machine)"
  BF->>FC: "CreateAsync(flowId)"
  FC->>API: "POST /api/first-run/flows"
  API-->>FC: "200 (flow state) | 404/401/403/405 | 409 | 429"
  FC-->>BF: "FirstRunCreateOutcome"
  BF->>B: "Open server/setup?s=<flowId>"
  loop "poll until finished / timeout / keypress"
    BF->>FC: "PollAsync(flowId)"
    FC->>API: "GET /api/first-run/flows/<flowId>"
    API-->>FC: "200 (state) | 410 | 404 | 401/403 | 429 | 5xx/transport"
    FC-->>BF: "FirstRunPollOutcome"
  end
  BF-->>SC: "FirstRunFlowResult"
  SC-->>U: "one-line outcome; continue setup"
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Server-minted flow IDs (CLI requests create-without-id)
  • ➕ Removes client responsibility for entropy/shape guarantees
  • ➕ Eliminates 409 collision retry logic from the CLI
  • ➖ Undermines the key design goal (CLI owns the flow before browser arrives) unless create is still authenticated and strongly bound to caller
  • ➖ Would require server API changes and coordinated rollout
2. Push-based completion (SSE/WebSocket) instead of polling
  • ➕ Lower server load and better UX responsiveness without frequent GETs
  • ➕ More natural place for rate limiting/backpressure
  • ➖ Significantly more complexity in CLI networking and server infrastructure
  • ➖ Harder to make robust across proxies, corporate networks, and CLI environments
3. Long-polling with server-side wait
  • ➕ Reduces request frequency while keeping simple HTTP semantics
  • ➕ Can provide near-real-time completion without 2s polling cadence
  • ➖ More server complexity and resource holding
  • ➖ Still needs timeout/backoff behavior and careful cancellation handling

Recommendation: Keep the PR’s current approach (client-minted 128-bit ID, create-then-open, short-interval polling with explicit verdict classification). It best matches the ownership model goal while remaining operationally simple and robust in typical CLI networking conditions; the code already mitigates risks via degraded outcomes, bounded retries/backoff, and comprehensive unit tests.

Files changed (18) +1511 / -0

Enhancement (10) +685 / -0
BrowserFirstRunFlow.csImplement create-open-poll orchestration for first-run flow +174/-0

Implement create-open-poll orchestration for first-run flow

• Introduces the main browser-first-run controller: generate ID, create flow (with conflict retries), open locally-composed setup URL, and poll until finished/terminal/budget. Adds keypress-to-dismiss behavior, 429 backoff, and detailed status handling.

src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs

FirstRunFlowClient.csAdd HTTP client seam for first-run flow create/poll routes +90/-0

Add HTTP client seam for first-run flow create/poll routes

• Adds an HttpClient-based implementation of create and poll requests that returns outcomes instead of throwing on transient errors. Handles JSON serialization/deserialization, Retry-After parsing for 429, and preserves HTTP status vs transport failure (status 0).

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs

FirstRunFlowId.csGenerate 128-bit base64url flow IDs (22 chars) +21/-0

Generate 128-bit base64url flow IDs (22 chars)

• Adds a dedicated generator for first-run flow IDs using CSPRNG 16 bytes and base64url encoding. Encodes design constraint that the server can validate length/alphabet but not entropy.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowId.cs

FirstRunFlowModels.csDefine request/response models for first-run flow API +45/-0

Define request/response models for first-run flow API

• Adds wire DTOs for POST create and flow state response, with snake_case JSON property names. Captures step, can_finish gate, and per-step outcomes mapping used by polling logic.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowModels.cs

FirstRunFlowOutcomes.csMap wire step/outcome strings onto closed local enums +108/-0

Map wire step/outcome strings onto closed local enums

• Introduces closed enums for known steps and outcomes plus mapping helpers that drop unknown values. Implements IsFinished based on server’s can_finish plus all known steps being settled, ensuring unknown new-server steps don’t stall old CLIs.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowOutcomes.cs

FirstRunFlowPoll.csExtract pure poll verdict classifier for HTTP responses +60/-0

Extract pure poll verdict classifier for HTTP responses

• Adds a deterministic classifier mapping status/body readability to loop verdicts (state/expired/gone/unauthenticated/slowdown/wait). Enables unit testing of poll semantics without network dependencies.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowPoll.cs

FirstRunFlowProgress.csAdd progress interface for rendering browser-leg UX +21/-0

Add progress interface for rendering browser-leg UX

• Defines an abstraction for rendering “opening browser”, poll ticks, and wait-ended behavior. Allows SetupCommand to supply Spectre.Console-based output while keeping core flow logic testable.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowProgress.cs

FirstRunFlowResult.csDefine result model for browser leg outcomes +38/-0

Define result model for browser leg outcomes

• Adds a discriminated result set capturing finished, expired, abandoned (budget), dismissed (keypress), unavailable (feature off), rate limited, and failed states. Ensures the browser leg never throws for reachable failures and setup can continue.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowResult.cs

Models.csRegister first-run flow DTOs for source-generated JSON +2/-0

Register first-run flow DTOs for source-generated JSON

• Adds JsonSerializable registrations for CreateFirstRunFlowRequest and FirstRunFlowResponse in the shared JSON context. Ensures the new HTTP client uses the same serialization infrastructure as the rest of the CLI.

src/Capacitor.Cli.Core/Models.cs

SetupCommand.csRun browser first-run flow after login with Spectre output +126/-0

Run browser first-run flow after login with Spectre output

• Adds an unnumbered post-login browser leg that creates and polls the first-run flow when available. Implements Spectre.Console progress rendering and a single-line outcome summary, skipping on --no-prompt and AuthProvider.None while continuing terminal setup regardless.

src/Capacitor.Cli/Commands/SetupCommand.cs

Tests (6) +804 / -0
BrowserFirstRunFlowTests.csAdd unit tests for create-open-poll loop, backoff, and keypress +422/-0

Add unit tests for create-open-poll loop, backoff, and keypress

• Introduces a fake channel, fake clock, and fake key watcher to test ordering (create before open), URL composition, 409 retry behavior, terminal statuses, 429 slowdown, polling budget, and keypress dismissal/drain semantics.

test/Capacitor.Cli.Core.Tests.Unit/FirstRun/BrowserFirstRunFlowTests.cs

FirstRunFlowClientTests.csTest wire contract for first-run flow HTTP client +158/-0

Test wire contract for first-run flow HTTP client

• Uses WireMock to verify create/poll paths, snake_case fields, Retry-After parsing, trailing slash tolerance, and handling of unreadable JSON vs transport failure. Guards against silent “unavailable” regressions caused by wrong paths or fields.

test/Capacitor.Cli.Core.Tests.Unit/FirstRun/FirstRunFlowClientTests.cs

FirstRunFlowIdTests.csTest flow ID length, alphabet, and non-repetition +31/-0

Test flow ID length, alphabet, and non-repetition

• Pins the generator to 22-character base64url output and verifies allowed characters. Adds a small uniqueness check to catch accidental determinism/regressions.

test/Capacitor.Cli.Core.Tests.Unit/FirstRun/FirstRunFlowIdTests.cs

FirstRunFlowOutcomesTests.csTest closed-set mapping and finish criteria enforcement +114/-0

Test closed-set mapping and finish criteria enforcement

• Verifies that unknown step/outcome strings are dropped (read as pending) and that can_finish gates completion. Ensures newer-server extra steps don’t stall old CLIs and that non-gate failures don’t prevent finish.

test/Capacitor.Cli.Core.Tests.Unit/FirstRun/FirstRunFlowOutcomesTests.cs

FirstRunFlowPollTests.csTest poll response classification matrix +32/-0

Test poll response classification matrix

• Covers each branch of FirstRunFlowPoll.Classify, including unreadable 200 bodies, 404-as-gone semantics, and unauthenticated vs retryable conditions. Ensures unexpected responses don’t silently spin without a correct terminal interpretation.

test/Capacitor.Cli.Core.Tests.Unit/FirstRun/FirstRunFlowPollTests.cs

SetupCommandTests.csTest SetupCommand’s browser-leg outcome messaging +47/-0

Test SetupCommand’s browser-leg outcome messaging

• Adds tests to ensure only Finished is treated as success, timeouts/expiry warn, dismissal does not warn, rate limits are rounded up to whole minutes, and failure messages are escaped for Spectre markup safety.

test/Capacitor.Cli.Tests.Unit/Commands/SetupCommandTests.cs

Documentation (2) +22 / -0
README.mdDocument browser-based finishing step in setup flow +15/-0

Document browser-based finishing step in setup flow

• Adds an explicit description of the post-login browser setup leg, including sample output and guidance for headless/remote completion. Clarifies that setup continues in-terminal and that waiting can be dismissed with any key.

README.md

help-setup.txtUpdate setup help text to describe browser-finishing leg +7/-0

Update setup help text to describe browser-finishing leg

• Documents that, after sign-in, setup may open a browser link and wait for completion, with printed URL fallback and keypress escape. Notes it is skipped under --no-prompt and on servers without the feature.

src/Capacitor.Cli.Core/Resources/help-setup.txt

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Cancellation swallowed in client 🐞 Bug ☼ Reliability
Description
FirstRunFlowClient treats OperationCanceledException as a transient transport blip and converts it
to StatusCode=0, which prevents CancellationToken cancellation (e.g., Ctrl+C / host shutdown) from
aborting browser setup polling. This can leave setup stuck polling until the 30-minute PollBudget
instead of stopping promptly on cancellation.
Code

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[R88-89]

+    static bool IsTransient(Exception e) =>
+        e is HttpRequestException or OperationCanceledException or JsonException or NotSupportedException;
Evidence
The new client explicitly includes OperationCanceledException in its transient filter, so any
cancellation (including caller-requested) is flattened into StatusCode=0. In contrast, existing
polling flows in the repo only treat OperationCanceledException as transient when the caller token
was not canceled, or they rethrow when the caller token is canceled.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[39-58]
src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[61-74]
src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[88-89]
src/Capacitor.Cli.Core/Auth/OAuthLoginFlow.cs[211-219]
src/Capacitor.Cli.Core/Config/ServerUrlNormalizer.cs[114-123]

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

## Issue description
`FirstRunFlowClient` currently classifies `OperationCanceledException` as transient and degrades it to `StatusCode = 0`. This swallows legitimate caller cancellation (`ct.IsCancellationRequested == true`), which means higher-level cancellation cannot stop the browser setup flow promptly.

## Issue Context
Elsewhere in the codebase, cancellation is explicitly preserved (either rethrown or only treated as transient when the *caller token* is not canceled).

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[39-74]
- src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[76-90]

### Implementation notes
- Update `CreateAsync` / `PollAsync` / `ReadAsync` to **rethrow** `OperationCanceledException` when `ct.IsCancellationRequested` is true.
- Only degrade `OperationCanceledException` to status 0 when it represents a timeout or other non-caller cancellation (i.e., `!ct.IsCancellationRequested`).
- One simple pattern:
 - `catch (OperationCanceledException) when (!ct.IsCancellationRequested) { return new(0, null); }`
 - `catch (HttpRequestException) { ... }` etc.
- Remove `OperationCanceledException` from the generic `IsTransient(Exception e)` helper, or replace the helper with overloads that can examine `ct`.

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



Remediation recommended

2. Verbose docblocks in FirstRun 📘 Rule violation ⚙ Maintainability
Description
Several newly-added comment blocks are overly long and include historical narrative (e.g., retired
pairing/spec references) that reduces readability and exceeds the “minimal, non-obvious rationale”
standard. This increases maintenance cost by burying the intent in multi-paragraph prose instead of
concise constraints.
Code

src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[R8-11]

+/// <para><b>Create-then-redirect, and that order is the whole point.</b> The browser then arrives at a
+/// flow that already has an owner, so the server's ownership check has something to check from the
+/// first request rather than from whenever a browser happens to turn up. Reversed, the first browser
+/// to open the link owns the flow — which is where it sat under the retired pairing, and is the one
Evidence
PR Compliance ID 25 requires comments to be minimal and provide only non-obvious rationale. The
added multi-paragraph XML/doc comments in the new FirstRun flow and Setup command include extended
narrative/historical context, making them unnecessarily verbose under this rule.

CLAUDE.md: Code Comments Must Be Minimal and Provide Non-Obvious Rationale (No Restating Code or Narrating Changes)
src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[5-18]
src/Capacitor.Cli/Commands/SetupCommand.cs[920-933]

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

## Issue description
Newly added comments are overly verbose and include historical/narrative detail rather than minimal, non-obvious rationale.

## Issue Context
Rule requires comments to be short and focused on important rationale/constraints, not long narration.

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[5-18]
- src/Capacitor.Cli/Commands/SetupCommand.cs[920-933]

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


3. Retry-After date ignored 🐞 Bug ≡ Correctness
Description
FirstRunFlowClient only reads Retry-After as a delta, so servers that send Retry-After as an HTTP
date will be treated as having no Retry-After and will fall back to the hardcoded 10-minute default.
This can misreport when browser setup will be available again.
Code

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[R52-55]

+            if (!resp.IsSuccessStatusCode)
+                return new((int)resp.StatusCode, null, resp.Headers.RetryAfter?.Delta);
+
+            return new((int)resp.StatusCode, await ReadAsync(resp, ct));
Evidence
The new code only reads the delta form of Retry-After; the codebase already has a helper that
handles both delta and date forms, showing this omission will lead to incorrect behavior when
date-form headers are used.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[52-55]
src/Capacitor.Cli/SessionStartMemory/SessionStartContextFetch.cs[76-83]
src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[77-79]

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

## Issue description
`FirstRunFlowClient.CreateAsync` only uses `resp.Headers.RetryAfter?.Delta`. If the server uses the date form (`RetryAfter.Date`), the value is ignored and the caller will fall back to a default (10 minutes), producing misleading messaging.

## Issue Context
There is already repo precedent for correctly parsing both delta and date forms.

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[39-58]

### Implementation notes
- When building `FirstRunCreateOutcome` for non-success responses, compute RetryAfter roughly like:
 - `var retryAfter = resp.Headers.RetryAfter?.Delta;
    if (retryAfter is null && resp.Headers.RetryAfter?.Date is { } date) {
       var v = date - DateTimeOffset.UtcNow;
       retryAfter = v > TimeSpan.Zero ? v : null;
    }`
- Keep `RetryAfter` null when absent/unparseable so the higher layer’s fallback remains intact.

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


Grey Divider

Tip of the day
💡 Did you know, you can tweak Display preferences with a live preview to see your comment before it ships

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +8 to +11
/// <para><b>Create-then-redirect, and that order is the whole point.</b> The browser then arrives at a
/// flow that already has an owner, so the server's ownership check has something to check from the
/// first request rather than from whenever a browser happens to turn up. Reversed, the first browser
/// to open the link owns the flow — which is where it sat under the retired pairing, and is the one

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

1. Verbose docblocks in firstrun 📘 Rule violation ⚙ Maintainability

Several newly-added comment blocks are overly long and include historical narrative (e.g., retired
pairing/spec references) that reduces readability and exceeds the “minimal, non-obvious rationale”
standard. This increases maintenance cost by burying the intent in multi-paragraph prose instead of
concise constraints.
Agent Prompt
## Issue description
Newly added comments are overly verbose and include historical/narrative detail rather than minimal, non-obvious rationale.

## Issue Context
Rule requires comments to be short and focused on important rationale/constraints, not long narration.

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[5-18]
- src/Capacitor.Cli/Commands/SetupCommand.cs[920-933]

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

Comment on lines +88 to +89
static bool IsTransient(Exception e) =>
e is HttpRequestException or OperationCanceledException or JsonException or NotSupportedException;

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

2. Cancellation swallowed in client 🐞 Bug ☼ Reliability

FirstRunFlowClient treats OperationCanceledException as a transient transport blip and converts it
to StatusCode=0, which prevents CancellationToken cancellation (e.g., Ctrl+C / host shutdown) from
aborting browser setup polling. This can leave setup stuck polling until the 30-minute PollBudget
instead of stopping promptly on cancellation.
Agent Prompt
## Issue description
`FirstRunFlowClient` currently classifies `OperationCanceledException` as transient and degrades it to `StatusCode = 0`. This swallows legitimate caller cancellation (`ct.IsCancellationRequested == true`), which means higher-level cancellation cannot stop the browser setup flow promptly.

## Issue Context
Elsewhere in the codebase, cancellation is explicitly preserved (either rethrown or only treated as transient when the *caller token* is not canceled).

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[39-74]
- src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[76-90]

### Implementation notes
- Update `CreateAsync` / `PollAsync` / `ReadAsync` to **rethrow** `OperationCanceledException` when `ct.IsCancellationRequested` is true.
- Only degrade `OperationCanceledException` to status 0 when it represents a timeout or other non-caller cancellation (i.e., `!ct.IsCancellationRequested`).
- One simple pattern:
  - `catch (OperationCanceledException) when (!ct.IsCancellationRequested) { return new(0, null); }`
  - `catch (HttpRequestException) { ... }` etc.
- Remove `OperationCanceledException` from the generic `IsTransient(Exception e)` helper, or replace the helper with overloads that can examine `ct`.

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

Comment on lines +52 to +55
if (!resp.IsSuccessStatusCode)
return new((int)resp.StatusCode, null, resp.Headers.RetryAfter?.Delta);

return new((int)resp.StatusCode, await ReadAsync(resp, ct));

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

3. Retry-after date ignored 🐞 Bug ≡ Correctness

FirstRunFlowClient only reads Retry-After as a delta, so servers that send Retry-After as an HTTP
date will be treated as having no Retry-After and will fall back to the hardcoded 10-minute default.
This can misreport when browser setup will be available again.
Agent Prompt
## Issue description
`FirstRunFlowClient.CreateAsync` only uses `resp.Headers.RetryAfter?.Delta`. If the server uses the date form (`RetryAfter.Date`), the value is ignored and the caller will fall back to a default (10 minutes), producing misleading messaging.

## Issue Context
There is already repo precedent for correctly parsing both delta and date forms.

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[39-58]

### Implementation notes
- When building `FirstRunCreateOutcome` for non-success responses, compute RetryAfter roughly like:
  - `var retryAfter = resp.Headers.RetryAfter?.Delta;
     if (retryAfter is null && resp.Headers.RetryAfter?.Date is { } date) {
        var v = date - DateTimeOffset.UtcNow;
        retryAfter = v > TimeSpan.Zero ? v : null;
     }`
- Keep `RetryAfter` null when absent/unparseable so the higher layer’s fallback remains intact.

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

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