You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Desktop shell: Home with repository and harness selection (#653)
* docs(plan): AI-2194 desktop shell Home implementation plan
* feat(ipc): advertise supported vendors on the daemon status snapshot
* feat(app): remember the chosen harness per repository
Add HarnessByRepo member to AppState to persist the vendor harness choice
per repository path. Null key means the choice was never made; empty string
key ("") holds the choice for the scratch "No repository" target.
Tests verify serialization round-trip and null default behavior.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(app): harness catalogue driven by the daemon's advertised vendors
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* fix(app): rename HarnessCatalog to HostedHarnessCatalog, derive vendors from Core
Removes duplicated source of truth by deriving the vendor list from
Capacitor.Cli.Core.Setup.HarnessCatalog.All instead of maintaining a
separate Known array. Transport family (pty/acp/rpc) is now kept in a
private map as it's specific to the daemon's hosting strategy, separate
from Core's vendor registration which handles installation flags and
detection logic.
This fixes the name collision that prevented using the new HostedHarnessCatalog
class alongside Core's HarnessCatalog without explicit qualification.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* fix(app): remove unnecessary qualification of HarnessCatalog reference
Now that the app's harness catalogue is renamed to HostedHarnessCatalog,
the unqualified HarnessCatalog reference correctly resolves to Core's
HarnessCatalog via the using statement. The workaround qualification
is no longer needed.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* feat(app): launch sessions through the server hub
Home needs every vendor, but the daemon's local Spawn frame only
resolves claude/codex against its PTY launcher dictionary. The
server's RequestLaunchAgentV2 reaches all nine vendors through the
runtime factories, so the launch path goes through the hub instead
of the local socket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(app): send RequestLaunchAgentV2 payload keys in snake_case
The server applies PropertyNamingPolicy = SnakeCaseLower to every hub
payload (kcap-server JsonDefaults.ConfigureSignalRPayload); the
client's payload was serializing camelCase keys, so DaemonName and
RepoPath would bind null server-side and every launch would fail.
Fix both the wire naming (explicit snake_case [JsonPropertyName] on
every member, plus the same SnakeCaseLower policy the daemon's own
ServerConnection/WatchCommand apply) and the test gap that missed it:
LaunchRequestTests now serializes through LaunchHubJson.Configure,
the exact JsonSerializerOptions ServerLaunchClient hands
AddJsonProtocol, instead of a bare context that only proved the
client's own idea of the format. Adds a test pinning the full
twelve-key set.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(app): Home view-model with per-repository harness memory
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(app): marshal HomeViewModel's daemon projections onto the UI thread
Sessions/Harnesses were bound straight off IDaemonClientService's background
thread, matching neither MainWindowViewModel nor ConsentPromptViewModel's
ObserveOn-before-binding rule. Add RxSchedulers.MainThreadScheduler ObserveOn
before SortAndBind/ToProperty, and bring HomeViewModelTests into the
AvaloniaSession.WithImmediateRxScheduler / NotInParallel("AvaloniaSession")
cohort those schedulers require.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(app): Home surface with repository and harness selection
Adds Home as the first MainWindow tab, backed by a new HomeView bound to
Task 5's HomeViewModel: goal input, repository/harness chips, remember
toggle, Start, and the active-sessions grid. Introduces the design's dark
palette as Application-level resources (App.axaml) instead of per-control
hex literals. HomeViewModel now implements IDisposable via a
CompositeDisposable, matching TrayViewModel/ActivityViewModel, since this
task is what first constructs one.
* fix(app): wire HomeViewModel through the composition root, make Home the default tab
Task 6 fix round: HomeView was inert (no DataContext) and Agents was pinned as
the default tab to dodge two smoke-test assumptions instead of fixing them.
- App.BuildAndShowMainWindow now constructs HomeViewModel over the same
IDaemonClientService instance MainWindowViewModel uses (never a second daemon
connection), plus a fresh AppStateStore/ServerLaunchClient — the same
cheap-construction pattern BuildLifecycleController already relies on.
MainWindowViewModel exposes it as Home; MainWindow.axaml binds HomeView's
DataContext to it. App reads Home back off the built window's own DataContext
into a new _home field, so BuildAndShowMainWindow's signature (and therefore
AppStartupTests' direct call to it) never changes; _home disposes through the
same UI-disposables list as _activity/_trayVm/_pause, on both the normal
shutdown and startup-failure paths.
- Removed the IsSelected="True" pin on Agents so Home is genuinely the default
tab, and updated the two MainWindowSmokeTests that assumed Agents opened
first to select it explicitly before asserting on its content.
* fix(app): correct false claims and thread/lifetime hazards on the Home surface
The scratch target's comments claimed a "" repo path launches into a daemon-owned
worktree; AgentOrchestrator rejects any repo path that fails Directory.Exists, so
the key is storage-only until the daemon accepts a repo-less launch. The concept
and its key handling stay.
ServerLaunchClient leaked a HubConnection whenever StartAsync threw (the instance
was never assigned to _hub) and disposed its gate out from under an in-flight
launch. The client is now held by the composition root, shared across window
rebuilds, and disposed after Home on both teardown paths.
SessionCardViewModel built SolidColorBrushes on the daemon pump thread, which
worked only by accident of per-instance dispatcher affinity; ImmutableSolidColorBrush
is not an AvaloniaObject, so the four dots are shared rather than reallocated per
card per revision.
Also fixes the tab comments Home's arrival falsified, and the JSON naming comment
in ILaunchClient: an explicit [JsonPropertyName] always beats a policy, and a
policy on JsonSerializerOptions does reach source-generated metadata.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(app): cover the daemon-advertised picker, the UI-thread marshalling and the vendor map
Three gaps the branch left unfalsifiable. A snapshot narrowing the harness picker
was untested end to end (the fake's supportedVendors parameter had no caller).
HomeViewModel's ObserveOn before SortAndBind could be deleted with every test still
green: the suite pins the scheduler to Immediate and pushes from the UI thread. The
new smoke test pushes from a background thread over the session's real scheduler and
asserts the bound collection is mutated ON the UI thread — "does not throw" is not
falsifiable here, since the push raises nothing and the container still realizes even
unmarshalled (a bare VerifyAccess and a control property set from the same thread do
throw, so the harness enforces affinity; this path defers its UI work).
The transport-family map is hand-written while the vendor list comes from Core, so a
tenth vendor would be labelled "chat" silently. The runtime fallback stays — an
unknown advertised vendor must still be listed — but the gap is now a red suite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(plan): correct the AOT constraint, hub payload shape, and Task 5 type name
Three corrections made while executing, so the plan matches what was built:
- Capacitor.Cli is the only AOT-published project; the publish gate never
compiles Capacitor.App, so it cannot be evidence about app code.
- The hub payload is a source-generated record, not an anonymous type.
- Task 5 consumes HostedHarnessCatalog; HarnessCatalog is Core's own type.
* fix(app): address PR review findings
- Thread the shutdown token into HomeViewModel.StartAsync; a launch held
CancellationToken.None and could not be cancelled against teardown.
- Hold the connection gate across the hub invoke. GetConnectionAsync
disposes and rebuilds a connection that is not Connected, so releasing
before the invoke let a second launch dispose one still in use.
- Compare repo keys the way the filesystem does: case-insensitive on
Windows and macOS, case-sensitive on Linux. Applied on read, since
System.Text.Json rebuilds the dictionary with an ordinal comparer.
- Assert JSON null through JsonElementExtensions.IsNull rather than
reading ValueKind directly.
- Remove Linear issue IDs from source comments (CI gate).
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
// _lifecycle goes first (guarded, so a throw never skips _service's disposal); the lane goes LAST — its substrate must outlive any caller still awaiting RunAsync.
1037
+
// Runs after the UI disposables (DisposeUiThenConfirmShutdownAsync), so _home is already gone
1038
+
// when its launch client is torn down here. _lifecycle then goes first (guarded, so a throw
1039
+
// never skips _service's disposal); the lane goes LAST — its substrate must outlive any caller
Console.Error.WriteLine($"kcap app failed to dispose the launch client during teardown: {ex}");
1069
+
}
1070
+
_launch=null;
1071
+
}
1072
+
1023
1073
/// <summary>Quiesces shutdown in two phases: sign-in and import finish uncapped so an in-progress commit isn't torn down, then lifecycle/lane quiesce under the cap.</summary>
0 commit comments