feat(providers): discover local runtimes during onboarding#30
Conversation
Add a local-provider discovery endpoint that checks known local runtime commands and probes unique default HTTP endpoints for Ollama, LM Studio, llama.cpp, and LocalAI. The response reports installed/running/not-detected states with command paths, probe URLs, model counts, and errors so the UI can guide first-run setup without guessing. Extract the shared Add Provider modal and use it from both Providers and Chats. The modal now opens on the Local tab, shows discovery badges, blocks duplicate endpoint submissions, prompts for custom names on provider-id collisions, and uses the same Hecate modal pattern for provider deletion instead of native browser confirm dialogs. Improve Chats empty-state behavior for model routing. When no providers are configured, Chats shows detected local runtimes and can add all addable local providers with one click. When providers are already configured but no models are routable, Chats now shows troubleshooting details instead of repeating setup prompts. Make provider deletion optimistic with rollback on failure, keep provider status refreshes from blocking empty provider states, and update docs for local discovery, duplicate endpoint rules, and the new discovery API. Tests cover the discovery API, Add Provider modal validation, optimistic delete/rollback, Chats detected-provider onboarding, configured-provider troubleshooting, Hecate delete confirmation, duplicate endpoint blocking, and the full provider/chat e2e flows. Verified with: - cd ui && bun run typecheck - cd ui && bun run test - cd ui && bun run test:e2e - GOCACHE=/Users/chicoxyzzy/dev/hecate/.gocache go test ./internal/api
There was a problem hiding this comment.
Pull request overview
This PR adds a local-runtime discovery surface to improve first-run provider onboarding, and consolidates provider creation UX across Providers and Chats so operators can add/configure local or cloud providers with fewer dead-ends.
Changes:
- Add
GET /admin/control-plane/providers/local-discoveryto probe known local runtime commands + default HTTP endpoints and report installed/running/not-detected with model counts/errors. - Extract a shared
AddProviderModaland use it from both Providers and Chats; update deletion UX to use the app’s confirm modal instead ofwindow.confirm. - Improve Chats empty-state onboarding: show detected local runtimes when no providers exist, provide “quick add” for detected locals, and show troubleshooting details when providers exist but no models are routable.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/types/runtime.ts | Adds UI wire types for local provider discovery response records. |
| ui/src/lib/provider-utils.ts | Removes conflict-map helper (no longer used by Providers UI). |
| ui/src/lib/provider-utils.test.ts | Drops tests for the removed conflict-map helper; keeps remaining utility tests. |
| ui/src/lib/api.ts | Adds discoverLocalProviders() client helper for the new admin discovery endpoint. |
| ui/src/lib/api.test.ts | Adds a fetch assertion test for the new discovery API client call. |
| ui/src/features/providers/ProvidersView.tsx | Switches to shared AddProviderModal, adds confirm modal for deletion, and adjusts polling condition. |
| ui/src/features/providers/ProvidersView.test.tsx | Updates provider add/delete tests for shared modal, discovery badges, and duplicate blocking. |
| ui/src/features/providers/AddProviderModal.tsx | New shared modal: preset picker (Local default), discovery badges, duplicate endpoint/id blocking, and form flow. |
| ui/src/features/chats/ChatView.tsx | Adds shared add-provider modal launch + local discovery “quick add” and model-routing troubleshooting in empty state. |
| ui/src/features/chats/ChatView.test.tsx | Adds coverage for opening the shared modal, troubleshooting state, and quick-add flow. |
| ui/src/app/useRuntimeConsole.ts | Implements optimistic provider deletion with rollback on failure. |
| ui/src/app/useRuntimeConsole.test.tsx | Adds tests for optimistic delete behavior and rollback on failure. |
| ui/src/app/AppShell.tsx | Removes onNavigate plumbing from ChatView usage. |
| ui/e2e/providers.spec.ts | Updates e2e flows for Local-default modal, discovery labels, confirm delete, and duplicate blocking behavior. |
| ui/e2e/provider-lifecycle.spec.ts | Updates delete flow to confirm via the app modal. |
| ui/e2e/fixtures.ts | Adds route fixture for local discovery + updates provider creation slugging to include custom_name. |
| ui/e2e/chat.spec.ts | Adds e2e coverage for quick-adding detected local providers and configured-provider troubleshooting. |
| internal/api/server.go | Registers the new local discovery handler route. |
| internal/api/openai.go | Adds response structs for local provider discovery payloads. |
| internal/api/handler_local_provider_discovery.go | Implements discovery: command presence + deduped HTTP probes + model decoding. |
| internal/api/handler_local_provider_discovery_test.go | Adds unit tests for probe dedupe, command presence, and Ollama probe URL behavior. |
| docs/runtime-api.md | Documents the new local discovery endpoint and response semantics. |
| docs/providers.md | Documents Local-tab discovery behavior and how it’s used in onboarding. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address PR review feedback on the local-provider discovery work. The add-provider modal now resets local discovery results, loading state, and errors every time it opens so stale failed probes cannot leak into the next provider-add flow. The unused CustomButton kind prop is removed as well. Provider deletion rollback now restores the selected provider and selected model while merging the removed provider/status row back into the latest state instead of replacing whole snapshots. That keeps optimistic delete fast without clobbering background dashboard refreshes. The chat quick-add path now creates all detected local providers without triggering a dashboard refresh after every POST, then refreshes once at the end. The e2e gateway fixture also registers the local-discovery mock after the provider wildcard so Playwright route precedence keeps the request in the mock layer. Coverage now includes the stale-discovery modal reopen regression, optimistic rollback preserving model selection, one-refresh quick-add behavior, exact detected-provider e2e assertions, and explicit Ollama discovery states for installed/stopped, running with no models, and running with models. Verified with: - GOCACHE=/Users/chicoxyzzy/dev/hecate/.gocache go test ./internal/api -run 'TestDiscoverLocalProviders' - cd ui && bun run typecheck - cd ui && bun run test -- ChatView ProvidersView useRuntimeConsole api provider-utils - cd ui && bunx playwright test e2e/chat.spec.ts e2e/providers.spec.ts e2e/provider-lifecycle.spec.ts
Add accessibility expectations to the canonical Hecate UI skill so future frontend work treats semantic controls, keyboard paths, focus management, contrast, reduced motion, and accessibility-oriented tests as part of the normal design pass. Also link that accessibility baseline from ui/AGENTS.md so agents entering through the UI directory see the guidance before making changes.
Update the README onboarding flow to lead with the new first-run Chats experience: when Hecate detects local runtimes such as Ollama or LM Studio, operators can add the detected local providers in one click instead of starting from the Providers workspace. Move the provider-management screenshots into the expandable Operator UI gallery so the README's main setup path stays focused on the easiest happy path, while still preserving the detailed Providers view, Add-provider modal, and populated provider table screenshots. Refresh documentation screenshots with the latest UI. The screenshot capture script now mocks local-provider discovery for deterministic docs output: the Chats first-run shot shows detected local providers and the Add detected providers action, and the Add-provider modal shot shows the Local tab with runtime status badges. Also update docs/providers.md so the Add-provider modal caption matches the Local-first screenshot instead of describing the old Cloud preset catalog shot. Verified with: - just screenshots
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 35 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address the latest PR review feedback for local-provider discovery and one-click provider setup. Local provider discovery now runs each provider as a parallel discovery unit: command detection happens first for that provider, then it waits on a shared per-endpoint HTTP probe. The shared probe task preserves the previous one-request-per-unique-endpoint behavior for defaults like llama.cpp and LocalAI, while independent endpoints no longer starve each other behind a single sequential timeout. Each HTTP probe gets its own timeout under the request context so cancellation still propagates cleanly. The Chats quick-add path now de-dupes candidates by effective base URL before issuing create requests. If two detected presets point at the same endpoint, only the first is submitted, avoiding the partial-add + backend 409 path where a later duplicate endpoint prevented the final dashboard refresh. The Add provider modal also drops the redundant Custom-name conditional and normalizes the Custom card height/padding with preset cards so the local provider badges align visually. Coverage adds backend tests for parallel command lookups, parallel HTTP probes, shared endpoint de-dupe, and Ollama stopped/running/no-model states, plus a UI test for duplicate-endpoint quick-add filtering. Verified with: - GOCACHE=/Users/chicoxyzzy/dev/hecate/.gocache go test -count=1 ./internal/api -run 'TestDiscoverLocalProviders' - cd ui && bun run typecheck - cd ui && bun run test -- ChatView ProvidersView useRuntimeConsole api provider-utils - cd ui && bunx playwright test e2e/chat.spec.ts e2e/providers.spec.ts e2e/provider-lifecycle.spec.ts
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 35 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Finish the remaining PR review fixes around local provider discovery and provider lifecycle UI state. The Add provider modal now guards local-discovery responses with a request token so an older in-flight probe from a previous modal open cannot overwrite newer discovery results or error state. Closing or reopening the modal invalidates the old request, which keeps the picker honest when local provider checks overlap. The Chats empty-state quick-add path now treats detected-provider creation as an all-settled operation. It continues attempting every deduped provider, refreshes the dashboard whenever at least one provider was created, and still surfaces the first failure inline so users are not left in a partial-added state with stale UI. The optimistic provider-delete rollback now restores removed provider rows at their original indexes in both configured-provider and provider-status state. That keeps row ordering stable after a failed delete instead of appending the restored provider to the end. Tests cover stale discovery response suppression, partial quick-add failure reconciliation, and middle-row delete rollback order. Verified with: - cd ui && bun run typecheck - cd ui && bun run test -- ChatView ProvidersView useRuntimeConsole - cd ui && bun run test - cd ui && bun run test:e2e
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 35 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address the latest PR review comments for local provider discovery and provider UI branding. Local HTTP probes now validate that a successful 2xx response also matches the expected provider schema. Invalid JSON or incompatible response bodies are reported as probe errors with HTTPAvailable=false instead of showing a misleading Running state with zero models. Provider icon color branding now lives in the shared provider-utils module and is reused by both the Providers table and the Add provider modal. This removes the duplicated PRESET_COLORS maps so future provider branding changes have one canonical place to land. The local discovery API client test title now names the real admin endpoint, matching the request path asserted by the test. Verified with: - cd ui && bun run typecheck - cd ui && bun run test -- api provider-utils ProvidersView - cd ui && bun run test - GOCACHE=/Users/chicoxyzzy/dev/hecate/.gocache go test ./internal/api -run 'TestDiscoverLocalProviders|TestLocalProviderProbeURL' - GOCACHE=/Users/chicoxyzzy/dev/hecate/.gocache go test ./internal/api
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 35 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export type LocalProviderDiscoveryRecord = { | ||
| preset_id: string; | ||
| name: string; | ||
| base_url: string; | ||
| probe_url: string; | ||
| status: "running" | "installed" | "not_detected" | "error" | string; | ||
| command?: string; |
| getProbe := func(probeURL, providerID string) *localHTTPProbeTask { | ||
| probesMu.Lock() | ||
| defer probesMu.Unlock() | ||
| if task, ok := probes[probeURL]; ok { | ||
| return task | ||
| } | ||
| task := &localHTTPProbeTask{done: make(chan struct{})} | ||
| probes[probeURL] = task | ||
| go func() { | ||
| probeCtx, cancel := context.WithTimeout(ctx, localProviderDiscoveryTimeout) | ||
| defer cancel() | ||
| task.result = probeLocalProviderHTTP(probeCtx, client, probeURL, providerID) | ||
| close(task.done) | ||
| }() |
Address the latest Copilot review comments on PR #30. Local provider discovery still dedupes network requests by probe URL, but the shared work now stops at fetching the raw HTTP response. Each preset decodes that raw response separately with its own provider identity, so shared endpoints such as llama.cpp and LocalAI cannot inherit whichever provider happened to start the probe first. This preserves the one-request-per-endpoint behavior while keeping schema errors and model decoding provider-specific. The local discovery UI type now models status as an actual closed union with an explicit unknown fallback instead of unioning the literals with string, which collapsed the type to plain string and removed useful compile-time checking. Regression coverage asserts that shared invalid endpoint responses are requested once but produce provider-specific decode errors for every preset using that endpoint. Verified with: - cd ui && bun run typecheck - cd ui && bun run test -- api provider-utils ProvidersView - cd ui && bun run test - GOCACHE=/Users/chicoxyzzy/dev/hecate/.gocache go test ./internal/api -run 'TestDiscoverLocalProviders|TestLocalProviderProbeURL' - GOCACHE=/Users/chicoxyzzy/dev/hecate/.gocache go test ./internal/api
Address the latest Copilot review comments on PR #30. Local provider discovery still dedupes network requests by probe URL, but the shared work now stops at fetching the raw HTTP response. Each preset decodes that raw response separately with its own provider identity, so shared endpoints such as llama.cpp and LocalAI cannot inherit whichever provider happened to start the probe first. This preserves the one-request-per-endpoint behavior while keeping schema errors and model decoding provider-specific. The local discovery UI type now models status as an actual closed union with an explicit unknown fallback instead of unioning the literals with string, which collapsed the type to plain string and removed useful compile-time checking. Regression coverage asserts that shared invalid endpoint responses are requested once but produce provider-specific decode errors for every preset using that endpoint. Verified with: - cd ui && bun run typecheck - cd ui && bun run test -- api provider-utils ProvidersView - cd ui && bun run test - GOCACHE=/Users/chicoxyzzy/dev/hecate/.gocache go test ./internal/api -run 'TestDiscoverLocalProviders|TestLocalProviderProbeURL' - GOCACHE=/Users/chicoxyzzy/dev/hecate/.gocache go test ./internal/api
Add a local-provider discovery endpoint that checks known local runtime commands and probes unique default HTTP endpoints for Ollama, LM Studio, llama.cpp, and LocalAI. The response reports installed/running/not-detected states with command paths, probe URLs, model counts, and errors so the UI can guide first-run setup without guessing.
Extract the shared Add Provider modal and use it from both Providers and Chats. The modal now opens on the Local tab, shows discovery badges, blocks duplicate endpoint submissions, prompts for custom names on provider-id collisions, and uses the same Hecate modal pattern for provider deletion instead of native browser confirm dialogs.
Improve Chats empty-state behavior for model routing. When no providers are configured, Chats shows detected local runtimes and can add all addable local providers with one click. When providers are already configured but no models are routable, Chats now shows troubleshooting details instead of repeating setup prompts.
Make provider deletion optimistic with rollback on failure, keep provider status refreshes from blocking empty provider states, and update docs for local discovery, duplicate endpoint rules, and the new discovery API.
Tests cover the discovery API, Add Provider modal validation, optimistic delete/rollback, Chats detected-provider onboarding, configured-provider troubleshooting, Hecate delete confirmation, duplicate endpoint blocking, and the full provider/chat e2e flows.
Verified with:
cd ui && bun run typecheck
cd ui && bun run test
cd ui && bun run test:e2e
go test ./internal/api