Repository navigation
Tray presence, agents list, per-agent stop, pause launches (desktop supervisor slice 2) - #474
Conversation
…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>
There was a problem hiding this comment.
💡 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| <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"> |
There was a problem hiding this comment.
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 👍 / 👎.
PR Summary by QodoTray presence, agents grid, per-agent stop, and pause-launches toggle
AI Description
Diagram
High-Level Assessment
Files changed (36)
|
Code Review by Qodo
1.
|
…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.
…; hide empty grid header
…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>
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
NeedsUpdate(macOS never raisesOpeningfor status-item menus — found in manual acceptance); the pause item's desired value is frozen intoCommandParameterat rebuild time because the native click path never mutatesIsChecked.rules[0](not a default flip): the rule is the state — correct restore by construction, survives app restarts,kcap daemon consent showtells 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.force:trueon 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.OnExplicitShutdownsteady-state; tray icon disposed before shutdown confirm. Executable named "Kurrent Capacitor" for a friendly dev-run dock label.LocalControlOpsone-shot socket ops (StopV2→StopAckwith strict line validation and an empty-id stop-ALL guard,ConsentRulesGet/Putwith 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
NativeMenuIteminitializer-orderIsEnabledclobber, and a quit-racing-startup shutdown brick. Final whole-branch review: APPROVED (0 critical/important). PR-bot wave: all four findings real and fixed (staleAgentscollection 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
NativeMenu.Openingfor status-item menus; refresh moved toNeedsUpdate+ on-connect edge (7bebbd608) — re-verified workingObservable.Intervalsubscription lands on an orphan thread-localDispatchernothing pumps; ticker now subscribed on the UI thread (7edeaa3a4, regression test times out pre-fix) — re-verified ticking--forcecopy) replaced by toasts + confirm-then-force dialog (e7a403cb8) — re-verifiedKnown follow-ups
requester_user_id(github:…); the server hasUserInfo.DisplayName. Follow-up pair: kcap-server stampsRequesterDisplayinto launch commands; kcap-cli adds the additive DTO field + daemon passthrough + app preference.LSUIElement/tray-only decision, needs a ≥512px icon master (largest existing brand asset is 96×96).🤖 Generated with Claude Code