Use incremental AppHost discovery in the extension - #18443
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 18443Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 18443" |
This comment has been minimized.
This comment has been minimized.
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
This comment has been minimized.
This comment has been minimized.
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
…r() docs Adds the test coverage and documentation flagged in code review of PR #18443: - Repository-level incremental streaming render is now exercised: a streamed ls candidate paints into workspaceAppHostCandidatePaths before discovery completes. - ps/ls union mid-discovery is pinned: an in-workspace running AppHost not yet reported by ls is kept while candidate paths are non-empty (the discoveryPending-only condition). - Tree-view union-append is pinned: a running AppHost not in any ls candidate path (and not sharing a directory) still appears alongside idle candidates. - Documents that AppHostDiscoveryService.discover()'s onCandidate only fires for a freshly started discovery (cache miss / forceRefresh). Also centralizes the --cli-wait-for-debugger flag in _runCliProcess so individual discovery call sites pass only their command args. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
extension/src/utils/appHostDiscovery.ts:69
- This
IMPORTANTnote states the AppHost panel "forces fresh runs," but the panel's two primary discovery paths do not passforceRefresh: true: the constructor's initial_fetchWorkspaceAppHost()and theonDidChangeCandidateshandler both call it withoutforceRefresh.onCandidatestill fires reliably for the panel, but the actual mechanism is cache-miss behavior (the initial fetch hits an empty cache, and the file watcher deletes the cache entry before firingonDidChangeCandidates), notforceRefresh. Consider correcting the wording so future maintainers don't rely on the inaccurate "forces fresh runs" claim.
this._ensureWatchers(workspaceFolder, key);
This comment has been minimized.
This comment has been minimized.
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…r() docs Adds the test coverage and documentation flagged in code review of PR #18443: - Repository-level incremental streaming render is now exercised: a streamed ls candidate paints into workspaceAppHostCandidatePaths before discovery completes. - ps/ls union mid-discovery is pinned: an in-workspace running AppHost not yet reported by ls is kept while candidate paths are non-empty (the discoveryPending-only condition). - Tree-view union-append is pinned: a running AppHost not in any ls candidate path (and not sharing a directory) still appears alongside idle candidates. - Documents that AppHostDiscoveryService.discover()'s onCandidate only fires for a freshly started discovery (cache miss / forceRefresh). Also centralizes the --cli-wait-for-debugger flag in _runCliProcess so individual discovery call sites pass only their command args. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2592b30 to
f1b55b2
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Tighten AppHost stream test helper types so the new incremental discovery coverage reads consistently with the production discovery candidate contracts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Tests selector (audit mode)The full test matrix and all jobs still run in audit mode. The tests and jobs below are what selective CI would run under enforcement. 0 / 100 test projects · 2 jobs, from 7 changed files. Selected test projects (0 / 100)none — no .NET test projects run for this change. Selected jobs (2)
How these were chosen — grouped by what changedJob reasons
Selection computed for commit |
adamint
left a comment
There was a problem hiding this comment.
Built this and drove it through a real panel — the win is real and measurable. Against a 5-AppHost workspace, running the exact argv from appHostDiscovery.ts:296, first parseable candidate lands at 0.387s with --stream versus 1.476s buffered, and on a cold run there's a 1.8s gap between waves so the incremental arrival is unambiguous. The per-cliPath probe cache, resetTimeoutOnOutput and the --nologo fallback are all thoughtful. Unit suite is 1194 passing / 0 failing.
One thing I think blocks it — and I have an A/B — plus a mechanism concern.
The watcher path now flashes "no AppHosts in workspace"
AppHostDataRepository.ts:283-285. onDidChangeCandidates clears then refetches, but never re-arms the loading state — and _loadingWorkspace is only ever assigned true at field init (:270), then set false at :1399/:1657. So aspire.loading can't become true again after first load. The welcome gate is aspire.noAppHosts && !aspire.fetchAppHostsError && !aspire.loading && aspire.viewMode != 'global', and mid-rediscovery we're sitting at noAppHosts=true, loading=false, so the welcome view renders.
I confirmed this is new by pulling the three added lines back out of the compiled output and re-running the same probe:
| checkpoint | with PR | without |
|---|---|---|
| after initial discovery | noAppHosts=false, candidates=[old] |
same |
| during rediscovery | noAppHosts=true, candidates=[] |
noAppHosts=false, candidates=[old] |
| after first streamed candidate | noAppHosts=false, candidates=[new] |
same |
That watcher fires on create/change/delete of **/*.csproj, **/*.fsproj, **/*.vbproj, **/apphost.*, **/aspire.config.json and **/.aspire/settings.json, so saving any project file in the workspace collapses the tree. That's the #17976 symptom, on a path that didn't have it before — so I don't think this closes #17976, and I'd expect it to make #18054 worse rather than better.
The concurrent case is worse still. _fetchWorkspaceAppHost() at :698-704 bumps _workspaceAppHostDiscoveryVersion when a run is already in flight, which makes that run's onCandidate and .then() both early-return. So nothing repopulates until run A drains and run B reaches its first candidate — refresh twice during a large scan and the panel stays empty for the rest of the old scan plus all of the new one.
Smallest fix is setting _loadingWorkspace = true before _updateWorkspaceContext() on both paths. But I'd rather we just not clear at all: keep the previous list rendered and let the stream overwrite it, which is what streaming buys us. If stale rows have to be reaped, mark them and prune in _handleWorkspaceAppHostCandidates. Either way hold the selection — clearing _workspaceAppHostPath means run/debug see "no AppHost selected" for the whole streaming window.
Two of the added tests pin the current behavior (appHostDataRepository.test.ts:3300-3317 and :3359-3366), so they'd move with the fix.
--stream support is detected by scraping the CLI's error text
appHostDiscovery.ts:342 plus cliCompatibility.ts:37-41. We already have a mechanism for this — KnownCapabilities in ExtensionHelper.cs, surfaced through aspire config info --json and read by ConfigInfoProvider.hasCapability(). AppHostDataRepository already holds a ConfigInfoProvider and already uses it for describeIncludeDisabledCommandsCapability. The comment on DescribeIncludeDisabledCommands describes this PR's approach as the thing it was added to avoid.
Concretely: the quote character class covers ', ", backtick and the four curly quotes, but not German low-9 quotes or French guillemets — and the CLI's messages are localized. On a de or fr machine with a pre---stream CLI, isLsStreamUnsupportedError returns false, the error escapes _discoverWithLs, and _discoverCore:260-266 skips the perfectly good buffered aspire ls path entirely and falls all the way back to extension get-apphosts or filesystem scanning. Silent, locale-dependent degradation with only an info log.
Adding ls-json-stream.v1 is about 30 lines across ExtensionHelper.cs, configInfo.ts and the service constructor, and #17955 already wrote it so it can be lifted. I'd keep isLsStreamUnsupportedError as a fallback for CLIs older than the capability, just not as the primary gate.
(Requiring quotes in the token match is a nice guard against the CLI's own The --stream option requires --format json. message, and I like that there's a test for it. My problem is the mechanism, not the care that went into it.)
The discovery timeout can re-arm forever
appHostDiscovery.ts:513-547 — resetTimeoutOnOutput re-arms on every stdout chunk, every parsed line and every stderr chunk, with no absolute cap. A CLI that's hung but chatty (SDK/MSBuild warnings, update-check noise) keeps discovery alive indefinitely, and because that pins _workspaceAppHostDiscoveryInProgress the panel stays stuck. Could we keep the inactivity reset but add an overall ceiling?
Related: package.nls.json:49 still says aspire.appHostDiscoveryTimeoutMs is a "Timeout in milliseconds for Aspire CLI commands that discover AppHost projects", which isn't what it means anymore on the default path. That string ships to every locale.
Ordering flips at completion
Streamed candidates append in arrival order, the final array is sorted by path. The test at diff line 372 asserts observed = [zeta, alpha] and result = [alpha, zeta], so a two-AppHost workspace renders one order and then flips — a click target moving under the cursor, which cuts against the responsiveness goal. Same comparator on both paths? n is tiny.
No E2E coverage
grep -c test-e2e on the diff is 0. This is exactly what extension/src/test-e2e/appHostTree.e2e.test.ts and discoveryConfiguration.e2e.test.ts are for, and none of the 12 new unit tests can catch either issue above because they all stub spawnCliProcess. Nothing covers "watcher fires while a stream is in flight" either, which is the case that produces the longest blank window.
Smaller things
:97-100:forceRefreshclears_streamingLsUnsupportedCliPath. It's keyed oncliPathso it already self-invalidates when the path changes — clearing it just re-probes a byte-identical binary on every refresh.:551: the--nologoretry forwardslineCallbackinto a second full run, andhandleLinepushes into the samecandidatesarray with no dedupe. I couldn't construct a trigger so it's latent, but retry and streaming are composed with no contract for already-delivered items.:558: the new|| stdoutin the error message can dump accumulated NDJSON — including absolute filesystem paths — as a multi-line blob into the user-visible panel error viaAppHostDataRepository.ts:749.stdoutis already on the error object structurally.AppHostDataRepository.ts:715-730callsgetWorkspaceAppHostProjectSearchResultonce per streamed line and discards everything butapp_host_candidates, so each NDJSON line costs a full selection resolution, twosetContextround-trips and a tree re-render. Could that be coalesced? There's already a 250 ms debounce pattern in this file.:298-326: per AGENTS.md the NDJSON parse wants a comment showing an example of the raw wire format. The producer side does it (LsCommand.cs:170-178) and this PR does it for the error parser at:602-604, so it's just this one spot.isWorkspaceAppHostDiscoveryCompleteis tracked and exposed butAspireAppHostTreeProvidernever reads it, so the tree can't tell "found 3, done" from "found 3 so far" — the loading indicator clears on the first candidate and the partial list looks settled. That's the one acceptance criterion in #17941 I don't think this hits yet.:304-315,:333-339: if candidate 1 parses and candidate 2 is truncated we return just candidate 1 with exit code 0 and no error, because the raw reparse only runs when zero valid candidates were seen.:381:invalidatedrops the cache entry but doesn't cancel the orphanedaspire ls --streamchild, so a laterdiscover()can run a second full-workspace walk alongside the first. Intentional?
On #17955 — I think we close it in favor of this one, but the capability token, the tree-provider wiring and the E2E/spec updates are worth lifting first. I'll comment over there.
Closes #17941
Consume the CLI's streaming
lsoutput so the VS Code AppHost panel renders discovered AppHosts incrementally instead of waiting for the full buffered result.New behavior (with streaming):
https://github.com/user-attachments/assets/06913508-4137-4506-a7ea-6b88b0a8d962
Original behavior (without streaming):
https://github.com/user-attachments/assets/25346b4f-6aa8-41c1-a979-d831e09050c1