Skip to content

Tray presence, agents list, per-agent stop, pause launches (desktop supervisor slice 2) - #474

Merged
alexeyzimarev merged 29 commits into
mainfrom
alexeyzimarev/ai-1651-tray-presence-agents-list-stop-desktop-supervisor-slice-2
Aug 7, 2026
Merged

alexeyzimarev merged 29 commits into
mainfrom
alexeyzimarev/ai-1651-tray-presence-agents-list-stop-desktop-supervisor-slice-2

Conversation

@alexeyzimarev

@alexeyzimarev alexeyzimarev commented Aug 6, 2026 •

Copy link
Copy Markdown
Member

AI-1651 (parent AI-1622). Spec: docs/superpowers/specs/2026-08-06-ai1651-tray-agents-stop-design.md (Codex spec-review flow, clean after 7 rounds; amended during live acceptance). Plan: docs/superpowers/plans/2026-08-06-ai1651-tray-agents-stop.md.

What this adds

  • Menu-bar tray icon — the product mark with a state overlay (colored dot: idle/connecting/attention/stopped; count badge while agents run) and a native menu: live agent entries with per-agent Stop and Open in web, a Pause new launches toggle, Open window, Quit. Rebuilds only in NeedsUpdate (macOS never raises Opening for status-item menus — found in manual acceptance); the pause item's desired value is frozen into CommandParameter at rebuild time because the native click path never mutates IsChecked.
  • Pause = wildcard deny rule at rules[0] (not a default flip): the rule is the state — correct restore by construction, survives app restarts, kcap daemon consent show tells the truth. Owner-originated launches are exempt (consent-engine invariant). All consent ops share one single-flight lane with a one-slot desired-state queue; unverifiable state disables the toggle instead of guessing. An edge-triggered refresh on connect makes the toggle correct on first open.
  • Main-window Agents grid: kind, vendor (model), repo-name labels (full path in tooltip), requester, verbatim status, ticking uptime, per-row Stop / Open in web; ellipsis-trimmed cells; rows persist across disconnects with actions disabled. Compact status area (semver-only version with build-metadata tooltip, status dot, contextual Start/Retry).
  • Stop semantics: graceful for plain agents; a review/flow participant gets a confirmation dialog ("stopping it will strand its flow — stop anyway?") that sends force:true on confirm. Failures surface as toast notifications (WindowNotificationManager, top-right, no layout shift); raw daemon error text (agent GUIDs, CLI flags) never reaches the UI — full detail goes to stderr.
  • Hide-to-tray lifecycle: window close hides, quit is explicit through the existing deferred-dispose shutdown machine; OnExplicitShutdown steady-state; tray icon disposed before shutdown confirm. Executable named "Kurrent Capacitor" for a friendly dev-run dock label.
  • Core: LocalControlOps one-shot socket ops (StopV2→StopAck with strict line validation and an empty-id stop-ALL guard, ConsentRulesGet/Put with structural DTO validation), total failure taxonomy with caller-cancellation checked before classification.

No CLI surface changes → no README change.

Review provenance

Spec: 7-round hosted Codex review to clean. Implementation: 7 SDD tasks, each with an independent spec+quality review; fix rounds fixed a permanently-wedged-lane bug, a row-subscription leak, a NativeMenuItem initializer-order IsEnabled clobber, and a quit-racing-startup shutdown brick. Final whole-branch review: APPROVED (0 critical/important). PR-bot wave: all four findings real and fixed (stale Agents collection after hide/reopen, grid vs window width, unserialized notifier subject, empty-id stop-ALL guard).

Manual macOS acceptance (spec §12) — performed live; three real bugs found & fixed

  • Tray icon states + count overlay (branded product mark)
  • Pause toggle initially dead — macOS never raises NativeMenu.Opening for status-item menus; refresh moved to NeedsUpdate + on-connect edge (7bebbd608) — re-verified working
  • Pause toggle vs consent rules (verified)
  • Uptime initially frozen — off-UI-thread Observable.Interval subscription lands on an orphan thread-local Dispatcher nothing pumps; ticker now subscribed on the UI thread (7edeaa3a4, regression test times out pre-fix) — re-verified ticking
  • Protected-stop UX reworked after live feedback — inline banner (layout jump, GUID, --force copy) replaced by toasts + confirm-then-force dialog (e7a403cb8) — re-verified
  • Hide-to-tray on close; Quit exits cleanly (exit code 0 observed)
  • Agents grid live-updates, repo labels, no column overlap (verified across multiple live flow participants)
  • Open-in-web deep links
  • Light/dark menu bar contrast check
  • Startup-failure window exits 1

Known follow-ups

  • Requester display name: the wire carries only requester_user_id (github:…); the server has UserInfo.DisplayName. Follow-up pair: kcap-server stamps RequesterDisplay into launch commands; kcap-cli adds the additive DTO field + daemon passthrough + app preference.
  • AI-1653 (bundling): dock icon + dock-name via Info.plist, LSUIElement/tray-only decision, needs a ≥512px icon master (largest existing brand asset is 96×96).
  • Final-review minors: §9 partial-composition hardening; misc test-doc nits.

🤖 Generated with Claude Code

alexeyzimarev and others added 21 commits August 6, 2026 17:19
…slice 2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner exemption documented (pause = non-owner launches), NeedsUpdate-only
menu rebuild cadence, stop UX pinned to the real Completed-then-removal
sequence, pause toggle single-flight + unverified state, LocalControlOps
structural validation, total tray-state projection, expanded test matrix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix the contradictory pause claim (non-owner launches), serialize passive
consent refreshes and toggles on one lane (drop-while-busy), pin StopAck
line/status validation with defined skipped presentation, define empty
ConsentAck degradation, relocate the engine acceptance test to the CLI
test project.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Toggle-during-passive gets a one-slot queue reserved for the user toggle,
the failure bullet now states the conditional reconcile-or-unverified
contract, and presentation assertions move to the app suite (skipped
banner, neutral ok:false copy, success-with-warning) with the wire-shape
assertion kept in the CLI suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Queue slot stores the desired checked value (never an inversion), the ops
failure taxonomy covers post-connect transport failures and caller
cancellation (checked before classifying, absorbed without banners), a
negative active_agents row makes the tray projection total, and the
staleness bound is stated as next-completed-refresh rather than one open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pin the desired-checked capture at menu-rebuild time (frozen command
parameter; the native click path never mutates IsChecked), give the ops
classification explicit EndOfStreamException-before-IOException
precedence with a truncated-frame test case, and split the ack-branch
tests to pin both sides of the conditional trailing-refresh contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds Capacitor.Cli.Core/LocalIpc/LocalControlOps.cs: fresh-socket-per-call
StopAgentAsync/GetConsentPolicyAsync/PutConsentPolicyAsync for the desktop
supervisor tray (design spec §7/§10), with pinned failure classification
(daemon_unreachable | daemon_rejected | unexpected_reply | timed_out) and
structural validation of consent-policy replies. Scripted-server tests cover
every StopAck/ConsentRules/ConsentAck branch plus transport classification;
adds one LaunchConsentEngine acceptance test pinning that the tray pause rule
never blocks an owner-originated launch (spec §6).
Adds the pure ten-row state mapping (spec §4), header copy (spec §5),
agent-entry projection, and pause-item enablement behind TrayViewModel,
plus the IPauseController contract Task 3 will implement.
…eController.State

Fail fast with a diagnostic InvalidOperationException instead of an
unexplained NRE if a future IPauseController implementation doesn't
replay a value synchronously on subscribe, per review fix round 1.
PauseController implements IPauseController (task 2's contract) as the single
lock-guarded lane behind ALL consent-policy IPC (passive refreshes and user
toggles), per spec §6: a passive refresh is dropped while the lane is busy, a
toggle arriving while a passive owns the lane reserves the one-slot queue and
runs its own fresh Get once the passive completes (success or failure), and a
toggle while a toggle owns the lane is ignored. TrayViewModel gains
TogglePauseCommand, wired straight through to RequestToggle.
RunPassiveAsync/RunToggleAsync/RunTrailingRefreshAsync previously caught only
OperationCanceledException and LocalControlOpsException; any other exception
(e.g. ArgumentOutOfRangeException from an over-long UnixDomainSocketEndPoint
path, which LocalControlOps does not classify) escaped the unobserved
Task.Run and left the lane wedged forever with Busy stuck true. Add a
terminal catch (Exception) to each method that releases the lane as a
failure (RunToggleAsync still falls through to the trailing refresh, like
the LocalControlOpsException arm). Also add the spec §12 "detection
strictness" test: a wildcard deny at index != 0 must not check the toggle.
Adds AgentActionService as the single code path for Stop (StopV2 force:false)
and open-in-web link building, shared by the tray menu and (later) the
main-window rows. AppNotifier carries one-line failure banners to both the UI
and stderr; TrayViewModel wires StopAgentCommand/OpenInWebCommand and folds
AgentActionService.StopsInFlight into the menu model's per-entry StopEnabled.
Main window gains an Agents grid bound to the daemon's SourceCache (sorted
CreatedAt asc, Id ordinal tiebreak), a shared 1s uptime ticker per spec §8
formatting rules, and a transient auto-dismissing banner projected from
AppNotifier per spec §11. MainWindowViewModel's ctor gains AgentActionService
and IAppNotifier; RepoLabel.Leaf is extracted from TrayViewModel into
TrayModels.cs and reused by the new AgentRowViewModel.
Transform recreates a row on every AgentStatusDto revision (chiefly a
Status transition), and without disposal the discarded row stayed
subscribed to the shared uptime ticker and StopsInFlight forever.
AgentRowViewModel now implements IDisposable over a CompositeDisposable;
MainWindowViewModel's pipeline adds DisposeMany() between Transform and
ObserveOn so replaced/removed rows are disposed, and disposing the
pipeline itself (window deactivation) disposes whatever rows are still
live. Review finding from task-5-review.md, fix round 1.
Thin, dumb Avalonia layer over the already-tested TrayViewModel (spec §4 icon,
§5 menu + NeedsUpdate-only rebuild cadence, §6 frozen-desired-value pause
capture): TrayIconRenderer draws cached 32x32 WindowIcons from new
Assets/TrayGlyphs.axaml StreamGeometry resources, TrayMenuBuilder rebuilds a
NativeMenu from a TrayMenuModel, TrayMenuSync is the pure open/dirty state
machine gating rebuilds, and TrayIconManager wires them to a real
TrayIcon/NativeMenu. TrayViewModel gains OpenMainWindowCommand/QuitCommand as
injected no-op-by-default delegates for Task 7 to supply.
NativeMenuItem recomputes IsEnabled from Command.CanExecute() whenever
Command is (re)assigned, so an earlier IsEnabled = pause.Enabled was
silently clobbered back to true — defeating the pause toggle's disable
contract (spec §5/§6/§11). Reorder so IsEnabled is set last, matching
BuildAgentItem's Stop item, and expand the adapter test to cover
Enabled: false for both Checked states.

Review: task-6-review.md Critical 1.
Spec §9: ShutdownMode.OnExplicitShutdown is now pinned once at framework-init,
MainWindowCoordinator owns the single main window across hide-to-tray cycles,
and StartAsync composes the shared ops/notifier/pause/actions plus the tray as
the last step of the success path. Teardown (quit and startup-failure alike)
disposes the tray icon before anything async, so a quit never strands a dead
menu-bar icon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The flag was set after OnShutdownRequested's _shutdownConfirmed guard, so a
coordinator that only came into existence between the two passes (quit racing
StartAsync's composition) was never flagged: the second pass closed the windows
with hide-on-close still armed, the window cancelled its own close, DoShutdown
aborted with windows still open, and every later quit early-returned on
_shutdownConfirmed — an app that could only be force-quit.

The two rules move into BeginShutdownPass (flag on every pass, then report
whether to defer), which is also what makes the race drivable by a test, and a
window built after a shutdown began now starts unprotected by construction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pause_disabled_when_not_connected_despite_retained_capability pushed a
disconnected AttachStatus with null Capabilities, which zeroes hasCapability
for the same reason connected does — a mutant dropping `connected &&` from
BuildPause's enabled formula still passed. Deliberately violate the real
AttachStatus contract (capabilities are never non-null when not Connected)
so capability alone cannot explain disablement, isolating the connected term.

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

linear-code Bot commented Aug 6, 2026

Copy link
Copy Markdown

AI-1651

@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: c107f377b8

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

.Transform(dto => new AgentRowViewModel(dto, actions, _ticker, _time, connected, stopsInFlight))
.DisposeMany()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.SortAndBind(out _agents, RowComparer)

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 Keep the bound Agents collection stable across activation

When the window activates, SortAndBind(out _agents, ...) replaces the collection instance without raising PropertyChanged for Agents. The ItemsControl can therefore remain bound to the initial NoAgents collection; this also reliably occurs after the normal hide-to-tray/reopen cycle, where reactivation creates another collection while the view still watches the previous one. Bind into a stable collection or notify that Agents changed whenever the instance is replaced.

Useful? React with 👍 / 👎.

Comment on lines +49 to +50
<StackPanel x:Name="AgentsGrid" Opacity="{Binding GridEnabled, Converter={x:Static views:GridEnabledOpacityConverter.Instance}}">
<Grid ColumnDefinitions="80,140,140,100,90,70,150" Margin="0,0,0,4">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make the agents grid scroll or fit the default window

At the default 720px window width, the 24px margins leave about 672px while these fixed columns require 770px, and the surrounding StackPanel provides no horizontal scrolling. Consequently the actions column is clipped—especially the Open in web button—and additional rows can also fall below the 480px viewport because there is no vertical scroller. Add scrolling or responsive/star-sized columns so all agent actions remain reachable at the configured default size.

Useful? React with 👍 / 👎.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Tray presence, agents grid, per-agent stop, and pause-launches toggle

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Adds a menu-bar tray icon reflecting daemon state, with a native menu listing live agents
 (per-agent Stop / Open in web), a pause-new-launches toggle, Open window, and Quit.
• Implements pause as a wildcard deny rule inserted at rules[0] (not a default flip), serialized
 through a single-flight consent lane with a one-slot queue for correctness across restarts and
 races.
• Adds a main-window Agents grid (kind, vendor, repo, requester, status, ticking uptime, per-row
 Stop/Open-in-web) that persists rows across disconnects and shows a transient failure banner.
Diagram

graph TD
  App["App.axaml.cs"] --> Ops["LocalControlOps"] --> Daemon[(Daemon Socket)]
  App --> Pause["PauseController"] --> Ops
  App --> Actions["AgentActionService"] --> Ops
  App --> Coordinator["MainWindowCoordinator"] --> MainVM["MainWindowViewModel"] --> Actions
  App --> TrayVM["TrayViewModel"] --> Pause
  TrayVM --> Actions
  App --> TrayIcon["TrayIconManager"] --> TrayVM
  subgraph Legend
    direction LR
    _svc([Service]) ~~~ _db[(External)]
  end
Loading
High-Level Assessment

The PR's approach — modeling pause as a persisted wildcard deny rule rather than a default-value flip — is the correct architectural choice: it survives restarts, coexists with concurrent CLI edits, and keeps consent show truthful, at the modest cost of first-match-wins rule-list bookkeeping which the single-flight lane already handles safely. Using Avalonia's native TrayIcon/NativeMenu instead of a custom popover was also considered and rejected for good reason (native look/dismissal/keyboard access for free); a custom popover remains a plausible future upgrade but isn't warranted for this slice.

Files changed (36) +5466 / -36

Enhancement (21) +1373 / -25
App.axaml.csWire tray icon, pause controller, agent actions, and shutdown ordering into app startup +123/-22

Wire tray icon, pause controller, agent actions, and shutdown ordering into app startup

• Constructs LocalControlOps, AppNotifier, PauseController, AgentActionService, MainWindowCoordinator, TrayViewModel, and TrayIconManager during startup; disposes them in a fixed order (tray icon first) on startup failure or shutdown; adds BeginShutdownPass to flag QuitInProgress on every shutdown pass so hide-to-tray never intercepts a real quit.

src/Capacitor.App/App.axaml.cs

TrayGlyphs.axamlAdd monochrome StreamGeometry glyphs for each tray state +15/-0

Add monochrome StreamGeometry glyphs for each tray state

• New resource dictionary defining StreamGeometry shapes (Stopped, Connecting, Idle, Running, Attention) used to render the 32x32 tray icon.

src/Capacitor.App/Assets/TrayGlyphs.axaml

AgentActionService.csNew shared Stop/Open-in-web action service for tray and grid +106/-0

New shared Stop/Open-in-web action service for tray and grid

• Implements per-agent Stop (calls ILocalControlOps.StopAgentAsync with in-flight gating) and OpenInWeb (builds a deep link from the latest snapshot's ServerUrl) shared by both the tray menu and main-window rows.

src/Capacitor.App/Services/AgentActionService.cs

AppNotifier.csAdd IAppNotifier for banner/stderr notification channel +23/-0

Add IAppNotifier for banner/stderr notification channel

• New Subject-backed notifier that writes to stderr and pushes messages to subscribers for the main-window banner.

src/Capacitor.App/Services/AppNotifier.cs

IPauseController.csDefine IPauseController contract and PauseState record +12/-0

Define IPauseController contract and PauseState record

• New interface exposing replay-1 PauseState observable plus RequestRefresh/RequestToggle methods for the pause-new-launches feature.

src/Capacitor.App/Services/IPauseController.cs

MainWindowCoordinator.csAdd coordinator owning the single main window across hide/show cycles +46/-0

Add coordinator owning the single main window across hide/show cycles

• New class that shows/hides the main window instance, intercepts window close to hide instead of close unless a quit is in progress.

src/Capacitor.App/Services/MainWindowCoordinator.cs

PauseController.csImplement single-flight consent-lane pause controller +195/-0

Implement single-flight consent-lane pause controller

• New PauseController serializes passive refreshes and user toggles through one lane with a one-slot queue, manages the wildcard deny rule at rules[0], and maps failures to neutral copy for the notifier.

src/Capacitor.App/Services/PauseController.cs

UrlOpener.csAdd IUrlOpener/ShellUrlOpener for opening URLs via the OS shell +13/-0

Add IUrlOpener/ShellUrlOpener for opening URLs via the OS shell

• New abstraction wrapping Process.Start with UseShellExecute to open Open-in-web links in the default browser.

src/Capacitor.App/Services/UrlOpener.cs

UptimeFormat.csAdd compact uptime formatting helper +27/-0

Add compact uptime formatting helper

• New static Format method producing compact seconds/minutes/hours/days uptime text for agent rows, clamping negative durations to zero.

src/Capacitor.App/UptimeFormat.cs

AgentRowViewModel.csAdd per-agent grid row view model with ticking uptime and actions +95/-0

Add per-agent grid row view model with ticking uptime and actions

• New disposable row VM computing presentation fields from an AgentStatusDto, exposing Stop/Open-in-web commands and an ActionsEnabled property gated by connection state and in-flight stops.

src/Capacitor.App/ViewModels/AgentRowViewModel.cs

MainWindowViewModel.csWire Agents collection, banner, and grid-enabled state into main window VM +87/-1

Wire Agents collection, banner, and grid-enabled state into main window VM

• Adds a DynamicData pipeline (Transform/DisposeMany/SortAndBind) projecting agent snapshots into sorted, disposable AgentRowViewModel rows, a shared ticker, GridEnabled property, and a latest-wins transient banner sourced from IAppNotifier.

src/Capacitor.App/ViewModels/MainWindowViewModel.cs

TrayModels.csAdd tray state enum and menu model records +16/-0

Add tray state enum and menu model records

• New TrayState enum, RepoLabel.Leaf helper, and TrayAgentEntry/TrayPauseItem/TrayMenuModel records used to drive the native tray menu.

src/Capacitor.App/ViewModels/TrayModels.cs

TrayViewModel.csAdd TrayViewModel projecting daemon/pause/action state into tray menu model +181/-0

Add TrayViewModel projecting daemon/pause/action state into tray menu model

• New VM combining daemon status/snapshots, pause state, and in-flight stops into a TrayMenuModel via a pure ten-row state projection, plus commands for toggling pause, stopping agents, opening in web, opening the window, and quitting.

src/Capacitor.App/ViewModels/TrayViewModel.cs

Converters.csAdd grid-opacity and empty-state visibility converters +26/-0

Add grid-opacity and empty-state visibility converters

• New GridEnabledOpacityConverter and EmptyStateVisibleConverter used by MainWindow.axaml to dim the grid and show the empty state text.

src/Capacitor.App/Views/Converters.cs

MainWindow.axamlAdd Agents grid, banner, and empty-state UI to main window +50/-1

Add Agents grid, banner, and empty-state UI to main window

• Expands the window and adds a banner border, an Agents header, an empty-state text block, and an ItemsControl-based grid with per-row Stop/Open-in-web buttons.

src/Capacitor.App/Views/MainWindow.axaml

MainWindow.axaml.csAdd CloseInterceptor hook for hide-to-tray behavior +12/-1

Add CloseInterceptor hook for hide-to-tray behavior

• MainWindow now exposes a CloseInterceptor delegate invoked on Closing, allowing MainWindowCoordinator to cancel the close and hide the window instead.

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

TrayIconManager.csAdd adapter wiring TrayViewModel into a real TrayIcon/NativeMenu +45/-0

Add adapter wiring TrayViewModel into a real TrayIcon/NativeMenu

• New thin adapter that updates the icon on every model change and rebuilds the native menu only inside NativeMenu.NeedsUpdate via TrayMenuSync, and triggers a pause refresh on menu Opening.

src/Capacitor.App/Views/TrayIconManager.cs

TrayIconRenderer.csAdd tray icon bitmap renderer with count badge +70/-0

Add tray icon bitmap renderer with count badge

• New static renderer drawing a cached 32x32 WindowIcon per (TrayState, capped count), overlaying a running-count badge via FormattedText.

src/Capacitor.App/Views/TrayIconRenderer.cs

TrayMenuBuilder.csAdd native menu builder rebuilding items from a TrayMenuModel +56/-0

Add native menu builder rebuilding items from a TrayMenuModel

• New class that clears and repopulates a NativeMenu's items (header, agent entries with Stop/Open-in-web submenus, pause toggle with frozen CommandParameter, Open window, Quit) from a model snapshot.

src/Capacitor.App/Views/TrayMenuBuilder.cs

TrayMenuSync.csAdd dirty-flag state machine gating menu rebuilds to NeedsUpdate +25/-0

Add dirty-flag state machine gating menu rebuilds to NeedsUpdate

• New pure helper recording the latest model and a dirty flag, rebuilding only when NativeMenu.NeedsUpdate fires and a change is pending.

src/Capacitor.App/Views/TrayMenuSync.cs

LocalControlOps.csAdd one-shot socket ops for stop and consent policy get/put +150/-0

Add one-shot socket ops for stop and consent policy get/put

• New LocalControlOps class performing fresh-socket Stop/ConsentRulesGet/ConsentRulesPut exchanges with strict StopAck line validation, structural DTO validation, and a stable failure-reason taxonomy (daemon_unreachable, daemon_rejected, unexpected_reply, timed_out).

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

Tests (12) +3262 / -11
AgentActionServiceTests.csAdd tests for AgentActionService stop/open-in-web behavior +262/-0

Add tests for AgentActionService stop/open-in-web behavior

• New tests covering in-flight gating, banner/stderr notification on failure, and open-in-web URL construction.

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

AgentGridTests.csAdd tests for main-window Agents grid projection +547/-0

Add tests for main-window Agents grid projection

• New tests covering DynamicData sorting, row disposal, persistence across disconnects, and grid-enabled/empty-state behavior.

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

AppStartupTests.csExtend app startup/shutdown tests for tray, pause, and coordinator wiring +295/-2

Extend app startup/shutdown tests for tray, pause, and coordinator wiring

• Adds coverage for disposal ordering of tray/pause/coordinator on startup failure and shutdown, and the quit-racing-startup shutdown scenario.

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

MainWindowSmokeTests.csUpdate smoke tests for new MainWindowViewModel constructor signature +48/-3

Update smoke tests for new MainWindowViewModel constructor signature

• Adjusts smoke tests to pass the new AgentActionService/IAppNotifier dependencies into MainWindowViewModel/BuildAndShowMainWindow.

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

MainWindowViewModelTests.csUpdate MainWindowViewModel tests for new constructor dependencies +21/-6

Update MainWindowViewModel tests for new constructor dependencies

• Adjusts existing tests to construct MainWindowViewModel with the added AgentActionService and IAppNotifier parameters.

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

PauseControllerTests.csAdd tests for PauseController lane serialization and state transitions +491/-0

Add tests for PauseController lane serialization and state transitions

• New tests covering initial refresh, wildcard-deny detection strictness, refresh failure handling, unmapped exceptions releasing the lane, and toggle/queue behavior.

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

RecordingAppServices.csAdd recording fakes for notifier and URL opener +28/-0

Add recording fakes for notifier and URL opener

• New RecordingNotifier/RecordingOpener test doubles used across the new test suites.

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

ScriptedLocalControlOps.csAdd scripted ILocalControlOps test double +80/-0

Add scripted ILocalControlOps test double

• New fake implementing ILocalControlOps with queued responses/failures for stop and consent-policy operations, used by pause/action/tray tests.

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

TrayAdapterTests.csAdd tests for TrayIconManager/TrayMenuBuilder/TrayMenuSync adapter +377/-0

Add tests for TrayIconManager/TrayMenuBuilder/TrayMenuSync adapter

• New tests validating icon updates, NeedsUpdate-only menu rebuild cadence, and CommandParameter freezing for the pause toggle.

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

TrayViewModelTests.csAdd tests for TrayViewModel state projection and commands +688/-0

Add tests for TrayViewModel state projection and commands

• New tests covering the ten-row tray state matrix, header text, agent entry projection, and pause-item enablement logic.

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

LaunchConsentEngineTests.csAdd owner-exemption acceptance test for consent engine +11/-0

Add owner-exemption acceptance test for consent engine

• New/relocated test asserting owner-originated launches are exempt from the wildcard pause rule.

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

LocalControlOpsTests.csAdd scripted-server integration tests for LocalControlOps +414/-0

Add scripted-server integration tests for LocalControlOps

• New tests driving LocalControlOps against a real Unix socket scripted server, covering StopAck parsing, error frames, malformed replies, timeouts, and abrupt disconnects.

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

Documentation (2) +824 / -0
2026-08-06-ai1651-tray-agents-stop.mdAdd implementation plan for tray/agents/stop/pause feature +399/-0

Add implementation plan for tray/agents/stop/pause feature

• New task-by-task implementation plan corresponding to the design spec, including review notes and fix-round history.

docs/superpowers/plans/2026-08-06-ai1651-tray-agents-stop.md

2026-08-06-ai1651-tray-agents-stop-design.mdAdd design spec for tray/agents/stop/pause feature +425/-0

Add design spec for tray/agents/stop/pause feature

• New design specification document covering tray state machine, pause mechanism, stop UX, and IPC ops contracts, refined over multiple review rounds.

docs/superpowers/specs/2026-08-06-ai1651-tray-agents-stop-design.md

Other (1) +7 / -0
App.axamlMerge tray glyph resource dictionary into application resources +7/-0

Merge tray glyph resource dictionary into application resources

• Adds a MergedDictionaries reference to Assets/TrayGlyphs.axaml so tray icon geometries are available as application resources.

src/Capacitor.App/App.axaml

@qodo-code-review

qodo-code-review Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Notifier subject concurrency ✓ Resolved 🐞 Bug ☼ Reliability
Description
AppNotifier.Notify pushes to a raw Subject without serialization, but Notify can be invoked
concurrently from multiple Task.Run operations (e.g., per-agent stops), which can cause Subject
observer reentrancy/concurrency failures and break banner delivery.
Code

src/Capacitor.App/Services/AppNotifier.cs[R19-22]

+    public void Notify(string message) {
+        Console.Error.WriteLine($"kcap: {message}");
+        _messages.OnNext(message);
+    }
Evidence
AppNotifier emits via an unsynchronized Subject, while AgentActionService explicitly allows
concurrent per-agent stop tasks and calls Notify from those tasks; PauseController also invokes the
notifier delegate from async code paths, making concurrent Notify calls plausible.

src/Capacitor.App/Services/AppNotifier.cs[14-22]
src/Capacitor.App/Services/AgentActionService.cs[48-78]
src/Capacitor.App/Services/PauseController.cs[101-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
`AppNotifier` uses a plain `Subject<string>` and calls `OnNext` without synchronization. This is unsafe because multiple producers (background tasks from stop operations and pause toggles) can call `Notify` concurrently, which can lead to overlapping observer callbacks, missed banner updates, or runtime exceptions.

## Issue Context
- `AgentActionService` runs stop operations concurrently across agent IDs and calls `_notifier.Notify(...)` from background tasks.
- `PauseController` also calls its notification delegate from async/background work.

## Fix Focus Areas
- src/Capacitor.App/Services/AppNotifier.cs[14-22]
- src/Capacitor.App/Services/AgentActionService.cs[48-78]
- src/Capacitor.App/Services/PauseController.cs[101-142]

## Suggested fix
- Make the notifier subject multi-producer safe, e.g.:
 - replace with a serialized subject: `readonly ISubject<string> _messages = Subject.Synchronize(new Subject<string>());`, or
 - lock around `_messages.OnNext(message)` (prefer a serialized subject to avoid holding locks while executing arbitrary observers).
- Keep stderr logging behavior unchanged.
- (Optional) Add a unit test that calls `Notify` concurrently and asserts no exception + all messages observed.

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



Remediation recommended

2. Empty id stops all ✓ Resolved 🐞 Bug ≡ Correctness
Description
LocalControlOps.StopAgentAsync forwards agentId directly into StopV2 with no non-empty validation;
the daemon treats an empty agentId as a stop-all request, so an accidental empty id could stop every
agent.
Code

src/Capacitor.Cli.Core/LocalIpc/LocalControlOps.cs[R41-43]

+    public async Task<StopAgentResult> StopAgentAsync(string agentId, bool force, CancellationToken ct) {
+        var reply = await ExchangeAsync(LocalFrame.StopV2(force, agentId), StopReplyTimeout, ct);
+        return reply.Type switch {
Evidence
The PR introduces StopAgentAsync which directly sends StopV2 using the provided agentId. In the
daemon implementation, agentId.Length == 0 explicitly executes the stop-all logic, and the frame
codec permits encoding an empty agent id, so without validation the stop-all behavior is reachable
if an empty string is ever supplied.

src/Capacitor.Cli.Core/LocalIpc/LocalControlOps.cs[41-47]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.LocalIpc.cs[75-90]
src/Capacitor.Cli.Core/LocalIpc/FrameCodec.cs[140-146]

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

## Issue description
`LocalControlOps.StopAgentAsync` sends `StopV2(force, agentId)` without validating `agentId`. On the daemon side, an empty agent id is interpreted as "stop all", creating a high-blast-radius footgun if any call path ever passes `""`.

## Issue Context
- Daemon stop handler uses `agentId.Length == 0` as the stop-all branch.
- FrameCodec.StopV2 encodes the length-prefixed agent id and allows empty strings.

## Fix Focus Areas
- src/Capacitor.Cli.Core/LocalIpc/LocalControlOps.cs[41-47]
- src/Capacitor.Cli.Core/LocalIpc/FrameCodec.cs[140-146]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.LocalIpc.cs[75-90]

## Suggested fix
- Add a guard in `LocalControlOps.StopAgentAsync`:
 - `if (string.IsNullOrEmpty(agentId)) throw new ArgumentException("agentId must be non-empty", nameof(agentId));`
- Optionally add a second guard at the UI boundary (`AgentActionService.RequestStop`) to avoid spinning up a Task.Run and to surface a clearer banner.
- Add/extend unit tests to assert empty id is rejected and no StopV2 is sent.

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


To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/Capacitor.App/Services/AppNotifier.cs
Comment thread src/Capacitor.Cli.Core/LocalIpc/LocalControlOps.cs
alexeyzimarev and others added 4 commits August 7, 2026 11:06
…p-all guard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removes the duplicated in-content title, compacts the status area into
a bold name/SEMVER-only version line, a subdued server URL, and a
colored dot + capitalized connection word merged with the agent count.
Start/Retry buttons and the StartMessage/Reason text now collapse when
their action or text is not meaningful instead of reserving dead space,
and the Agents grid header/row buttons get a lighter, denser treatment.
alexeyzimarev and others added 4 commits August 7, 2026 14:02
…l never ticks

Rows are built inside DynamicData's Transform, which runs on whatever thread
mutates the SourceCache — in production DaemonClientService's socket pump
thread. AvaloniaScheduler's non-zero-dueTime path does not marshal: it calls
DispatcherTimer.RunOnce, which binds the timer to the thread-local
Dispatcher.CurrentDispatcher and, off the UI thread, silently constructs a
brand-new dispatcher for that thread which nothing ever pumps. Its Tick never
fires, so Observable.Interval produced no value at all and every row's Uptime
froze at its seed — no exception, no log.

SubscribeOn forces the subscription (and therefore Interval's first Schedule
call) through AvaloniaScheduler's zero-dueTime Dispatcher.UIThread.Post path.
Publish().RefCount() makes the ticker genuinely shared, as its comment already
claimed, while keeping the connection lazy so constructing the VM under an
immediate scheduler still cannot spin. StartWith is dropped: the row's OAPH
initialUptime argument already seeds it, and that emission was the one value
that did arrive — on the pump thread, off the UI thread.

Regression test drives the real ticker from a background thread and asserts a
genuine periodic tick arrives on the dispatcher; it times out against the
previous implementation.

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

Replaces the placeholder StreamGeometry glyphs with the product mark
(Assets/kcap-icon.png) as the tray icon's 32x32 base, with per-state color
now a small overlay dot in the bottom-right corner (Running keeps the count
badge, on a dark-green circle for legibility). MainWindow and the
startup-error window use the same mark. Also sets AssemblyName to
"Kurrent Capacitor" so the unbundled dev binary shows a human name on dock
hover instead of "Capacitor.App".
…tions replace the inline banner

Repo-owner feedback on two UX rough edges: the inline failure banner pushed the
agents list down and back up (layout jump), showed a meaningless agent GUID, and
quoted the daemon's "Pass --force" CLI-speak the app can't act on.

- MainWindow now hosts an Avalonia WindowNotificationManager (top-right, ~4s
  expiry) subscribed to IAppNotifier.Messages; the inline Border banner and
  MainWindowViewModel's Banner/BannerLifetime are gone. AppNotifier itself and
  its stderr mirroring are unchanged.
- AgentActionService gains a confirm-then-force seam: a protected kind (wire
  Kind != "agent") awaits a confirmation dialog before stopping with force;
  cancelling is a quiet no-op. The dialog is composed in App.axaml.cs, owned by
  the main window when visible or shown standalone otherwise.
- A daemon Error result no longer surfaces verbatim in the UI — it becomes a
  generic toast while the full text (which may carry a raw agent id or
  "Pass --force" CLI-speak) goes to stderr only.
- Spec doc updated (decision 5, §7, §11, §12) to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexeyzimarev
alexeyzimarev merged commit 5c882bc into main Aug 7, 2026
6 checks passed
@alexeyzimarev
alexeyzimarev deleted the alexeyzimarev/ai-1651-tray-presence-agents-list-stop-desktop-supervisor-slice-2 branch August 7, 2026 15:07
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