Skip to content

Improve desktop rail feedback for selection, launches and pull requests - #950

Merged
realtonyyoung merged 13 commits into
mainfrom
desktop-rail-feedback
Sep 15, 2026
Merged

realtonyyoung merged 13 commits into
mainfrom
desktop-rail-feedback

Conversation

@realtonyyoung

@realtonyyoung realtonyyoung commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

No tracker issue on either side: the four changes were requested directly.

What & why

Four desktop fixes. The open session row was indistinguishable from a hovered one, so it now carries an accent edge, its own background and a heavier title. A launch showed nothing until the daemon finished the runtime handshake; the daemon now reports in-flight launches and their stage over the local status snapshot, and the app adds a placeholder row the moment the server accepts, so the rail row and a stage line in the workspace appear at once. A triple click selected the whole text box; one application-wide tunnel handler selects the line under the pointer instead. The worktree's branch glyph and the PR card's labels share one colour vocabulary: red for closed or failing checks, green ready, muted grey draft, pulsing grey while checks run, purple merged, the warning colour for conflicts.

Where to look

Conflicts need a mergeable field the server does not send yet; the client reads it as null until it does. The tone cache reads every listed session's server-linked PRs every two minutes through the reader's own reveal gate, but keeps a tone past the 30-second window: it is one colour per worktree, not the protected content the window guards, and re-reading each PR every 30 s would multiply GitHub reads. The gh fallback needs the work-context repository the rail rows lack, so local-only PRs stay uncoloured. Markdown bodies keep MarkView's own selection, which takes no click count.

Verification

Capacitor.App.Tests.Unit        total 2014, failed 0 (before the review fixes); 166 across the affected suites after them, failed 0
Capacitor.Cli.Core.Tests.Unit   total 3301, failed 0 (9 skipped); status DTO suites 25 passed after the type move
Capacitor.Cli.Daemon.Tests.Unit (status IPC, pending launch, launch stage suites)  total 48, failed 0
dotnet publish src/Capacitor.Cli -c Release   killed twice locally by memory pressure during native code generation; the CI AOT job is the check

The triple-click UI tests fail with the handler uninstalled (SelectedText reads the double-click word), so they pin the behaviour rather than Avalonia's default.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XWRNeNnUJeSbRrQQFwuSui

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Improve rail feedback for launches, selection, and pull requests

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Show accepted launches immediately, then replace placeholders with daemon-published sessions and
 live stages.
• Distinguish selected sessions and unify rail and pull-request status colors.
• Make triple-click select the logical line across supported Avalonia text controls.
Diagram

graph TD
    USER["Desktop user"] --> APP["App shell"] --> DIR["Agent directory"] --> UI["Rail workspace"]
    DAEMON["Launch orchestrator"] --> IPC["Status IPC"] --> DIR
    PR["PR source"] --> TONE["Tone cache"] --> UI
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Daemon-only pending rows
  • ➕ Uses one authoritative producer for launch state.
  • ➕ Avoids client-generated placeholder expiry and reconciliation.
  • ➖ Leaves the acceptance-to-daemon-reporting gap blank.
  • ➖ Cannot provide immediate feedback when server acceptance precedes local daemon observation.
2. Server-pushed launch progress
  • ➕ Could provide one end-to-end launch lifecycle across local and remote targets.
  • ➕ Would remove local snapshot polling as the progress transport.
  • ➖ Requires a broader server protocol and deployment change.
  • ➖ Adds unnecessary latency and complexity for local launches.
3. Server-pushed PR tones
  • ➕ Avoids periodic per-session client reads.
  • ➕ Could scale better for very large session lists.
  • ➖ Requires server-side aggregation and a new subscription contract.
  • ➖ Moves presentation-specific precedence into the backend.

Recommendation: Keep the PR's complementary client-placeholder and daemon-snapshot approach: it closes both sides of the launch visibility gap while preserving published agent rows as authoritative. The centralized client tone cache and application-wide tunnel handler also fit the existing desktop architecture; server-pushed progress or tones are only worth revisiting if scale or remote-launch requirements expand.

Files changed (46) +1527 / -99

Enhancement (23) +445 / -35
App.axamlAdd selection and shared pulse resources +17/-0

Add selection and shared pulse resources

• Introduces a dedicated selected-row brush and a reusable pulsing animation for in-flight launches and running checks.

src/Capacitor.App/App.axaml

App.axaml.csWire pending launches, PR tones, and line selection into startup +13/-6

Wire pending launches, PR tones, and line selection into startup

• Installs the global line-selection handler, creates and disposes the PR tone cache, injects new dependencies, and treats pending agents as local workspace targets.

src/Capacitor.App/App.axaml.cs

AgentDirectory.csReconcile launch placeholders with pending and published agents +41/-6

Reconcile launch placeholders with pending and published agents

• Consumes daemon pending launches and merges them with app placeholders and published rows. Published agents retire stand-ins, daemon entries temporarily supersede placeholders, and stale placeholders expire after ten minutes.

src/Capacitor.App/Services/AgentDirectory.cs

AgentRow.csRepresent pending launches as directory rows +25/-4

Represent pending launches as directory rows

• Adds the Pending origin and launch-stage metadata, with factories for daemon-reported launches and app-created placeholders.

src/Capacitor.App/Services/AgentRow.cs

DaemonClientService.csProject pending launches from daemon snapshots +4/-0

Project pending launches from daemon snapshots

• Maintains a keyed pending-launch cache alongside published agents and updates it on connection and status events.

src/Capacitor.App/Services/DaemonClientService.cs

IDaemonClientService.csExpose daemon pending launches +4/-0

Expose daemon pending launches

• Extends the daemon client contract with a keyed cache of launches still being started.

src/Capacitor.App/Services/IDaemonClientService.cs

PullRequestToneCache.csCache pull-request tones for all listed sessions +119/-0

Cache pull-request tones for all listed sessions

• Periodically reads each listed session's PR links and overviews, publishes reduced tones, preserves values across transient failures, and clears denied or removed sessions.

src/Capacitor.App/Services/PullRequestToneCache.cs

LaunchStages.csFormat runtime launch stages for display +25/-0

Format runtime launch stages for display

• Maps known runtime stages to user-facing labels and humanizes unknown stage tokens without dropping them.

src/Capacitor.App/ViewModels/LaunchStages.cs

PullRequestStatus.csDifferentiate draft, conflict, and running-check states +6/-2

Differentiate draft, conflict, and running-check states

• Adds muted and pulsing presentation flags, assigns warning styling to conflicts, and supplies draft and conflict icons.

src/Capacitor.App/ViewModels/PullRequestStatus.cs

PullRequestTone.csDefine actionable pull-request tones +5/-0

Define actionable pull-request tones

• Introduces an ordered state vocabulary shared by PR cards and worktree branch glyphs.

src/Capacitor.App/ViewModels/PullRequestTone.cs

PullRequestTones.csReduce PR overviews to shared tones +50/-0

Reduce PR overviews to shared tones

• Maps lifecycle, checks, draft, and mergeability data into prioritized tones, labels, and lifecycle statuses.

src/Capacitor.App/ViewModels/PullRequestTones.cs

RailRepoViewModel.csPass PR tones into worktree view models +3/-2

Pass PR tones into worktree view models

• Threads the shared tone stream through repository groups to each worktree.

src/Capacitor.App/ViewModels/RailRepoViewModel.cs

RailSessionViewModel.csPresent pending sessions as actively starting +8/-3

Present pending sessions as actively starting

• Displays launch stages instead of age metadata, marks pending rows for animation, and opens them through the local path.

src/Capacitor.App/ViewModels/RailSessionViewModel.cs

RailWorktreeViewModel.csAggregate PR tones per worktree +28/-1

Aggregate PR tones per worktree

• Selects the strongest tone across a worktree's sessions and exposes glyph color, pulse state, and tooltip metadata.

src/Capacitor.App/ViewModels/RailWorktreeViewModel.cs

SessionRailViewModel.csFeed PR tones and pending rows through the rail +8/-2

Feed PR tones and pending rows through the rail

• Marshals tone updates to the UI thread and expands worktrees when an auto-opened session is still pending.

src/Capacitor.App/ViewModels/SessionRailViewModel.cs

PullRequestStatusLabel.axamlPulse running pull-request checks +1/-1

Pulse running pull-request checks

• Applies the shared pulsing class when a status represents checks in progress.

src/Capacitor.App/Views/PullRequestStatusLabel.axaml

PullRequestToneBrushConverter.csMap PR tones to application palette brushes +30/-0

Map PR tones to application palette brushes

• Centralizes branch-glyph color selection so rail tones use the same success, warning, danger, muted, and merged palette.

src/Capacitor.App/Views/PullRequestToneBrushConverter.cs

WorkspaceView.axamlShow an in-workspace launch-stage panel +16/-0

Show an in-workspace launch-stage panel

• Overlays the empty workspace with a pulsing startup panel until the daemon publishes the agent.

src/Capacitor.App/Views/WorkspaceView.axaml

StatusIpc.csAdd pending launches to the local status contract +11/-1

Add pending launches to the local status contract

• Extends daemon snapshots with an optional pending list and defines its identity, repository, title, creation time, and stage payload.

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

PullRequestOverviewDto.csRead optional pull-request mergeability +3/-0

Read optional pull-request mergeability

• Adds the nullable mergeable field needed to distinguish conflicting pull requests while remaining compatible with current servers.

src/Capacitor.Cli.Core/PullRequests/PullRequestOverviewDto.cs

AgentOrchestrator.LocalIpc.csSnapshot in-flight launches for local clients +11/-0

Snapshot in-flight launches for local clients

• Projects unpublished launches and their current stages into deterministic, creation-ordered status entries.

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

AgentOrchestrator.csTrack launch identity and pulse status changes +15/-6

Track launch identity and pulse status changes

• Retains vendor, repository, and title data for pending launches and notifies local status consumers when tracking or launch stages change.

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

DaemonStatusIpc.csPublish pending launches in status snapshots +2/-1

Publish pending launches in status snapshots

• Includes the orchestrator's pending-launch projection in each serialized local status payload.

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

Bug fix (5) +139 / -30
HomeViewModel.csCreate and retire accepted-launch placeholders +17/-8

Create and retire accepted-launch placeholders

• Adds local placeholders only when launch tracking remains active, removes them on failure, and prevents pending rows from falsely confirming launch success.

src/Capacitor.App/ViewModels/HomeViewModel.cs

PullRequestContextViewModel.Presentation.csUse shared PR lifecycle presentation +1/-1

Use shared PR lifecycle presentation

• Delegates lifecycle status rendering to the shared tone vocabulary so drafts and conflicts match the rail.

src/Capacitor.App/ViewModels/PullRequestContextViewModel.Presentation.cs

WorkspaceViewModel.csPopulate workspaces while launches are pending +29/-3

Populate workspaces while launches are pending

• Combines daemon presence with directory pending rows so titles, repository labels, and launch stages appear before the first agent DTO.

src/Capacitor.App/ViewModels/WorkspaceViewModel.cs

LineSelection.csSelect logical lines on triple-click +59/-0

Select logical lines on triple-click

• Installs tunnel-route handlers for TextBox and SelectableTextBlock, replacing Avalonia's select-all behavior with pointer-based line selection.

src/Capacitor.App/Views/LineSelection.cs

SessionRailView.axamlStrengthen selected, starting, and PR rail feedback +33/-18

Strengthen selected, starting, and PR rail feedback

• Adds a distinct selected background, accent edge, heavier title, pulsing starting dots, and tone-colored worktree branch glyphs.

src/Capacitor.App/Views/SessionRailView.axaml

Tests (17) +913 / -34
FakeAgentDirectory.csRecord placeholder operations in directory tests +9/-0

Record placeholder operations in directory tests

• Extends the fake directory with placeholder addition and removal tracking.

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

FakeDaemonClientService.csExpose pending launches in the fake daemon client +1/-0

Expose pending launches in the fake daemon client

• Adds the pending-launch cache required by application and rail tests.

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

HomeViewModelTests.csTest accepted-launch placeholder lifecycle +97/-0

Test accepted-launch placeholder lifecycle

• Covers local creation, remote omission, failure cleanup, and the rule that pending rows do not confirm successful launches.

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

LaunchStagesTests.csTest launch-stage display labels +28/-0

Test launch-stage display labels

• Verifies known mappings, unknown-stage humanization, and vendor-aware starting text.

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

LineSelectionTests.csTest application-wide triple-click line selection +123/-0

Test application-wide triple-click line selection

• Covers line-bound calculations, TextBox and SelectableTextBlock integration, and preservation of double-click word selection.

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

MainWindowSmokeTests.csTest selected rail row visual distinction +74/-31

Test selected rail row visual distinction

• Refactors rail window setup and verifies selected rows differ from hovered siblings by background, edge, and title weight.

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

PendingLaunchRowsTests.csTest pending-row reconciliation +89/-0

Test pending-row reconciliation

• Verifies daemon pending rows, placeholder precedence, published-agent replacement, explicit removal, and absence from session mappings.

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

PullRequestStatusTests.csTest pull-request status presentation flags +40/-0

Test pull-request status presentation flags

• Validates muted drafts, pulsing checks, warning conflicts, and existing success, danger, and merged colors.

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

PullRequestToneCacheTests.csTest session-wide PR tone caching +122/-0

Test session-wide PR tone caching

• Covers initial reads, empty links, refresh cadence, row removal, denied access, and strongest-tone aggregation.

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

PullRequestTonesTests.csTest PR tone mapping and precedence +84/-0

Test PR tone mapping and precedence

• Pins lifecycle, check, conflict, and draft mappings plus labels, ordering, and lifecycle status presentation.

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

RailSessionViewModelTests.csTest pending session rail presentation +27/-0

Test pending session rail presentation

• Verifies pending rows show launch stages and open locally while published rows remain non-starting.

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

RailWorktreeViewModelTests.csTest worktree PR tone aggregation +35/-2

Test worktree PR tone aggregation

• Ensures branch glyph state follows the strongest live session tone and exposes appropriate pulse and tooltip values.

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

SessionRailViewModelTests.csTest pending worktree expansion +18/-0

Test pending worktree expansion

• Confirms launch auto-open expands the worktree containing a pending row.

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

WorkspaceViewModelTests.csTest pending workspace feedback +53/-0

Test pending workspace feedback

• Verifies pending rows populate startup headers and stages until agent publication, while unknown agents are not shown as starting.

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

PendingLaunchDtoJsonTests.csTest pending-launch IPC compatibility +32/-0

Test pending-launch IPC compatibility

• Pins snake-case serialization and confirms payloads from older daemons deserialize with a null pending list.

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

StatusIpcJsonTests.csUpdate exact status snapshot JSON +1/-1

Update exact status snapshot JSON

• Extends the pinned wire payload with the explicit nullable pending field.

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

PendingLaunchStatusTests.csTest daemon pending-launch status reporting +80/-0

Test daemon pending-launch status reporting

• Covers projected identity and stages, deterministic ordering, suppression after publication, and notifier pulses during lifecycle changes.

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

Documentation (1) +30 / -0
CHANGES.mdDocument desktop launch, PR tone, and line-selection behavior +30/-0

Document desktop launch, PR tone, and line-selection behavior

• Explains pending-launch reconciliation, worktree pull-request tones, and application-wide triple-click line selection, including compatibility and access semantics.

docs/CHANGES.md

@qodo-code-review

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Pull request tones outlive access ✓ Resolved 🐞 Bug ⛨ Security
Description
PullRequestToneCache.ReadAsync accepts overview data without checking CanReveal, then retains
the resulting tone until a later refresh changes it or the session disappears. When the reader
returns a response whose access lifetime has expired or has fewer than five seconds remaining, the
rail continues exposing its pull-request state outside the existing access window.
Code

src/Capacitor.App/Services/PullRequestToneCache.cs[R79-81]

+                var read = await _source.OverviewAsync(session, PullRequestWire.Subject(link), ct).ConfigureAwait(false);
+                if (read.Kind is PullRequestReadKind.Ready or PullRequestReadKind.Stale && read.Data is not null && read.AccessFailure is null)
+                    tones.Add(PullRequestTones.From(read.Data));
Relevance

●●● Strong

Retaining protected PR state beyond the access window violates the established reader contract and
security expectations.

PR-#856

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The core read contract requires both valid data and at least five seconds of remaining access, and
the interactive PR view applies that contract. The new cache's acceptance predicate omits it and
stores the derived tone in a long-lived dictionary.

src/Capacitor.App/Services/PullRequestToneCache.cs[76-85]
src/Capacitor.App/Services/PullRequestToneCache.cs[92-103]
src/Capacitor.Cli.Core/PullRequests/PullRequestRead.cs[3-8]
src/Capacitor.App/ViewModels/PullRequestContextViewModel.cs[237-246]

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 tone cache derives and retains PR state from reads that the existing protected-content contract says must not be revealed after their access window expires.

## Fix Focus Areas
- src/Capacitor.App/Services/PullRequestToneCache.cs[79-85]
- src/Capacitor.Cli.Core/PullRequests/PullRequestRead.cs[3-8]
- src/Capacitor.App/ViewModels/PullRequestContextViewModel.cs[237-246]

## Recommended Fix
Require `read.CanReveal(_time)` before deriving a tone. Also schedule removal or revalidation when the accepted read's remaining access time expires, so a previously valid tone cannot remain published beyond that deadline.

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


2. Failed launches stay in the rail ✓ Resolved 🐞 Bug ☼ Reliability
Description
AgentDirectory.Recompute removes expired placeholders only while recomputing rows, and
AddPlaceholder does not schedule a timer or delayed callback for the PlaceholderTtl boundary.
When an accepted launch receives no daemon pending update, published row, failure event, registry or
connection change, or other placeholder update, its starting row remains visible indefinitely.
Code

src/Capacitor.App/Services/AgentDirectory.cs[R202-203]

+            var cutoff = DateTime.UtcNow - PlaceholderTtl;
+            foreach (var id in _placeholders.Where(kv => kv.Value.CreatedAt < cutoff).Select(kv => kv.Key).ToList()) _placeholders.Remove(id);
Relevance

●●● Strong

Accepted reliability findings target stale state and missing lifecycle cleanup; recent
timer/disposal feedback was accepted.

PR-#831

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
All placeholder expiry checks occur inside Recompute, while every shown invocation is driven by
cache, source, status, connection, snapshot, or explicit placeholder changes rather than elapsed
time; the class has no time-based trigger to initiate pruning when PlaceholderTtl expires.

src/Capacitor.App/Services/AgentDirectory.cs[77-98]
src/Capacitor.App/Services/AgentDirectory.cs[119-126]
src/Capacitor.App/Services/AgentDirectory.cs[166-203]

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

## Issue description
Placeholder expiry is evaluated only during `AgentDirectory.Recompute`, so merely passing the ten-minute TTL never removes a stale starting row from an otherwise idle directory.

## Fix Focus Areas
- src/Capacitor.App/Services/AgentDirectory.cs[57-59]
- src/Capacitor.App/Services/AgentDirectory.cs[119-126]
- src/Capacitor.App/Services/AgentDirectory.cs[197-210]

## Recommended Fix
Track or schedule the next expiration when a placeholder is added, using a disposable timer, delayed task, or observable callback tied to the directory lifetime. At expiration, remove only the matching placeholder if it is still present and expired, then invoke `Recompute`; cancel, replace, dispose, or reschedule the expiration mechanism whenever placeholders are added, removed, published or otherwise superseded, and when the directory is disposed.

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


3. Local launches can open remote sessions ✓ Resolved 🐞 Bug ≡ Correctness
Description
RowExists now accepts any non-pending row with a matching ID, even though the directory
deliberately retains source-scoped local and remote rows with the same ID as distinct agents. If a
remote agent already has the accepted ID for a new local launch, tracking returns false and
suppresses the local placeholder, then auto-open resolves that ID to the unrelated remote row.
Code

src/Capacitor.App/ViewModels/HomeViewModel.cs[R960-962]

    bool RowExists(string agentId) =>
        _directory is { } directory
-        && directory.Rows.Items.Any(r => NormalizeAgentId(r.Id) == agentId);
+        && directory.Rows.Items.Any(r => r.Origin != AgentOrigin.Pending && NormalizeAgentId(r.Id) == agentId);
Relevance

●●● Strong

The ID-only launch check conflicts with source-scoped rows and can suppress a legitimate local
launch; Home race fixes were accepted.

PR-#766

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The directory explicitly preserves same IDs on both lanes because they can denote different agents.
The new ID-only check suppresses tracking before the new placeholder is added, while unresolved
opens fall back to a remote row when neither a local nor pending row exists.

src/Capacitor.App/Services/AgentRow.cs[7-12]
src/Capacitor.App/Services/AgentDirectory.cs[184-210]
src/Capacitor.App/ViewModels/HomeViewModel.cs[909-915]
src/Capacitor.App/ViewModels/HomeViewModel.cs[919-947]
src/Capacitor.App/App.axaml.cs[632-636]
src/Capacitor.App/ViewModels/MainWindowViewModel.cs[442-475]

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 local launch is considered published when any remote row has the same logical agent ID, despite same-ID rows on different lanes representing different agents.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/HomeViewModel.cs[919-947]
- src/Capacitor.App/ViewModels/HomeViewModel.cs[960-962]
- src/Capacitor.App/App.axaml.cs[634-636]

## Recommended Fix
Make local launch settlement and existence checks consider only a local published row. Keep pending local rows as non-settling, and do not let an unrelated remote row suppress the placeholder or determine the launch auto-open origin.

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



Remediation recommended

4. Denied pull requests keep a tone ✓ Resolved 🐞 Bug ≡ Correctness
Description
PullRequestToneCache.ReadAsync records successful overview tones even when another overview in the
same session sets denied, because Strongest(tones) ignores that flag. When a session links
multiple pull requests and at least one remains readable, a denied read does not clear the worktree
tone as the new cache contract states.
Code

src/Capacitor.App/Services/PullRequestToneCache.cs[R84-85]

+            // A transient miss keeps the last tone; a denial clears it, as the card does.
+            if (tones.Count > 0 || denied || links.Data.Items.Length == 0) Set(session, PullRequestTones.Strongest(tones));
Relevance

●●● Strong

The implementation contradicts its own denial-clears contract; related PR presentation semantics are
actively enforced.

PR-#856

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The loop preserves successful tones alongside the denial flag, and the final call always derives its
value solely from those tones; the adjacent comment explicitly says a denial clears the tone.

src/Capacitor.App/Services/PullRequestToneCache.cs[76-85]
src/Capacitor.App/Services/PullRequestToneCache.cs[92-97]

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 denied overview only clears the tone when no other linked overview produced a tone, contrary to the cache's stated denial semantics.

## Fix Focus Areas
- src/Capacitor.App/Services/PullRequestToneCache.cs[76-85]

## Recommended Fix
Give `denied` precedence over collected tones: call `Set(session, PullRequestTone.None)` when any overview reports denied or invalid access, and aggregate successful tones only when no denial occurred. Add a test with one successful overview and one denied overview.

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


5. Review requests look like running checks ✓ Resolved 🐞 Bug ≡ Correctness
Description
PullRequestStatus.IsPulsing now makes every status with kind pending pulse and IsWarning no
longer assigns that kind the warning colour. ReviewStatus also uses pending for
review_required, so review requests become muted animated labels even though only running checks
are intended to pulse.
Code

src/Capacitor.App/ViewModels/PullRequestStatus.cs[R8-10]

+    /// A running check and a draft share the muted colour; only the check pulses.
+    public bool IsMuted => Kind is "pending" or "draft";
+    public bool IsPulsing => Kind == "pending";
Relevance

●●● Strong

Review-required and check-pending share a kind despite different visual semantics, causing an
obvious user-facing correctness bug.

PR-#790

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The status model itself says only checks should pulse, but both the review summary and check summary
produce the same pending kind, which the status label binds directly to the application-wide pulse
class.

src/Capacitor.App/ViewModels/PullRequestStatus.cs[3-10]
src/Capacitor.App/ViewModels/PullRequestContextViewModel.Presentation.cs[41-44]
src/Capacitor.App/ViewModels/PullRequestContextViewModel.Presentation.cs[45-69]
src/Capacitor.App/Views/PullRequestStatusLabel.axaml[16-22]

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 generic `pending` kind is shared by running checks and required reviews, so enabling pulse behavior for that kind changes both labels.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/PullRequestStatus.cs[3-10]
- src/Capacitor.App/ViewModels/PullRequestContextViewModel.Presentation.cs[41-44]
- src/Capacitor.App/ViewModels/PullRequestContextViewModel.Presentation.cs[45-69]

## Recommended Fix
Represent required review with a distinct warning kind, or model pulse behavior independently from colour and semantic status. Keep check-pending labels muted and pulsing while leaving review-required labels warning-coloured and still, then test both projections.

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


6. Foreign sessions can colour the rail ✓ Resolved 🐞 Bug ≡ Correctness
Description
PullRequestToneCache collects session IDs from every directory row instead of applying the
directory's LocalDaemonOnAppServer ownership rule. When the local daemon is connected to another
server, the cache queries the current server's pull-request source with foreign session IDs, which
can colour a worktree from a colliding session or generate invalid reads.
Code

src/Capacitor.App/Services/PullRequestToneCache.cs[R37-41]

+        directory.Rows.Connect().ToCollection()
+            .Subscribe(rows => {
+                var sessions = rows.Where(r => r.SessionId is { Length: > 0 }).Select(r => r.SessionId!).ToFrozenSet(StringComparer.Ordinal);
+                lock (_lock) _sessions = sessions;
+                Tick();
Relevance

●●● Strong

Ownership-gating bugs are accepted when state from foreign sessions can affect user-visible UI
behavior.

PR-#790

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The directory documents that local session IDs name different sessions while the daemon points at
another server and filters those rows for its server-lane session map. The cache bypasses this
filter and sends every collected ID to its PR source.

src/Capacitor.App/Services/PullRequestToneCache.cs[37-42]
src/Capacitor.App/Services/PullRequestToneCache.cs[58-71]
src/Capacitor.App/Services/AgentDirectory.cs[105-115]
src/Capacitor.App/Services/AgentDirectory.cs[129-142]

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 rail tone cache includes local session IDs that the directory identifies as belonging to another server, unlike the existing server-session consumers.

## Fix Focus Areas
- src/Capacitor.App/Services/PullRequestToneCache.cs[37-42]
- src/Capacitor.App/Services/AgentDirectory.cs[105-142]

## Recommended Fix
Derive the cache session set through an ownership-filtered directory API, or combine directory rows with `LocalDaemonOnAppServer` and exclude local rows whenever the daemon is not connected to the app server. Recompute the tone map when that ownership state changes.

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


View medium (3)
7. Signing out leaves stale rail colours ✓ Resolved 🐞 Bug ≡ Correctness
Description
PullRequestToneCache.ReadAsync returns for every discovery result other than Supported without
removing a previously cached tone. After discovery changes to SignedOut, listed sessions retain
their former pull-request colours across every refresh until their rows disappear.
Code

src/Capacitor.App/Services/PullRequestToneCache.cs[R69-70]

+            var capability = await _source.DiscoverAsync(false, ct).ConfigureAwait(false);
+            if (capability.Kind != PullRequestCapabilityKind.Supported) return;
Relevance

●●● Strong

Stale authenticated presentation after sign-out is a correctness issue; recent PR-state cleanup
feedback was accepted.

PR-#856

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Only Set(session, None) removes a cached tone, but the discovery branch returns before calling it;
the existing workspace reader clears protected pull-request state when discovery reports sign-out.

src/Capacitor.App/Services/PullRequestToneCache.cs[66-74]
src/Capacitor.App/Services/PullRequestToneCache.cs[92-98]
src/Capacitor.App/ViewModels/PullRequestContextViewModel.Reads.cs[29-44]
src/Capacitor.Cli.Core/PullRequests/PullRequestClient.cs[27-40]

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

## Issue description
Capability discovery returning `SignedOut` bypasses `Set`, leaving previously published pull-request tones visible indefinitely.

## Fix Focus Areas
- src/Capacitor.App/Services/PullRequestToneCache.cs[66-74]
- src/Capacitor.App/Services/PullRequestToneCache.cs[92-98]

## Recommended Fix
Handle discovery results by category rather than returning uniformly. Clear the session tone for `SignedOut` and other definitive access-invalid states, while retaining the prior tone only for transient unavailable results; add a refresh test that transitions from supported to signed out.

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


8. Local pull requests never colour rail 🐞 Bug ≡ Correctness
Description
PullRequestToneCache invokes the shared source without supplying the reader registry's per-session
repository and branch context. When server-linked PR reads are unavailable, the registry can only
use its local provider fallback after DescribeSession has populated that context, so those locally
discoverable pull requests remain absent from the rail.
Code

src/Capacitor.App/Services/PullRequestToneCache.cs[R66-81]

+    async Task ReadAsync(string session) {
+        try {
+            var ct = _cancel.Token;
+            var capability = await _source.DiscoverAsync(false, ct).ConfigureAwait(false);
+            if (capability.Kind != PullRequestCapabilityKind.Supported) return;
+            var links = await _source.ListAsync(session, ct).ConfigureAwait(false);
+            if (links.Kind != PullRequestReadKind.Ready || links.Data is null) {
+                if (links.Kind is PullRequestReadKind.SubjectUnavailable or PullRequestReadKind.SignedOut || links.AccessFailure is "invalid" or "denied") Set(session, PullRequestTone.None);
+                return;
+            }
+            var tones = new List<PullRequestTone>();
+            var denied = false;
+            foreach (var link in links.Data.Items) {
+                var read = await _source.OverviewAsync(session, PullRequestWire.Subject(link), ct).ConfigureAwait(false);
+                if (read.Kind is PullRequestReadKind.Ready or PullRequestReadKind.Stale && read.Data is not null && read.AccessFailure is null)
+                    tones.Add(PullRequestTones.From(read.Data));
Relevance

●●● Strong

The cache bypasses established reader context and can omit valid local fallback data; analogous
PR-state findings were accepted.

PR-#856

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The workspace path describes the session before reading, while the registry uses that stored
repository and branch both for fallback after unavailable server links and for live provider
discovery. The new cache performs the reads without any such call.

src/Capacitor.App/Services/PullRequestToneCache.cs[66-81]
src/Capacitor.App/ViewModels/PullRequestContextViewModel.Reads.cs[25-33]
src/Capacitor.Cli.Core/PullRequests/Readers/IPullRequestReaders.cs[3-8]
src/Capacitor.Cli.Core/PullRequests/Readers/PullRequestReaderRegistry.cs[30-64]

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 background PR reader bypasses the session-description step required for local provider discovery and fallback.

## Fix Focus Areas
- src/Capacitor.App/Services/PullRequestToneCache.cs[33-45]
- src/Capacitor.App/Services/PullRequestToneCache.cs[66-81]
- src/Capacitor.App/ViewModels/PullRequestContextViewModel.Reads.cs[25-33]
- src/Capacitor.Cli.Core/PullRequests/Readers/PullRequestReaderRegistry.cs[30-64]

## Recommended Fix
Pass the reader-context interface and enough per-row repository and branch metadata into the cache, then call `DescribeSession` before discovery/listing. Preserve that metadata on the directory row or expose it through a suitable directory/context API so local provider discovery receives the same inputs as the workspace reader.

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


9. Draft pull requests show as open ✓ Resolved 🐞 Bug ≡ Correctness
Description
PullRequestTones.LifecycleStatus checks only the lifecycle string for drafts, while From also
treats IsDraft == true as draft state. For an overview represented as an open lifecycle with its
draft flag set, the rail says “Draft” but the pull-request card says “Open.”
Code

src/Capacitor.App/ViewModels/PullRequestTones.cs[R43-45]

+        "open" when overview.Mergeable == false => new("Merge conflicts", "conflict"),
+        "draft" => new("Draft", "draft"),
+        "open" => new("Open", "open"),
Relevance

●●● Strong

The shared lifecycle mapper omits the existing draft flag, creating contradictory card and rail
presentation.

PR-#856

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The tone mapper explicitly supports draft state through either field, but the newly shared lifecycle
mapper supports only the lifecycle value; the DTO defines these as separate fields.

src/Capacitor.App/ViewModels/PullRequestTones.cs[8-20]
src/Capacitor.App/ViewModels/PullRequestTones.cs[41-49]
src/Capacitor.Cli.Core/PullRequests/PullRequestOverviewDto.cs[11-16]
src/Capacitor.App/ViewModels/PullRequestContextViewModel.Presentation.cs[41-44]

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 lifecycle label ignores `IsDraft`, allowing the card and rail to classify the same overview differently.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/PullRequestTones.cs[8-20]
- src/Capacitor.App/ViewModels/PullRequestTones.cs[41-49]

## Recommended Fix
After applying conflict precedence, classify an open overview with `IsDraft == true` as `new("Draft", "draft")`. Add a lifecycle-status assertion for an open overview whose draft flag is true.

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



Informational

10. Conflicts never reach desktop users ✓ Resolved 🔗 Cross-repo conflict ≡ Correctness
Description
PullRequestTones.From requires Mergeable == false, but the pinned kcap-server neither requests
nor serializes pull-request mergeability. Every server-backed conflicting pull request therefore
deserializes this value as null and falls through to the running, draft, or ready presentation in
both the rail and lifecycle card.
Code

src/Capacitor.App/ViewModels/PullRequestTones.cs[17]

+        if (overview.Mergeable == false) return PullRequestTone.Conflict;
Relevance

● Weak

The PR explicitly documents mergeability as intentionally unavailable until the server adds the
field.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR explicitly branches on the new nullable mergeability field, while the pinned server response
DTO omits it, the GitHub overview query does not request it, and the provider cannot populate it.
This proves server responses leave the client value null even for conflicting pull requests.

src/Capacitor.App/ViewModels/PullRequestTones.cs[15-20]
src/Capacitor.Cli.Core/PullRequests/PullRequestOverviewDto.cs[14-16]
External repo: kurrent-io/kcap-server, src/Capacitor.Api.Public.Abstractions/PullRequests/PullRequestOverviewDto.cs [10-16]
External repo: kurrent-io/kcap-server, src/Capacitor.Server/PullRequests/GitHubPullRequestQueries.cs [16-38]
External repo: kurrent-io/kcap-server, src/Capacitor.Server/PullRequests/GitHubPullRequestProvider.cs [43-57]

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 desktop now depends on `mergeable` to identify conflicts, but the pinned server does not include that field in its overview contract or GitHub query.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/PullRequestTones.cs[15-20]
- src/Capacitor.Cli.Core/PullRequests/PullRequestOverviewDto.cs[14-16]
- /cross_repos/kcap-server/src/Capacitor.Api.Public.Abstractions/PullRequests/PullRequestOverviewDto.cs[10-16]
- /cross_repos/kcap-server/src/Capacitor.Server/PullRequests/GitHubPullRequestQueries.cs[16-38]
- /cross_repos/kcap-server/src/Capacitor.Server/PullRequests/GitHubPullRequestProvider.cs[43-57]

## Recommended Fix
Coordinate a kcap-server change that adds the serialized nullable `mergeable` field, requests GitHub mergeability in the overview query, and maps the result into the response DTO. Deploy that compatible server contract with or before enabling the client conflict presentation.

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


11. Pending launch model lacks its own file ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
PendingLaunchDto is added as another public top-level record in StatusIpc.cs, whose filename
does not match this model. Later readers must locate the pending-launch wire contract inside an
existing bundle of unrelated primary types, making further schema changes harder to navigate safely.
Code

src/Capacitor.Cli.Core/LocalIpc/StatusIpc.cs[R19-20]

+public sealed record PendingLaunchDto(
+    string Id, string Vendor, string? RepoPath, string? Title, DateTime CreatedAt, string? Stage);
Relevance

● Weak

Recent precedents reject one-type-per-file findings for DTO bundles and partial class files in this
repository.

PR-#873
PR-#875
PR-#865

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 3162234 requires one public primary type per file and requires the filename to match
that type. The cited branch region adds the public PendingLaunchDto record to StatusIpc.cs
alongside the existing public status model.

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

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

## Issue description
`PendingLaunchDto` is a public top-level record placed in `StatusIpc.cs`, rather than a file matching its type name.

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

## Recommended Fix
Move `PendingLaunchDto` into `src/Capacitor.Cli.Core/LocalIpc/PendingLaunchDto.cs`, preserving its namespace and public API, and leave `DaemonStatusDto` referencing the moved type.

ⓘ 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/kcap-server (sha: 022ff5bf)
Review mode: 🧠 Deep: This is a broad, behavior-heavy cross-layer change spanning UI interaction, launch lifecycle and IPC contracts, asynchronous caching, and pull-request state mapping, with many independent logic paths where a redundant review could catch subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.App/Services/PullRequestToneCache.cs Outdated
Comment thread src/Capacitor.App/Services/PullRequestToneCache.cs Outdated
Comment thread src/Capacitor.App/ViewModels/PullRequestTones.cs
Comment thread src/Capacitor.App/ViewModels/PullRequestStatus.cs
Comment thread src/Capacitor.App/Services/AgentDirectory.cs Outdated
Comment thread src/Capacitor.App/ViewModels/HomeViewModel.cs Outdated
Comment thread src/Capacitor.App/Services/PullRequestToneCache.cs Outdated
Comment thread src/Capacitor.App/Services/PullRequestToneCache.cs
Comment on lines +66 to +81
async Task ReadAsync(string session) {
try {
var ct = _cancel.Token;
var capability = await _source.DiscoverAsync(false, ct).ConfigureAwait(false);
if (capability.Kind != PullRequestCapabilityKind.Supported) return;
var links = await _source.ListAsync(session, ct).ConfigureAwait(false);
if (links.Kind != PullRequestReadKind.Ready || links.Data is null) {
if (links.Kind is PullRequestReadKind.SubjectUnavailable or PullRequestReadKind.SignedOut || links.AccessFailure is "invalid" or "denied") Set(session, PullRequestTone.None);
return;
}
var tones = new List<PullRequestTone>();
var denied = false;
foreach (var link in links.Data.Items) {
var read = await _source.OverviewAsync(session, PullRequestWire.Subject(link), ct).ConfigureAwait(false);
if (read.Kind is PullRequestReadKind.Ready or PullRequestReadKind.Stale && read.Data is not null && read.AccessFailure is null)
tones.Add(PullRequestTones.From(read.Data));

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

9. Local pull requests never colour rail 🐞 Bug ≡ Correctness

PullRequestToneCache invokes the shared source without supplying the reader registry's per-session
repository and branch context. When server-linked PR reads are unavailable, the registry can only
use its local provider fallback after DescribeSession has populated that context, so those locally
discoverable pull requests remain absent from the rail.
Agent Prompt
## Issue description
The new background PR reader bypasses the session-description step required for local provider discovery and fallback.

## Fix Focus Areas
- src/Capacitor.App/Services/PullRequestToneCache.cs[33-45]
- src/Capacitor.App/Services/PullRequestToneCache.cs[66-81]
- src/Capacitor.App/ViewModels/PullRequestContextViewModel.Reads.cs[25-33]
- src/Capacitor.Cli.Core/PullRequests/Readers/PullRequestReaderRegistry.cs[30-64]

## Recommended Fix
Pass the reader-context interface and enough per-row repository and branch metadata into the cache, then call `DescribeSession` before discovery/listing. Preserve that metadata on the directory row or expose it through a suitable directory/context API so local provider discovery receives the same inputs as the workspace reader.

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

realtonyyoung and others added 6 commits September 15, 2026 08:31
Avalonia's TextBox and SelectableTextBlock select everything on the third
click in their own class handler, so the replacement runs on the tunnel
route and marks the press handled before that handler sees it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWRNeNnUJeSbRrQQFwuSui
Hover already paints the raised surface brush, so a selection that used the
same brush was indistinguishable from the pointer resting on a row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWRNeNnUJeSbRrQQFwuSui
The agent instance only exists once the runtime handshake ends, so the
daemon now reports in-flight launches and their stage over the local status
snapshot, and the app adds its own placeholder the moment the server accepts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWRNeNnUJeSbRrQQFwuSui
The rail reads PR state through a cache over the same per-session links and
overview reads the workspace uses; conflicts stay uncoloured until the server
sends a mergeable field, which the client already accepts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWRNeNnUJeSbRrQQFwuSui
A placeholder expired only when some other change recomputed the rows, and a
same-id row on the other lane could settle a launch it never belonged to. The
tone cache now honours the reader's reveal gate, clears on a denial or a
signed-out discovery, and skips local rows while the daemon reports another server.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWRNeNnUJeSbRrQQFwuSui
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

Qodo findings, addressed in the latest push:

  1. Failed launches stay in the rail — fixed. AgentDirectory now owns a timer on its TimeProvider that fires at the earliest placeholder expiry and recomputes, re-armed on every recompute; a test advances a fake clock past ten minutes with nothing else changing.
  2. Local launches can open remote sessions — fixed. Launch tracking records the lane it launched on; only a published row on that lane settles it, and a same-id remote row neither suppresses the local placeholder nor confirms the launch (test added).
  3. Tones outlive access — the cache now derives a tone only from a read that passes the reader's own CanReveal gate, so an expired or short-lived read yields nothing. It does not schedule removal at the 30-second window: the tone is a single colour per worktree, not the protected content the window guards, and re-reading every 30 s per PR for every listed session would multiply GitHub reads for no user-visible gain. Noted in the PR description as a deliberate call.
  4. Denied PRs keep a tone — fixed; a denial on any linked PR clears the session's tone (test added).
  5. Signing out leaves stale colours — fixed; a definitive discovery result (signed out, legacy, unsupported) clears tones, a transient Unavailable keeps the last one (tests added for both).
  6. Draft PRs show as open — fixed; the lifecycle label honours IsDraft like the tone does (test added).
  7. Review requests look like running checks — fixed; review_required maps to a distinct warning kind that keeps the amber colour and never pulses (test added).
  8. Foreign sessions can colour the rail — fixed; local rows are excluded while the daemon reports another server, using the directory's own LocalDaemonOnAppServer rule (test added).
  9. Local PRs never colour the rail — not addressed. The gh fallback needs the session's PullRequestRepository (provider, host, owner, name, hash), which the workspace gets from the server work-context read; rail rows carry only a local path, so the cache cannot describe sessions and reads server-linked PRs only. Left as a documented limitation.
  10. Rule: one type per file — fixed; PendingLaunchDto moved to its own file.
  11. Cross-repo: conflicts never reach desktop users — known and documented; orange stays dormant until kcap-server serialises mergeable. The client already accepts the field.

@qodo-code-review

Copy link
Copy Markdown

Qodo findings, addressed in the latest push:

  1. Failed launches stay in the rail — fixed. AgentDirectory now owns a timer on its TimeProvider that fires at the earliest placeholder expiry and recomputes, re-armed on every recompute; a test advances a fake clock past ten minutes with nothing else changing.
  2. Local launches can open remote sessions — fixed. Launch tracking records the lane it launched on; only a published row on that lane settles it, and a same-id remote row neither suppresses the local placeholder nor confirms the launch (test added).
  3. Tones outlive access — the cache now derives a tone only from a read that passes the reader's own CanReveal gate, so an expired or short-lived read yields nothing. It does not schedule removal at the 30-second window: the tone is a single colour per worktree, not the protected content the window guards, and re-reading every 30 s per PR for every listed session would multiply GitHub reads for no user-visible gain. Noted in the PR description as a deliberate call.
  4. Denied PRs keep a tone — fixed; a denial on any linked PR clears the session's tone (test added).
  5. Signing out leaves stale colours — fixed; a definitive discovery result (signed out, legacy, unsupported) clears tones, a transient Unavailable keeps the last one (tests added for both).
  6. Draft PRs show as open — fixed; the lifecycle label honours IsDraft like the tone does (test added).
  7. Review requests look like running checks — fixed; review_required maps to a distinct warning kind that keeps the amber colour and never pulses (test added).
  8. Foreign sessions can colour the rail — fixed; local rows are excluded while the daemon reports another server, using the directory's own LocalDaemonOnAppServer rule (test added).
  9. Local PRs never colour the rail — not addressed. The gh fallback needs the session's PullRequestRepository (provider, host, owner, name, hash), which the workspace gets from the server work-context read; rail rows carry only a local path, so the cache cannot describe sessions and reads server-linked PRs only. Left as a documented limitation.
  10. Rule: one type per file — fixed; PendingLaunchDto moved to its own file.
  11. Cross-repo: conflicts never reach desktop users — known and documented; orange stays dormant until kcap-server serialises mergeable. The client already accepts the field.

Thanks for the update. Based on your summary, the latest push addresses findings 1–7, 9, and 11 in the review index, including the lane-aware launch tracking, placeholder expiry, PR access/denial handling, and rail filtering.

The remaining items are:

  • finding 8 — Local pull requests never colour rail: intentionally left as a documented limitation because local rows lack the server-side PullRequestRepository identity required for fallback reads.
  • finding 10 — Conflicts never reach desktop users: known server-side limitation; the client already consumes mergeable when it is provided.

The rationale for retaining tones between refreshes is also clear: CanReveal gates whether a new tone may be derived, while avoiding high-frequency per-PR reads.

realtonyyoung and others added 6 commits September 15, 2026 10:49
…tone

The server accepts a launch as a dashed Guid while the daemon publishes the
N form, and only a local row may retire a local launch's stand-in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWRNeNnUJeSbRrQQFwuSui
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWRNeNnUJeSbRrQQFwuSui
The expiry timer's callback runs on a pool thread, where an unhandled
ObjectDisposedException from the disposed timer would take the process down.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A daemon that dies mid-handshake never sends the snapshot that would retire
its pending entry, so the rail and workspace showed "Starting" for good.

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

Copy link
Copy Markdown
Collaborator Author

NO FINDINGS

@realtonyyoung
realtonyyoung merged commit 2b45f18 into main Sep 15, 2026
13 of 15 checks passed
@realtonyyoung
realtonyyoung deleted the desktop-rail-feedback branch September 15, 2026 20:22
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