Skip to content

Consent prompt window, Activity feed, tray attention (desktop supervisor) - #504

Open
alexeyzimarev wants to merge 26 commits into
mainfrom
alexeyzimarev/ai-1652-consent-prompt-window-activity-feed
Open

Consent prompt window, Activity feed, tray attention (desktop supervisor)#504
alexeyzimarev wants to merge 26 commits into
mainfrom
alexeyzimarev/ai-1652-consent-prompt-window-activity-feed

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

Closes #503. AI-1652.

The consent UX for the desktop supervisor app — the piece that answers the original "daemon launches without asking" complaint on machines running the app.

What's in here

App (src/Capacitor.App)

  • Consent prompt window, auto-raised when a launch parks at the daemon's consent gate: requester (display name), kind, repo, vendor, countdown from the daemon's deadline hint; Allow once / Allow & remember / Deny; "1 of N" queue. Closing without deciding is an explicit defer — the tray keeps Attention and "Review pending launches…" reopens it. Expiry is never fabricated as a verdict: past the hint the buttons stay live and the daemon's ack decides.
  • Activity tab in the main window (now tabbed Agents | Activity): the consent decision log, newest first — outcome badge, requester, kind, repo, vendor, source ("you", "rule", "timeout", …). Reads the daemon's JSONL directly (writer-safe sharing), works with the daemon down, keeps last-good rows on partial reads.
  • Tray: pending consent asserts the Attention state ("N launches awaiting approval") while connected; new menu item between the agents section and pause.
  • ConsentService: status-driven subscription gated on the new consent/2 capability, identity-guarded pending cache, service-lifetime tombstones (ghost-replay defense), prune hygiene, one-resolve-at-a-time lane. The shared 1 Hz ticker is hoisted into an app-lifetime UiTicker.

Wire/daemon hardening (all additive)

  • Daemon-minted prompt_id request identity on pendings, echoed on resolves, atomically claimed by the broker — a stale resolve can never decide a different launch that reused the agent id (the daemon makes no id-non-reuse guarantee).
  • V2 consent frames (ConsentSubscribeV2 = 17, ConsentResolveV2 = 18): a pre-upgrade daemon's codec rejects them before routing, so mixed-version consent fails closed on the wire — no capability-check TOCTOU across daemon restarts. consent/2 is advertised for discovery only.
  • rule_saved on consent acks: the handler deliberately persists an "Allow & remember" rule before resolving (durable trust), so an already-decided outcome now discloses the installed rule instead of hiding it. The button says "Allow & remember" — not "Always allow" — because earlier deny rules (including Pause) shadow the appended allow.
  • requester_display threaded through the consent pipeline (gate input → prompt request → pending DTO → decision record) so prompts and the feed show names, not github:… ids.

Process

Spec survived a 7-round hosted-Codex review (docs/superpowers/specs/2026-08-08-ai1652-consent-prompt-activity-feed-design.md); implemented as 11 SDD tasks with per-task reviews, two task-level fix rounds, and a whole-branch final review whose one must-fix (identity-aware raise signal) landed in the final fix wave. App suite 307/307; consent-focused CLI suites green; AOT publish clean.

🤖 Generated with Claude Code

alexeyzimarev and others added 26 commits August 8, 2026 16:03
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aved/requester_display DTO fields, hoisted decision record
…ache, lifetime tombstones, prune hygiene

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in-flight TOCTOU

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oordinator-owned raise

The window renders ONE pinned request: the sorted queue head, released only on an
advance, so an arrival, a prune or a replay can never swap the display out from under
a click. Three honesty rules carry the design:

* hint expiry is not a verdict — the countdown says so and the buttons stay active, so
  a click after zero either applies (backward clock step) or runs the "Already decided"
  path on the daemon's Ok=false;
* an in-flight resolve outranks the clock — "Expiring…", the ack governs;
* a transport failure discloses nothing about the rule, whose outcome on that path
  describes a save that was never sent.

The advance also skips the identity it just concluded: the service's eviction and the
ack's continuation are two independently posted jobs, so the queue view may not have
caught up yet.

The coordinator owns the single window (open-or-activate, close = defer, reopen
re-creates) and is what filters the service's unconditional entry-added signal by
visibility and marshals it to the UI thread.
…rtup and shutdown

ConsentService is created beside the daemon client over the shared ops/ticker/notifier,
and the prompt coordinator builds a fresh window (and ViewModel) per raise. Teardown
stays reverse-creation order and now runs coordinator before service before daemon
client (spec §5), on both the shutdown and startup-failure paths, so no click can reach
a disposed service; a resolve already in flight was cancelled by the shutdown token and
settles on the ViewModel's silent-abort path.
…der it

On the last pending request — the common single-prompt case — the warning toast was
notified and then discarded: the advance emptied the queue and the window closed on the
same beat, before the posted overlay could render, leaving the disclosure on stderr
only. Exactly what "never a silent success" exists to prevent.

Settle now asks what the advance would land on. With something queued it advances as
before (multi-entry behaviour unchanged, and the toast lands over the next request);
with nothing queued it takes the existing 2-tick terminal hold, showing the warning in
the window as well as over it, and closes after. Applied with no warning still advances
and closes immediately.

The hold now serves two conclusions, so the phase is named Concluded rather than
AlreadyDecided and both entry points share one Hold helper.

Also drops an issue number from a comment (scripts/check-linear-ids.sh).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ing the prompt window

EntryAdded keyed on the cache KEY, so it was wrong in both directions. A successor
B under A's RequestId — a relaunch, the likeliest second prompt there is — replaced
A's slot in silence and never raised a window for the retry. And a resubscribe's
clear+replay made every replayed entry look new, so a window the user had explicitly
closed (a defer) came straight back on any reconnect blip, while an OPEN window saw
the intermediate empty changeset, closed itself, and was rebuilt a moment later with
a fresh ViewModel, a reset pin and stolen focus.

The signal now fires on the FIRST SURFACING of a PromptId: a service-lifetime
`_surfaced` set with the same never-reused-GUID argument the tombstones rest on
(tombstones are a subset of it, kept separate because a tombstone DROPS a frame
while `_surfaced` only keeps it quiet). A successor carries a fresh identity and
raises; a replayed one does not.

The window half is a one-beat close deferral: only a DECISION — an ack, or the end
of its terminal hold — closes on the spot. A queue the cache emptied waits one
ticker beat, and a replay landing inside it disarms the close. The pin still
releases immediately, so nothing that left the cache stays on screen.

Also moves OnStatus's down-level `_cache.Clear()` inside `_lock`, closing the window
where an Upsert that had already passed its tombstone test lands its insert after
the clear and resurrects a previous incarnation's entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sistency items

ActivityViewModel's ticker subscription was constructor-scoped and dropped on the
floor. The shared ticker is Publish().RefCount(), so an undisposed subscriber keeps
its Interval — and the ViewModel — alive past teardown, including the startup-failure
path where the app lingers on an error window. It becomes IDisposable, holds the
subscription, and joins App's two disposal lists in reverse creation order (after the
consent service, before the pause controller).

Also, from the same review pass:

* the DateTimeOffset parse styles now agree — RoundtripKind in both the feed and
  ConsentService, over the same daemon-written ISO stamps;
* LaunchConsentBroker's class doc names all four instance-scoped removal sites, not
  two (the timeout claim and TryResolve's own claim were missing);
* UiTicker's pipeline note says "this ticker", which is what it constructs;
* the §5/§6 refinements this wave discovered are recorded in the spec the way the §6
  disclosure amendment was.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 9, 2026

Copy link
Copy Markdown

AI-1652

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Desktop supervisor consent prompt + activity feed + tray attention (consent/2)

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add auto-raised consent prompt window to approve/deny queued pending launches.
• Add Activity tab that renders the daemon’s consent decision JSONL (works offline).
• Harden consent IPC with consent/2, prompt_id identity echo, and rule_saved disclosure.
Diagram

graph TD
app["Desktop supervisor app"] --> consent["ConsentService"] --> prompt("Consent prompt window")
app --> tray["Tray state/menu"] --> prompt
app --> activity["Activity feed VM"] --> log[("Decision log JSONL")]
consent --> daemon["Daemon consent pipeline"] --> log
subgraph Legend
direction LR
_svc["Service"] ~~~ _ui("UI window") ~~~ _file[("File")]
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Stream decision log over IPC instead of file reads
  • ➕ Single transport surface; avoids file sharing/rotation edge cases
  • ➕ Near-real-time updates without polling/stat keys
  • ➖ New daemon IPC frames/handlers and versioning surface area
  • ➖ Worse offline behavior (daemon down means no Activity)
2. Use FileSystemWatcher instead of stat polling for Activity refresh
  • ➕ No periodic polling; updates on actual writes
  • ➕ Lower steady-state work while Activity visible
  • ➖ Cross-platform watcher semantics are finicky (rename/atomic move during rotation)
  • ➖ More complex to make robust under Windows sharing + file rotation
3. Keep consent/1 frames and rely on capability gating only
  • ➕ Less codec/frame churn; simpler wire change
  • ➖ Reintroduces stale-resolve risk if IDs are reused across daemon restarts
  • ➖ TOCTOU around capability checks vs daemon incarnation changes

Recommendation: The chosen approach (direct JSONL reads for Activity + consent/2 structurally fail-closed frames with prompt_id echo) is the best balance of robustness and additive change. File-based Activity preserves offline usefulness and avoids expanding IPC, while consent/2 eliminates stale resolve hazards and mixed-version ambiguity by rejecting v2 frames at the codec on older daemons.

Files changed (57) +7035 / -306

Enhancement (29) +1622 / -158
App.axaml.csCompose consent services, prompt window coordinator, ticker, and Activity VM +68/-7

Compose consent services, prompt window coordinator, ticker, and Activity VM

• Wires a shared UiTicker, ConsentService subscription loop, ConsentPromptCoordinator/window factory, and ActivityViewModel into app startup and shutdown ordering; adds Activity stat-key helper.

src/Capacitor.App/App.axaml.cs

ConsentPromptCoordinator.csAdd single-instance prompt window coordinator with raise/defer semantics +59/-0

Add single-instance prompt window coordinator with raise/defer semantics

• Introduces a UI-thread marshaled coordinator that auto-raises the prompt window on first-surfaced pending entries, avoids focus-stealing while visible, and supports manual reopening from the tray.

src/Capacitor.App/Services/ConsentPromptCoordinator.cs

ConsentService.csAdd consent subscription loop + identity-guarded pending cache and resolve lane +293/-0

Add consent subscription loop + identity-guarded pending cache and resolve lane

• Implements consent/2 capability-gated subscription, a SourceCache-backed pending queue keyed by RequestId with PromptId-based tombstones/surfacing guards, periodic prune hygiene, and ResolveAsync that always echoes prompt_id and handles rule_saved outcomes.

src/Capacitor.App/Services/ConsentService.cs

IConsentService.csDefine consent service contracts and pending consent model +62/-0

Define consent service contracts and pending consent model

• Adds PendingConsent, resolve outcome enums/record, and observable streams (pending changeset, pending count, first-surfaced EntryAdded) used by the prompt window and tray.

src/Capacitor.App/Services/IConsentService.cs

UiTicker.csHoist shared UI-thread 1Hz ticker into app-lifetime service +39/-0

Hoist shared UI-thread 1Hz ticker into app-lifetime service

• Adds ITicker/UiTicker providing a Publish().RefCount() hot interval on the UI scheduler to drive countdowns, Activity polling, and other 1Hz UI updates safely.

src/Capacitor.App/Services/UiTicker.cs

ActivityViewModel.csAdd Activity feed view model backed by decision log reads +135/-0

Add Activity feed view model backed by decision log reads

• Implements row projection and refresh logic for the consent decision log using injected read/stat functions and a shared ticker; preserves last-good rows on partial/failed reads.

src/Capacitor.App/ViewModels/ActivityViewModel.cs

ConsentPromptViewModel.csAdd queued consent prompt VM with countdown, pinning, and resolve flows +425/-0

Add queued consent prompt VM with countdown, pinning, and resolve flows

• Implements a single-pinned queued prompt UX with phases (Ready/Resolving/Expired/Concluded), 1-of-N indicator, requester/kind/repo/vendor projections, and resolve commands that never treat hint expiry as verdict.

src/Capacitor.App/ViewModels/ConsentPromptViewModel.cs

MainWindowViewModel.csInject shared ticker and Activity VM; remove per-window ticker +9/-34

Inject shared ticker and Activity VM; remove per-window ticker

• Updates MainWindowViewModel to consume the app-lifetime ticker and expose the injected ActivityViewModel for the new Activity tab.

src/Capacitor.App/ViewModels/MainWindowViewModel.cs

TrayModels.csExtend tray menu model with pending consent count +1/-1

Extend tray menu model with pending consent count

• Adds PendingConsent to TrayMenuModel so the menu builder can show a review action and the tray can assert Attention when approvals are pending.

src/Capacitor.App/ViewModels/TrayModels.cs

TrayViewModel.csAdd tray Attention for pending consent and review command +51/-28

Add tray Attention for pending consent and review command

• Combines daemon status, agent snapshots, pause state, stops-in-flight, and consent.PendingCount to compute tray state/header, and adds a 'Review pending launches…' command entry.

src/Capacitor.App/ViewModels/TrayViewModel.cs

ConsentPromptWindow.axamlAdd consent prompt window UI layout and buttons +50/-0

Add consent prompt window UI layout and buttons

• Defines the topmost, non-resizable consent prompt window with requester/kind/repo display, countdown/phase text, and Allow once / Allow & remember / Deny actions.

src/Capacitor.App/Views/ConsentPromptWindow.axaml

ConsentPromptWindow.axaml.csWire prompt window to notifier toasts and close-on-empty behavior +54/-0

Wire prompt window to notifier toasts and close-on-empty behavior

• Adds toast notifications via WindowNotificationManager and subscribes to the ViewModel CloseRequested signal so the window closes itself only when the queue empties.

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

Converters.csAdd outcome brush converter for Activity feed badges +15/-0

Add outcome brush converter for Activity feed badges

• Introduces OutcomeBrushConverter mapping allowed/denied to green/red brushes while keeping Activity rows free of UI-thread-affined types.

src/Capacitor.App/Views/Converters.cs

MainWindow.axamlAdd Agents | Activity tabbed main window layout +102/-50

Add Agents | Activity tabbed main window layout

• Wraps the existing Agents UI in a TabControl and adds an Activity tab with header + rows bound to ActivityViewModel, preserving named controls used by existing tests.

src/Capacitor.App/Views/MainWindow.axaml

MainWindow.axaml.csDrive Activity refresh only when tab selected and window visible +31/-0

Drive Activity refresh only when tab selected and window visible

• Adds tab selection and window visibility tracking to call Activity.OnTabVisibleChanged only when the Activity tab is actually on-screen (including hide-to-tray behavior).

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

TrayMenuBuilder.csAdd tray menu item to review pending launches +5/-0

Add tray menu item to review pending launches

• Inserts a conditional 'Review pending launches…' menu item between agent entries and pause when PendingConsent > 0.

src/Capacitor.App/Views/TrayMenuBuilder.cs

ConsentDecisionLog.csHoist consent decision log record type and add safe tail reader +75/-0

Hoist consent decision log record type and add safe tail reader

• Introduces ConsentDecisionRecord + source-gen context and a reader that safely reads both rotated files with writer-safe sharing and rotation deduplication.

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

ConsentIpc.csExtend consent DTOs with requester_display, prompt_id, and rule_saved +6/-3

Extend consent DTOs with requester_display, prompt_id, and rule_saved

• Adds trailing fields to ConsentPendingDto/ConsentResolveDto and adds RuleSaved to ConsentAckDto to support identity-checked resolves and durable rule disclosure.

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

ConsentSubscription.csAdd consent/2 subscription client yielding typed stream events +74/-0

Add consent/2 subscription client yielding typed stream events

• Implements an async iterator that dials the daemon socket, writes ConsentSubscribeV2, yields a client-local Subscribed marker, and streams structurally valid ConsentPending frames.

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

FrameCodec.csTeach codec about consent v2 frame types +2/-0

Teach codec about consent v2 frame types

• Adds ConsentSubscribeV2/ConsentResolveV2 to the codec’s text payload allowlist for round-trip read/write.

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

FrameType.csAdd append-only consent v2 frame type values 17/18 +4/-0

Add append-only consent v2 frame type values 17/18

• Defines ConsentSubscribeV2 = 17 and ConsentResolveV2 = 18 with a documented fail-closed contract on v1 daemons.

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

LocalControlOps.csAdd ResolveConsentAsync using v2 resolve frame +18/-0

Add ResolveConsentAsync using v2 resolve frame

• Adds a typed operation that always uses ConsentResolveV2 (not v1) to ensure prompt_id echo enforcement and downlevel fail-closed behavior.

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

AgentOrchestrator.csThread requester_display into consent inputs +1/-1

Thread requester_display into consent inputs

• Passes requester display name into LaunchConsentInput so downstream prompts and decision records can show friendly names.

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

LaunchConsentDecisionLog.csSwitch decision log serialization to Core ConsentDecisionRecord +4/-13

Switch decision log serialization to Core ConsentDecisionRecord

• Reuses the shared Core record + JSON context for daemon decision log writes, keeping on-disk field names stable and adding requester_display.

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

LaunchConsentEngine.csExtend consent input with requester display name +2/-1

Extend consent input with requester display name

• Adds RequesterDisplay to the consent input struct so the display string can flow from launch commands into prompts and logs.

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

LaunchConsentGate.csMint prompt_id per prompt and record requester_display in decisions +7/-4

Mint prompt_id per prompt and record requester_display in decisions

• Extends prompt requests with daemon-minted GUID prompt_id and requester_display, and writes decision records using the shared Core log type.

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

LaunchConsentIpc.csEnforce prompt_id echo on v2 resolves and add rule_saved in acks +19/-11

Enforce prompt_id echo on v2 resolves and add rule_saved in acks

• Adds a requireEcho mode for v2 resolves, persists save_rule before attempting resolution and returns RuleSaved on both Ok branches, and stamps pending DTOs with requester_display and prompt_id.

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

LocalControlCapabilities.csAdvertise consent/2 capability +8/-4

Advertise consent/2 capability

• Adds consent/2 to the daemon capability list for discovery while relying on v2 frames to enforce fail-closed semantics.

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

LocalControlServer.csRoute v2 consent frames to LaunchConsentIpc +3/-1

Route v2 consent frames to LaunchConsentIpc

• Adds routing for ConsentSubscribeV2 and ConsentResolveV2, using requireEcho=true for v2 resolves.

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

Bug fix (1) +16 / -3
LaunchConsentBroker.csAdd atomic prompt_id-echo claim for resolves +16/-3

Add atomic prompt_id-echo claim for resolves

• Extends TryResolve to optionally require a prompt_id echo and uses KeyValuePair-conditional removal to prevent ABA/stale-resolve hazards under RequestId reuse.

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

Tests (24) +3313 / -145
ActivityViewModelTests.csAdd unit tests for ActivityViewModel mapping and refresh semantics +288/-0

Add unit tests for ActivityViewModel mapping and refresh semantics

• Covers row projection, visibility-triggered refresh, stat polling behavior, and robustness to read/stat exceptions and partial reads.

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

AgentGridTests.csUpdate agent grid tests for tabbed main window layout +15/-57

Update agent grid tests for tabbed main window layout

• Adjusts existing tests to accommodate the new TabControl structure while preserving key named controls.

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

AppStartupTests.csUpdate startup tests for new composition services +15/-8

Update startup tests for new composition services

• Aligns startup assertions with the new ticker/consent/activity service wiring and disposal ordering.

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

ConsentPromptCoordinatorTests.csAdd tests for prompt coordinator raise/visibility rules +339/-0

Add tests for prompt coordinator raise/visibility rules

• Verifies single-instance window behavior, no reactivation while visible, and tray-driven re-open semantics via the coordinator.

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

ConsentPromptViewModelTests.csAdd full prompt window ViewModel behavior matrix tests +645/-0

Add full prompt window ViewModel behavior matrix tests

• Adds headless Avalonia tests for queue ordering/pinning, countdown/expiry honesty, resolve outcomes, terminal hold timing, and requester/kind/repo projection rules.

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

ConsentServiceTests.csAdd unit tests for ConsentService subscription, cache guards, and resolves +580/-0

Add unit tests for ConsentService subscription, cache guards, and resolves

• Tests capability gating, subscribed-boundary cache clearing, EntryAdded semantics, tombstones/ABA defenses, prune behavior, and resolve outcomes (including rule_saved and transport failures).

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

FakeConsentService.csAdd fake consent service for higher-level UI/tray tests +107/-0

Add fake consent service for higher-level UI/tray tests

• Provides a controllable IConsentService test double emitting pending changes and counts to drive coordinator/tray behaviors.

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

FakeTicker.csAdd fake ticker for deterministic tick-driven ViewModel tests +22/-0

Add fake ticker for deterministic tick-driven ViewModel tests

• Adds a Subject-backed ITicker implementation used to advance countdowns and polling without real time delays.

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

MainWindowSmokeTests.csUpdate main window smoke tests for Activity tab addition +60/-10

Update main window smoke tests for Activity tab addition

• Adjusts smoke tests to account for the tabbed layout and new Activity-bound controls.

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

MainWindowViewModelTests.csUpdate MainWindowViewModel tests for injected ticker and Activity VM +8/-8

Update MainWindowViewModel tests for injected ticker and Activity VM

• Updates construction and expectations after removing the internal ticker and adding ActivityViewModel as a dependency.

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

ScriptedLocalControlOps.csExtend scripted local control ops to support consent resolves +24/-1

Extend scripted local control ops to support consent resolves

• Adds hooks for ResolveConsentAsync to drive ConsentService/ConsentPromptViewModel tests across success/failure outcomes.

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

TrayAdapterTests.csUpdate tray adapter tests for new menu model fields +72/-10

Update tray adapter tests for new menu model fields

• Adjusts assertions to account for PendingConsent and the added review menu item behavior.

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

TrayViewModelTests.csAdd/adjust tray tests for consent Attention and review action +150/-37

Add/adjust tray tests for consent Attention and review action

• Covers Attention state assertion when pending approvals exist and correct header/menu entries across connection states.

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

UiTickerTests.csAdd tests for shared UI ticker behavior +54/-0

Add tests for shared UI ticker behavior

• Verifies UiTicker produces ticks correctly under the expected scheduler model and remains shareable across subscribers.

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

ConsentDecisionLogReaderTests.csAdd tests for decision log tail reader and sharing/rotation behavior +181/-0

Add tests for decision log tail reader and sharing/rotation behavior

• Covers reading both rotated files, deduplication, handling absent/unreadable files, and structural validation of parsed records.

test/Capacitor.Cli.Tests.Unit/ConsentDecisionLogReaderTests.cs

ConsentSubscriptionTests.csAdd tests for consent subscription stream events and validation +259/-0

Add tests for consent subscription stream events and validation

• Validates Subscribed boundary semantics, EOF/transport failure handling, and structural validity filtering for pending frames.

test/Capacitor.Cli.Tests.Unit/ConsentSubscriptionTests.cs

ConsentWireContractsTests.csPin consent/2 wire shapes and frame values +74/-0

Pin consent/2 wire shapes and frame values

• Adds tests ensuring DTO serialization names/ordering, null-writing conventions, backward compatibility with v1 payloads, and frame type byte values 17/18.

test/Capacitor.Cli.Tests.Unit/ConsentWireContractsTests.cs

LaunchConsentBrokerTests.csUpdate broker tests for prompt_id echo claim behavior +45/-2

Update broker tests for prompt_id echo claim behavior

• Extends daemon broker tests to validate atomic claim semantics with prompt_id and the legacy null-echo path.

test/Capacitor.Cli.Tests.Unit/Daemon/LaunchConsentBrokerTests.cs

LaunchConsentDecisionLogTests.csUpdate decision log tests for shared Core record type +4/-2

Update decision log tests for shared Core record type

• Adjusts tests to reflect serialization through ConsentDecisionRecord and the appended requester_display field.

test/Capacitor.Cli.Tests.Unit/Daemon/LaunchConsentDecisionLogTests.cs

LaunchConsentEngineTests.csUpdate consent engine tests for extended input shape +13/-2

Update consent engine tests for extended input shape

• Updates test setup to include RequesterDisplay and ensures verdict behavior remains unchanged.

test/Capacitor.Cli.Tests.Unit/Daemon/LaunchConsentEngineTests.cs

LaunchConsentGateTests.csUpdate gate tests for prompt_id minting and requester_display threading +47/-2

Update gate tests for prompt_id minting and requester_display threading

• Extends tests to validate prompt_id presence and display name propagation into pending DTOs and decision records.

test/Capacitor.Cli.Tests.Unit/Daemon/LaunchConsentGateTests.cs

LaunchConsentIpcTests.csUpdate IPC tests for v2 resolve echo requirement and rule_saved acks +166/-3

Update IPC tests for v2 resolve echo requirement and rule_saved acks

• Validates v2 handler rejects missing prompt_id, reports rule_saved on both Ok branches, and stamps pending DTOs appropriately.

test/Capacitor.Cli.Tests.Unit/Daemon/LaunchConsentIpcTests.cs

LocalControlHelloTests.csUpdate capability tests to include consent/2 +3/-3

Update capability tests to include consent/2

• Adjusts hello/capability expectations for the new advertised consent/2 capability.

test/Capacitor.Cli.Tests.Unit/Daemon/LocalControlHelloTests.cs

LocalControlOpsTests.csUpdate LocalControlOps tests for ResolveConsentAsync and v2 frames +142/-0

Update LocalControlOps tests for ResolveConsentAsync and v2 frames

• Extends tests to cover the new ResolveConsentAsync op using ConsentResolveV2 and correct ack/error handling.

test/Capacitor.Cli.Tests.Unit/LocalControlOpsTests.cs

Documentation (2) +2083 / -0
2026-08-08-ai1652-consent-prompt-activity-feed.mdAdd detailed implementation plan for consent prompt + activity feed +1310/-0

Add detailed implementation plan for consent prompt + activity feed

• Introduces a task-by-task implementation plan, constraints, and file structure for AI-1652, including wire contracts, UI behavior, and testing guidance.

docs/superpowers/plans/2026-08-08-ai1652-consent-prompt-activity-feed.md

2026-08-08-ai1652-consent-prompt-activity-feed-design.mdAdd design spec for consent prompt window, Activity tab, and consent/2 hardening +773/-0

Add design spec for consent prompt window, Activity tab, and consent/2 hardening

• Adds the reviewed design specification covering UX, subscription semantics, prompt queueing, decision log reading, and the additive consent/2 wire changes.

docs/superpowers/specs/2026-08-08-ai1652-consent-prompt-activity-feed-design.md

Other (1) +1 / -0
Capacitor.App.Tests.Unit.csprojAdd TimeProvider testing dependency for deterministic time-based tests +1/-0

Add TimeProvider testing dependency for deterministic time-based tests

• Adds Microsoft.Extensions.TimeProvider.Testing to support FakeTimeProvider-based unit tests without sleeping.

test/Capacitor.App.Tests.Unit/Capacitor.App.Tests.Unit.csproj

@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: 94da3db84e

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

internal static string ActivityStatKey(string daemonName) {
try {
var path = ConsentDecisionLogReader.PathFor(daemonName);
return $"{StatOf(path + ".1")}|{StatOf(path)}";

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 Handle a missing rotated log independently

On a normal installation, consent-decisions.jsonl.1 does not exist until the live log first exceeds the rotation threshold. StatOf(path + ".1") therefore throws and collapses the entire pair to the constant key "absent", so creating or appending to the current log never looks like a stat change. This can leave the Activity tab empty or stale indefinitely—especially when the ack-triggered refresh runs before the daemon appends its record—until the user hides and reopens the tab or the first 1 MB rotation occurs. Represent each missing file independently so changes to the live file still alter the polling key.

Useful? React with 👍 / 👎.

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Activity feed never refreshes 🐞 Bug ≡ Correctness
Description
App.ActivityStatKey returns "absent" when the rotated log file (.1) is missing, so
ActivityViewModel’s stat-key poll never detects changes while only consent-decisions.jsonl exists.
This leaves the Activity tab stale after new decisions are appended unless the tab is reselected or
an explicit refresh is triggered.
Code

src/Capacitor.App/App.axaml.cs[R254-257]

+    internal static string ActivityStatKey(string daemonName) {
+        try {
+            var path = ConsentDecisionLogReader.PathFor(daemonName);
+            return $"{StatOf(path + ".1")}|{StatOf(path)}";
Evidence
ActivityStatKey calls StatOf(path + ".1") inside a broad try/catch; StatOf uses
FileInfo.Length, which throws when the file doesn’t exist, causing a constant "absent" key.
ActivityViewModel compares the key and skips refresh when unchanged, and the daemon only creates
.1 on rotation so it is usually missing.

src/Capacitor.App/App.axaml.cs[248-263]
src/Capacitor.App/ViewModels/ActivityViewModel.cs[61-91]
src/Capacitor.Cli.Daemon/Services/LaunchConsentDecisionLog.cs[29-33]

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

### Issue description
`ActivityStatKey()` computes a combined key for two files (`consent-decisions.jsonl.1` and `consent-decisions.jsonl`). If `.1` is missing, `new FileInfo(path).Length` throws, the broad `catch` returns the constant string `"absent"`, and the key never changes even when the main log grows. `ActivityViewModel` uses this key to suppress refreshes, so updates are not detected.

### Issue Context
- `.1` is only created after rotation; it is commonly absent.
- The stat key should change whenever either file changes, and should not collapse to a constant value when only one file exists.

### Fix Focus Areas
- src/Capacitor.App/App.axaml.cs[248-264]

### Suggested fix
- Replace `StatOf()` with a missing-file tolerant implementation:
 - Option A: `FileInfo fi = new(path); if (!fi.Exists) return "missing"; return $"{fi.LastWriteTimeUtc.Ticks}:{fi.Length}";`
 - Option B: catch `FileNotFoundException/DirectoryNotFoundException` inside `StatOf` and return a stable sentinel like `"0:0"`.
- Avoid wrapping the entire combined computation in a single catch that masks changes in the existing file.
- Add/adjust a unit test to cover the common case: only the main log exists and changes -> stat key changes.

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



Remediation recommended

2. ConsentService comment block too long 📘 Rule violation ⚙ Maintainability
Description
New multi-paragraph doc/comments were added that restate design rationale in-line, which reduces
readability and increases maintenance overhead. The compliance checklist requires keeping comments
minimal and favoring self-explanatory code.
Code

src/Capacitor.App/Services/ConsentService.cs[R14-17]

+/// Four guards carry the reviewed reasoning:
+///
+/// * <b>EntryAdded is the FIRST SURFACING of a PromptId, never a new cache key</b> — the signal
+///   is the raise trigger (spec §6), so it has to mean "a request the user has not been offered
Evidence
PR Compliance ID 7 requires minimal comments. The added doc blocks in ConsentService and
ActivityViewModel are long, narrative explanations (multiple paragraphs/bullets), which meet the
checklist’s failure criteria for verbose/excessive comments.

CLAUDE.md: Keep comments minimal; favor self-explanatory code
src/Capacitor.App/Services/ConsentService.cs[10-40]
src/Capacitor.App/ViewModels/ActivityViewModel.cs[17-32]

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

## Issue description
Verbose multi-paragraph comments were introduced (especially in `ConsentService` and `ActivityViewModel`) where clearer naming/structure (and shorter, targeted comments) would better convey intent.

## Issue Context
PR Compliance requires comments to be brief and necessary, avoiding long narrative blocks.

## Fix Focus Areas
- src/Capacitor.App/Services/ConsentService.cs[10-40]
- src/Capacitor.App/ViewModels/ActivityViewModel.cs[17-32]

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


3. UI-thread log reads 🐞 Bug ➹ Performance
Description
ActivityViewModel calls the injected log reader synchronously from UiTicker ticks, which run on the
UI thread. Reading and parsing the JSONL files on the UI thread can cause visible UI stalls when the
log is large or the disk is slow.
Code

src/Capacitor.App/ViewModels/ActivityViewModel.cs[R93-97]

+    void SafeRefresh() {
+        ConsentLogReadResult result;
+        try { result = _read(); } catch { return; } // swallowed — last-good rows stay on display
+        Apply(result);
+    }
Evidence
UiTicker explicitly delivers ticks on the UI thread, ActivityViewModel subscribes to it and calls
_read() synchronously in SafeRefresh(). The reader implementation reads every line from both
files before filtering to the tail, so this work can be substantial and will run on the UI thread as
written.

src/Capacitor.App/Services/UiTicker.cs[6-12]
src/Capacitor.App/ViewModels/ActivityViewModel.cs[25-27]
src/Capacitor.App/ViewModels/ActivityViewModel.cs[51-57]
src/Capacitor.App/ViewModels/ActivityViewModel.cs[93-97]
src/Capacitor.Cli.Core/LocalIpc/ConsentDecisionLog.cs[29-44]
src/Capacitor.Cli.Core/LocalIpc/ConsentDecisionLog.cs[51-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
`ActivityViewModel.SafeRefresh()` performs `_read()` (file open/read + JSON parse) inline on the UI thread because it is invoked from `UiTicker.Ticks` which is scheduled on `RxSchedulers.MainThreadScheduler`. This can block rendering/input during refresh.

### Issue Context
- `ConsentDecisionLogReader.ReadTail()` reads all lines from both log files into memory before taking the tail.
- Even if the file is capped, synchronous disk IO + JSON parsing on the UI thread is avoidable.

### Fix Focus Areas
- src/Capacitor.App/ViewModels/ActivityViewModel.cs[51-97]
- src/Capacitor.Cli.Core/LocalIpc/ConsentDecisionLog.cs[29-57]
- src/Capacitor.App/Services/UiTicker.cs[6-38]

### Suggested fix
- Run `_read()` on a background scheduler/task and marshal only `Apply(result)` to the UI thread.
 - Example approach: `Observable.Start(_read, RxSchedulers.TaskpoolScheduler)` (or `Task.Run`) then `.ObserveOn(RxSchedulers.MainThreadScheduler)`.
- Keep the existing “swallow errors / keep last-good rows” semantics.
- Consider debouncing/coalescing refresh triggers so multiple triggers in quick succession don’t queue multiple reads.

ⓘ 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 ask Qodo to dismiss a finding you disagree with, with your reason on record

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +14 to +17
/// Four guards carry the reviewed reasoning:
///
/// * <b>EntryAdded is the FIRST SURFACING of a PromptId, never a new cache key</b> — the signal
/// is the raise trigger (spec §6), so it has to mean "a request the user has not been offered

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. consentservice comment block too long 📘 Rule violation ⚙ Maintainability

New multi-paragraph doc/comments were added that restate design rationale in-line, which reduces
readability and increases maintenance overhead. The compliance checklist requires keeping comments
minimal and favoring self-explanatory code.
Agent Prompt
## Issue description
Verbose multi-paragraph comments were introduced (especially in `ConsentService` and `ActivityViewModel`) where clearer naming/structure (and shorter, targeted comments) would better convey intent.

## Issue Context
PR Compliance requires comments to be brief and necessary, avoiding long narrative blocks.

## Fix Focus Areas
- src/Capacitor.App/Services/ConsentService.cs[10-40]
- src/Capacitor.App/ViewModels/ActivityViewModel.cs[17-32]

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

Comment on lines +254 to +257
internal static string ActivityStatKey(string daemonName) {
try {
var path = ConsentDecisionLogReader.PathFor(daemonName);
return $"{StatOf(path + ".1")}|{StatOf(path)}";

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. Activity feed never refreshes 🐞 Bug ≡ Correctness

App.ActivityStatKey returns "absent" when the rotated log file (.1) is missing, so
ActivityViewModel’s stat-key poll never detects changes while only consent-decisions.jsonl exists.
This leaves the Activity tab stale after new decisions are appended unless the tab is reselected or
an explicit refresh is triggered.
Agent Prompt
### Issue description
`ActivityStatKey()` computes a combined key for two files (`consent-decisions.jsonl.1` and `consent-decisions.jsonl`). If `.1` is missing, `new FileInfo(path).Length` throws, the broad `catch` returns the constant string `"absent"`, and the key never changes even when the main log grows. `ActivityViewModel` uses this key to suppress refreshes, so updates are not detected.

### Issue Context
- `.1` is only created after rotation; it is commonly absent.
- The stat key should change whenever either file changes, and should not collapse to a constant value when only one file exists.

### Fix Focus Areas
- src/Capacitor.App/App.axaml.cs[248-264]

### Suggested fix
- Replace `StatOf()` with a missing-file tolerant implementation:
  - Option A: `FileInfo fi = new(path); if (!fi.Exists) return "missing"; return $"{fi.LastWriteTimeUtc.Ticks}:{fi.Length}";`
  - Option B: catch `FileNotFoundException/DirectoryNotFoundException` inside `StatOf` and return a stable sentinel like `"0:0"`.
- Avoid wrapping the entire combined computation in a single catch that masks changes in the existing file.
- Add/adjust a unit test to cover the common case: only the main log exists and changes -> stat key changes.

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

Comment on lines +93 to +97
void SafeRefresh() {
ConsentLogReadResult result;
try { result = _read(); } catch { return; } // swallowed — last-good rows stay on display
Apply(result);
}

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. Ui-thread log reads 🐞 Bug ➹ Performance

ActivityViewModel calls the injected log reader synchronously from UiTicker ticks, which run on the
UI thread. Reading and parsing the JSONL files on the UI thread can cause visible UI stalls when the
log is large or the disk is slow.
Agent Prompt
### Issue description
`ActivityViewModel.SafeRefresh()` performs `_read()` (file open/read + JSON parse) inline on the UI thread because it is invoked from `UiTicker.Ticks` which is scheduled on `RxSchedulers.MainThreadScheduler`. This can block rendering/input during refresh.

### Issue Context
- `ConsentDecisionLogReader.ReadTail()` reads all lines from both log files into memory before taking the tail.
- Even if the file is capped, synchronous disk IO + JSON parsing on the UI thread is avoidable.

### Fix Focus Areas
- src/Capacitor.App/ViewModels/ActivityViewModel.cs[51-97]
- src/Capacitor.Cli.Core/LocalIpc/ConsentDecisionLog.cs[29-57]
- src/Capacitor.App/Services/UiTicker.cs[6-38]

### Suggested fix
- Run `_read()` on a background scheduler/task and marshal only `Apply(result)` to the UI thread.
  - Example approach: `Observable.Start(_read, RxSchedulers.TaskpoolScheduler)` (or `Task.Run`) then `.ObserveOn(RxSchedulers.MainThreadScheduler)`.
- Keep the existing “swallow errors / keep last-good rows” semantics.
- Consider debouncing/coalescing refresh triggers so multiple triggers in quick succession don’t queue multiple reads.

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

Desktop app: consent prompt window, Activity feed, tray attention

1 participant