Repository navigation
Desktop shell: Home with repository and harness selection - #653
Conversation
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>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…rs 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>
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>
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>
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>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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.
…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.
…e 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>
…ing 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>
… 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.
PR Summary by QodoDesktop shell Home tab with repository/harness selection and server-launched sessions
AI Description
Diagram
High-Level Assessment
Files changed (27)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4cdfb8eb01
ℹ️ 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".
| // `new AppStateStore(PathHelpers.ConfigPath("app-state.json"))` already relies on. The | ||
| // composition root passes its held client so teardown can dispose it; a caller that passes | ||
| // none (a test) gets an unheld one, which owns nothing until a launch is actually made. | ||
| var home = new HomeViewModel(service, new AppStateStore(PathHelpers.ConfigPath("app-state.json")), launch ?? new ServerLaunchClient()); |
There was a problem hiding this comment.
Share the state store across runtime writers
This creates a second AppStateStore for the same file instead of sharing the instance created for the lifecycle graph. Because AppStateStore serializes read-modify-write operations only with its per-instance semaphore, a harness choice can overlap a lifecycle/shim/consent update, with both instances reading the old record and the last rename silently discarding the other update (for example, losing ConsentQuarantineAcked or the new HarnessByRepo entry). Pass the runtime's shared store into Home or make serialization common across instances.
Useful? React with 👍 / 👎.
| <Button x:Name="StartButton" Content="Start" Command="{Binding StartCommand}" | ||
| Background="{StaticResource KcapAccentBrush}" Foreground="#07120E" FontWeight="SemiBold" | ||
| CornerRadius="7" Padding="16,7" | ||
| IsEnabled="{Binding SelectedRepoPath, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" /> |
There was a problem hiding this comment.
Prevent launching a daemon-unavailable harness
When the daemon advertises that the selected vendor is unavailable, the flyout disables that vendor but this button remains enabled because it checks only the repository path. For example, a daemon supporting only codex plus a newly selected repository leaves the default claude selected, so clicking Start sends a launch request for a capability the daemon explicitly lacks and predictably surfaces a server rejection. Include the selected option's Available state in the command/button predicate or switch to an available selection.
Useful? React with 👍 / 👎.
Code Review by Qodo
1.
|
|
Note for reviewers: the Active session cards on Home are display-only on purpose — no click handler, no command. Opening a session needs the session workspace, which is AI-2195 (declared Wiring the two actions So an inert card is expected behaviour here, not a defect. |
- 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).
|
All five review findings verified against the code and fixed in
The casing fix carries a test that asserts the platform's own answer rather than one hardcoded expectation, so it is meaningful on every CI leg. Verified it fails without the fix: reverting the comparer to App suite 1170/1170. |
Gives the Avalonia desktop app a Home screen: pick a repository, pick a harness, start a hosted session. First slice of the hosted-agents shell.
Linear: AI-2194 (part of AI-2171 — design record and decisions are in the AI-2171 comment).
What this does
DaemonInfoDtogains a trailingSupportedVendors, populated from the same set already sent onDaemonConnect— one source of truth, no second computation.nullmeans unknown (an older daemon), never none.AppState.HarnessByRepomaps repo path → vendor token. Switching repository switches the chip; each repo keeps its own. A repo with no stored choice falls back to the default, never to the previously selected vendor.HostedHarnessCatalogbuilds the picker rows fromCore.Setup.HarnessCatalog.All, contributing only what Core has no notion of: transport family and daemon-advertised availability. A tenth vendor added to Core appears automatically.Spawnframe resolves against the daemon's PTY launchers —claudeandcodexonly — so a nine-vendor picker cannot use it.ServerLaunchClientinvokesRequestLaunchAgentV2, which reaches every vendor via the runtime factories.HomeViewModel+HomeView, wired through the composition root and disposed on both shutdown paths. Home is the window's default tab.Worth a reviewer's attention
SnakeCaseLower. An earlier revision of this branch used camelCase keys: it compiled, passed every test, and would have bounddaemon_name/repo_pathas null on every launch.LaunchHubJson.Configureis now shared by the client and its tests so the two cannot diverge, and a test pins the exact twelve-key set.IDaemonClientService.Agents/.Snapshotsare pushed from a background thread; both projectionsObserveOnbefore their binding operator. There is a test that genuinely fails if either is removed — it asserts the thread the bound collection is mutated on, because "does not throw" turned out not to be falsifiable here.Not in this slice
RequestedThemeVariant="Default"). On a light-mode host it renders as a dark slab in light chrome.Tests
Capacitor.App.Tests.Unit1169/1169 ·Capacitor.Cli.Core.Tests.Unit1996/1996 · AOT publish clean.Capacitor.Cli.Daemon.Tests.Unitis 2759 pass / 1 fail / 38 skipped. The failure isInstalled_codex_schema_matches_the_vendored_pin— installed codex 0.149.0 against an older vendored pin. Pre-existing onmain; this branch touches no codex or schema files.🤖 Generated with Claude Code