P3 Slice Zero, Task 4 — replace Web free-text selection with the structured harness catalog (#1170)
ci/woodpecker/push/publish Pipeline failed

Co-authored-by: Jason Woltje <[email protected]>
This commit was merged in pull request #1170.
This commit is contained in:
2026-08-12 02:50:18 +00:00
committed by Mos
parent aca28405be
commit 9cd9409089
9 changed files with 1510 additions and 25 deletions
+94 -18
View File
@@ -1,4 +1,5 @@
import { useState, type KeyboardEvent, type ReactElement } from 'react';
import type { HarnessSelectionValue } from './use-harness-selection';
interface ComposerProps {
onSend: (input: { content: string; provider?: string; modelId?: string }) => void;
@@ -9,6 +10,23 @@ interface ComposerProps {
* pre-ack window where a second send could otherwise slip through. */
sending: boolean;
hasConversation: boolean;
/** Structured harness/provider/model selection state. The composer never
* accepts free-text provider/model — every sendable tuple is a validated,
* persisted catalog entry, and the send projection is derived from it. */
harness: HarnessSelectionValue;
}
/** The distinct provider ids present in the current catalog, in first-seen
* order — the provider select is catalog-derived, never a hardcoded list. */
function providerOptions(harness: HarnessSelectionValue): string[] {
const seen = new Set<string>();
const out: string[] = [];
for (const model of harness.catalog?.models ?? []) {
if (seen.has(model.providerId)) continue;
seen.add(model.providerId);
out.push(model.providerId);
}
return out;
}
export function Composer({
@@ -17,21 +35,19 @@ export function Composer({
streaming,
sending,
hasConversation,
harness,
}: ComposerProps): ReactElement {
const [content, setContent] = useState('');
const [provider, setProvider] = useState('');
const [modelId, setModelId] = useState('');
const busy = streaming || sending;
function submit(): void {
if (busy) return;
// Send is gated on a validated, persisted catalog tuple — a draft or unset
// selection can never emit, so provider/model never travel as free text.
if (!harness.canSend) return;
const trimmed = content.trim();
if (!trimmed) return;
onSend({
content: trimmed,
provider: provider.trim() || undefined,
modelId: modelId.trim() || undefined,
});
onSend({ content: trimmed, ...harness.projection });
setContent('');
}
@@ -42,6 +58,19 @@ export function Composer({
}
}
// Scope the model options to the intentionally selected provider. With no
// provider chosen (`providerId === ''`) nothing matches, so the model select
// offers only the placeholder — never a cross-provider row.
const models = (harness.catalog?.models ?? []).filter(
(model) => model.providerId === harness.providerId,
);
// A collision-safe composite option identity covering the full provider+model
// tuple. The controlled select mirrors the same identity so the exact catalog
// row highlights (a bare modelId would collide across providers).
const modelOptionValue = (model: { providerId: string; modelId: string }): string =>
`${model.providerId}:${model.modelId}`;
const selectedModelValue = harness.modelId ? `${harness.providerId}:${harness.modelId}` : '';
return (
<form
onSubmit={(event) => {
@@ -51,21 +80,68 @@ export function Composer({
className="flex flex-col gap-2 border-t p-4"
>
<div className="flex flex-wrap gap-2">
<input
<select
aria-label="Harness"
value={harness.harnessId}
onChange={(event) => harness.selectHarness(event.target.value)}
className="rounded border px-2 py-1 text-xs"
>
<option value="">Select a harness</option>
{harness.harnesses.map((item) => (
<option key={item.id} value={item.id}>
{item.displayName}
</option>
))}
</select>
<select
aria-label="Provider"
value={provider}
onChange={(event) => setProvider(event.target.value)}
placeholder="Provider (optional)"
value={harness.providerId}
onChange={(event) => harness.selectProvider(event.target.value)}
disabled={harness.catalogUnavailable || providerOptions(harness).length === 0}
className="rounded border px-2 py-1 text-xs"
/>
<input
>
<option value="">Select a provider</option>
{providerOptions(harness).map((providerId) => (
<option key={providerId} value={providerId}>
{providerId}
</option>
))}
</select>
<select
aria-label="Model"
value={modelId}
onChange={(event) => setModelId(event.target.value)}
placeholder="Model (optional)"
value={selectedModelValue}
onChange={(event) => {
// Resolve the composite option identity back to the exact catalog
// row and persist that row's own provider+model — never a bare id.
const selected = models.find((model) => modelOptionValue(model) === event.target.value);
if (selected) harness.selectModel(selected.providerId, selected.modelId);
}}
disabled={harness.catalogUnavailable || models.length === 0}
className="rounded border px-2 py-1 text-xs"
/>
>
<option value="">Select a model</option>
{models.map((model) => (
<option key={modelOptionValue(model)} value={modelOptionValue(model)}>
{model.displayName}
</option>
))}
</select>
</div>
{harness.catalogUnavailable ? (
<p role="status" className="text-xs opacity-70">
This harness catalog is currently unavailable.
</p>
) : null}
{harness.isStale ? (
<p role="status" className="text-xs opacity-70">
The saved model is no longer available pick another to continue.
</p>
) : null}
{harness.persistError ? (
<p role="alert" className="text-xs">
{harness.persistError.message}
</p>
) : null}
<div className="flex items-end gap-2">
<textarea
aria-label="Message"
@@ -78,7 +154,7 @@ export function Composer({
/>
<button
type="submit"
disabled={!content.trim() || busy}
disabled={!content.trim() || busy || !harness.canSend}
className="rounded px-3 py-2 text-sm font-medium"
>
Send