From 812f6d573a75f698ea31a1c1f2a200d60edc1165 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Thu, 27 Aug 2026 16:07:18 +0200 Subject: [PATCH 1/5] Clarify execution profile selection --- docs/reference/execution-profiles.md | 6 +- docs/reference/web-console.md | 16 +- src/Highbyte.Wrighty.Web/Assets/app.js | 4 +- .../Assets/page-regions.mjs | 6 +- .../Pages/Index.cshtml.cs | 56 ++++-- .../Pages/Shared/_CreateForm.cshtml | 13 +- .../Pages/Shared/_EditForm.cshtml | 6 +- .../Pages/Shared/_ItemDetail.cshtml | 3 +- .../Shared/_RepositorySettingsForms.cshtml | 16 +- src/Highbyte.Wrighty.Web/WebViewModels.cs | 33 +++- .../Web/WrightyWebServerTests.cs | 179 +++++++++++++++++- tests/javascript/page-regions.test.mjs | 11 +- 12 files changed, 300 insertions(+), 49 deletions(-) diff --git a/docs/reference/execution-profiles.md b/docs/reference/execution-profiles.md index 6e7cfbfa..4b7634ef 100644 --- a/docs/reference/execution-profiles.md +++ b/docs/reference/execution-profiles.md @@ -103,8 +103,10 @@ wrighty edit --profile economy --claimant-id --claim-token wrighty edit --clear-profile --claimant-id --claim-token ``` -On the web console the item editor offers **Execution profile** beside **Agent policy**, -whenever the repository configures a vocabulary. +On the web console the create and edit item forms offer **Execution profile** beside **Agent policy**. +They show the repository vocabulary when configured, or the built-in `economy`, `balanced`, and +`deep` names otherwise. The repository-default option includes the configured default profile name +when one exists, or says `vendor defaults` when Wrighty will pass no model or effort override. **Resolution fails closed.** A profile that resolves to nothing usable is an error (`AGENT_PROFILE_UNAVAILABLE`), never a quiet fallback. Wrighty does not drop to a cheaper profile to diff --git a/docs/reference/web-console.md b/docs/reference/web-console.md index e2244ee6..fb1d02b9 100644 --- a/docs/reference/web-console.md +++ b/docs/reference/web-console.md @@ -327,15 +327,19 @@ active-work, completion, and archive-triggering statuses are excluded and reject With worker-queue authorization enabled, status owns execution eligibility: creation in `defaultPickFrom` authorizes execution and the form shows that rule instead of an independent checkbox. With queue authorization disabled, the form offers **Allow automatic execution**, off by -default. An agent policy does not imply eligibility. **Create item** uses the ordinary retry-safe -creation pipeline. It never claims the new item, starts a worker, or launches a vendor agent. +default. The form also offers the item's agent and execution-profile policies; neither implies +eligibility. **Create item** uses the ordinary retry-safe creation pipeline. It never claims the new +item, starts a worker, or launches a vendor agent. The item editor's **Execution policy** section explains status-controlled authorization when the worker queue owns that decision; otherwise it offers the per-item automatic-execution checkbox. It -also carries agent policy and — when the repository configures an execution-profile vocabulary — -**Execution profile**. A repository that does not use profiles sees no such control. The choice -applies to the item's next fresh run; a recorded session keeps the model and effort it started with. See -[Execution profiles](execution-profiles.md). +also carries **Agent policy** and **Execution profile**. The profile choices come from the repository +vocabulary when configured, or from the built-in `economy`, `balanced`, and `deep` names otherwise. +For both policies, the repository-default choice includes the configured value when one exists; an +execution profile with no repository default says **vendor defaults**, meaning Wrighty passes no +model or effort override. The item viewer reports both policies with the same repository-default +labels. A profile choice applies to the item's next fresh run; a recorded session keeps the model and +effort it started with. See [Execution profiles](execution-profiles.md). The web console also shows configured status columns, priority and claim state, supports active/archived filtering, and renders each item's Markdown. The Board-wide sort offers operational diff --git a/src/Highbyte.Wrighty.Web/Assets/app.js b/src/Highbyte.Wrighty.Web/Assets/app.js index c38e21d2..90ffe797 100644 --- a/src/Highbyte.Wrighty.Web/Assets/app.js +++ b/src/Highbyte.Wrighty.Web/Assets/app.js @@ -191,8 +191,10 @@ function visibleCountDescription(count, total, archived, filtered) { function dispatchAuthenticationReady() { if (authenticationReadyDispatched || (tokenAuthenticationRequired && !token)) return; + const htmx = globalThis.htmx; + if (!htmx) return; authenticationReadyDispatched = true; - readyPageRegions(document, globalThis.htmx); + readyPageRegions(document, htmx); } const contextPanel = createContextPanelController({ diff --git a/src/Highbyte.Wrighty.Web/Assets/page-regions.mjs b/src/Highbyte.Wrighty.Web/Assets/page-regions.mjs index c92134fe..8696680e 100644 --- a/src/Highbyte.Wrighty.Web/Assets/page-regions.mjs +++ b/src/Highbyte.Wrighty.Web/Assets/page-regions.mjs @@ -21,11 +21,15 @@ export const readyRegionSelectors = [ * placeholder with its request never sent. Anything missing here is a page variant, not an error. */ export function readyPageRegions(doc, htmx) { + // A ready event sent before htmx has processed the region is lost forever. The module and the + // deferred htmx script normally initialize in the expected order, but a cold or cached load can + // reverse them. Wait for the later htmx:load callback instead of consuming the one-time startup. + if (!htmx) return []; const regions = readyRegionSelectors .map((selector) => doc.querySelector(selector)) .filter((region) => region !== null && region !== undefined); for (const region of regions) { - htmx?.process(region); + htmx.process(region); region.dispatchEvent(new CustomEvent("wrighty:ready")); } return regions; diff --git a/src/Highbyte.Wrighty.Web/Pages/Index.cshtml.cs b/src/Highbyte.Wrighty.Web/Pages/Index.cshtml.cs index b4e80552..38c68408 100644 --- a/src/Highbyte.Wrighty.Web/Pages/Index.cshtml.cs +++ b/src/Highbyte.Wrighty.Web/Pages/Index.cshtml.cs @@ -89,6 +89,11 @@ private IReadOnlyList AgentPolicyOptions() => descriptorsByName .Select(descriptor => new AgentOptionView(descriptor.Id, descriptor.DisplayName)) .ToArray(); + private IReadOnlyList ExecutionProfileOptions() => + state.Config.EffectiveWorker.EffectiveExecutionProfiles is { Count: > 0 } configuredProfiles + ? configuredProfiles + : BuiltInExecutionProfiles.Names; + public IReadOnlyList PriorityOptions => state.Config.LocalMarkdown?.Priorities ?? []; public string WebAuthenticationMode => @@ -1484,21 +1489,20 @@ private async Task return []; } - var catalogs = new List(); - foreach (var agent in adaptersByName.Keys.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)) - { - try - { - catalogs.Add(await modelDiscoveries.DiscoverAsync(agent, cancellationToken)); - } - catch (Exception exception) when (exception is not OperationCanceledException) + return await Task.WhenAll(adaptersByName.Keys + .OrderBy(name => name, StringComparer.OrdinalIgnoreCase) + .Select(async agent => { - catalogs.Add(Workers.AgentModelCatalog.Unavailable( - agent, Workers.ModelDiscoveryFailure.Unavailable)); - } - } - - return catalogs; + try + { + return await modelDiscoveries.DiscoverAsync(agent, cancellationToken); + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + return Workers.AgentModelCatalog.Unavailable( + agent, Workers.ModelDiscoveryFailure.Unavailable); + } + })); } private async Task LoadOperationalItemsAsync( @@ -2172,11 +2176,15 @@ public IActionResult OnGetCreate() null, false, null, + null, CreationAttempt.NormalizeOrCreate(null), creationStatuses, local.Priorities, state.Config.EffectiveWorker.UseWorkerQueue, state.Config.DefaultPickFrom, + RepositoryDefaultAgentLabel(), + state.Config.EffectiveWorker.DefaultExecutionProfile, + ExecutionProfileOptions(), AvailableAgents: AgentPolicyOptions())); } @@ -2187,6 +2195,7 @@ public async Task OnPostCreateAsync( string? priority, bool automaticExecutionAllowed, string? agentPolicy, + string? executionProfile, string creationAttemptId, CancellationToken cancellationToken) { @@ -2210,11 +2219,15 @@ public async Task OnPostCreateAsync( string.IsNullOrWhiteSpace(priority) ? null : priority, effectiveAutomaticExecutionAllowed, string.IsNullOrWhiteSpace(agentPolicy) ? null : agentPolicy, + string.IsNullOrWhiteSpace(executionProfile) ? null : executionProfile, creationAttemptId, creationStatuses, local.Priorities, queueAuthorizesExecution, state.Config.DefaultPickFrom, + RepositoryDefaultAgentLabel(), + state.Config.EffectiveWorker.DefaultExecutionProfile, + ExecutionProfileOptions(), AvailableAgents: AgentPolicyOptions()); if (body.Length > MaximumBodyLength) @@ -2237,7 +2250,8 @@ public async Task OnPostCreateAsync( status, draft.Priority, AutomaticExecutionAllowed: effectiveAutomaticExecutionAllowed, - AgentPolicy: draft.AgentPolicy), + AgentPolicy: draft.AgentPolicy, + ExecutionProfile: draft.ExecutionProfile), creationAttemptId, cancellationToken); return ClosePanelAndRefresh(); @@ -4305,7 +4319,9 @@ private async Task Item( workspaceView), CanDelete: canDelete, ExecutionProfile: item.ExecutionProfile, - ExecutionProfiles: state.Config.Worker?.EffectiveExecutionProfiles ?? [], + ExecutionProfiles: ExecutionProfileOptions(), + RepositoryDefaultAgentLabel: RepositoryDefaultAgentLabel(), + RepositoryDefaultExecutionProfile: state.Config.EffectiveWorker.DefaultExecutionProfile, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, QueueAuthorizesExecution: state.Config.EffectiveWorker.UseWorkerQueue, @@ -5247,6 +5263,14 @@ private static string ResponseRevision( : value.Trim().ToLowerInvariant(); } + private string? RepositoryDefaultAgentLabel() + { + var defaultAgent = state.Config.EffectiveWorker.DefaultAgent; + return string.IsNullOrWhiteSpace(defaultAgent) + ? null + : AgentDisplayName(defaultAgent); + } + private HashSet InstalledProbeAgents() => agentRuntimeCatalog.Snapshot().InstalledAgents .Select(runtime => runtime.Agent) diff --git a/src/Highbyte.Wrighty.Web/Pages/Shared/_CreateForm.cshtml b/src/Highbyte.Wrighty.Web/Pages/Shared/_CreateForm.cshtml index 62c6101d..5737e4a7 100644 --- a/src/Highbyte.Wrighty.Web/Pages/Shared/_CreateForm.cshtml +++ b/src/Highbyte.Wrighty.Web/Pages/Shared/_CreateForm.cshtml @@ -51,14 +51,23 @@ } -

Creation does not claim the item or start a worker. The agent policy only selects an agent once automatic execution is authorized.

+ +

Creation does not claim the item or start a worker. Agent and execution-profile policies apply once automatic execution is authorized.

diff --git a/src/Highbyte.Wrighty.Web/Pages/Shared/_EditForm.cshtml b/src/Highbyte.Wrighty.Web/Pages/Shared/_EditForm.cshtml index 5c6de1f5..dc9d9349 100644 --- a/src/Highbyte.Wrighty.Web/Pages/Shared/_EditForm.cshtml +++ b/src/Highbyte.Wrighty.Web/Pages/Shared/_EditForm.cshtml @@ -37,7 +37,7 @@ {