Add provider request control surface
This commit is contained in:
1 parent
e5946f25c8
commit
273f69eb05
4 files changed
+218
-1
No files matched your search
@@ -135,6 +135,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
catalog={snapshot.providerCatalog}
|
||||
agents={agents}
|
||||
providerRateLimits={providerRateLimits}
|
||||
providerRequests={providerRequests}
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import type { ProviderCatalogEntry, ProviderRecord, ProviderRequestRecord } from "../../../../shared/domain";
|
||||
import { PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, PROVIDER_REQUEST_STATE, type ProviderExecutionMode } from "../../../../shared/domain";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
import { Button, Field, Icon, Pill, Segmented } from "../../shared/ui";
|
||||
|
||||
interface ProviderRequestPanelProps {
|
||||
orgId: string;
|
||||
providers: ProviderRecord[];
|
||||
catalog: ProviderCatalogEntry[];
|
||||
providerRequests: ProviderRequestRecord[];
|
||||
actions: SnapshotActions;
|
||||
}
|
||||
|
||||
const MODE_OPTIONS: ReadonlyArray<{ id: ProviderExecutionMode; label: string }> = [
|
||||
{ id: PROVIDER_EXECUTION_MODE.FOREGROUND, label: "Foreground" },
|
||||
{ id: PROVIDER_EXECUTION_MODE.BACKGROUND, label: "Background" },
|
||||
];
|
||||
|
||||
const ACTIVE_REQUEST_STATES = new Set<string>([
|
||||
PROVIDER_REQUEST_STATE.PENDING,
|
||||
PROVIDER_REQUEST_STATE.STREAMING,
|
||||
]);
|
||||
|
||||
export function ProviderRequestPanel({
|
||||
orgId,
|
||||
providers,
|
||||
catalog,
|
||||
providerRequests,
|
||||
actions,
|
||||
}: ProviderRequestPanelProps) {
|
||||
const executableProviders = providers.filter((provider) => provider.enabled && provider.authType === "api_key");
|
||||
const [providerId, setProviderId] = useState(executableProviders[0]?.id ?? "");
|
||||
const [mode, setMode] = useState<ProviderExecutionMode>(PROVIDER_EXECUTION_MODE.FOREGROUND);
|
||||
const selectedProvider = resolveSelectedProvider(executableProviders, providerId);
|
||||
const canRunBackground = selectedProvider ? isBackgroundReady(selectedProvider, catalog) : false;
|
||||
const selectedMode = canRunBackground ? mode : PROVIDER_EXECUTION_MODE.FOREGROUND;
|
||||
const recentRequests = providerRequests
|
||||
.filter((request) => executableProviders.some((provider) => provider.id === request.providerId))
|
||||
.slice(0, 8);
|
||||
|
||||
if (executableProviders.length === 0) return null;
|
||||
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget);
|
||||
const prompt = String(form.get("prompt") ?? "").trim();
|
||||
if (!prompt) throw new Error("Prompt is required.");
|
||||
const instructions = String(form.get("instructions") ?? "").trim();
|
||||
const formElement = event.currentTarget;
|
||||
void actions.run(() => window.agentCompany.createProviderResponse({
|
||||
orgId,
|
||||
providerId: requireProviderId(selectedProvider),
|
||||
prompt,
|
||||
instructions: instructions || null,
|
||||
executionMode: selectedMode,
|
||||
})).then((result) => { if (result) formElement.reset(); });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section-label" style={{ marginBottom: 10 }}>Requests</div>
|
||||
<form className="card" style={{ marginBottom: 10 }} onSubmit={submit}>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "minmax(150px, 1fr) minmax(200px, 1fr) auto", gap: 10, alignItems: "end" }}>
|
||||
<Field label="Provider">
|
||||
<select
|
||||
className="input"
|
||||
aria-label="Request provider"
|
||||
value={selectedProvider?.id ?? ""}
|
||||
onChange={(event) => setProviderId(event.target.value)}
|
||||
>
|
||||
{executableProviders.map((provider) => (
|
||||
<option key={provider.id} value={provider.id}>{provider.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<div className="field">
|
||||
<span>Mode</span>
|
||||
<div style={{ alignSelf: "flex-start" }}>
|
||||
<Segmented
|
||||
value={selectedMode}
|
||||
onChange={setMode}
|
||||
options={backgroundAwareOptions(canRunBackground)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" tone="primary">
|
||||
<span className="row" style={{ gap: 6 }}>
|
||||
<Icon name="send" size={13} />
|
||||
Run
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Field label="Prompt">
|
||||
<textarea className="input" aria-label="Request prompt" name="prompt" required rows={3} style={{ resize: "vertical" }} />
|
||||
</Field>
|
||||
<Field label="Instructions">
|
||||
<input className="input" aria-label="Request instructions" name="instructions" />
|
||||
</Field>
|
||||
</form>
|
||||
{recentRequests.length > 0 && (
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden", marginBottom: 28 }}>
|
||||
<div className="table-row head" style={{ gridTemplateColumns: "minmax(120px, 1fr) 92px 100px minmax(120px, 1fr) 86px" }}>
|
||||
<div>Provider</div><div>Mode</div><div>State</div><div>Output</div><div>Action</div>
|
||||
</div>
|
||||
{recentRequests.map((request, index) => (
|
||||
<ProviderRequestRow
|
||||
key={request.id}
|
||||
orgId={orgId}
|
||||
request={request}
|
||||
provider={requireRequestProvider(providers, request)}
|
||||
last={index === recentRequests.length - 1}
|
||||
actions={actions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderRequestRow({
|
||||
orgId,
|
||||
request,
|
||||
provider,
|
||||
last,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
request: ProviderRequestRecord;
|
||||
provider: ProviderRecord;
|
||||
last: boolean;
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
const canCancel = ACTIVE_REQUEST_STATES.has(request.state) && Boolean(request.externalResponseId);
|
||||
const cancel = () => {
|
||||
void actions.run(() => window.agentCompany.cancelProviderResponse({ orgId, requestId: request.id }));
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="table-row"
|
||||
style={{
|
||||
gridTemplateColumns: "minmax(120px, 1fr) 92px 100px minmax(120px, 1fr) 86px",
|
||||
borderBottom: last ? "none" : "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>{request.model}</div>
|
||||
</div>
|
||||
<div className="mono" style={{ color: "var(--text-2)" }}>{request.executionMode}</div>
|
||||
<div><Pill tone={stateTone(request.state)}>{request.state}</Pill></div>
|
||||
<div className="mono" style={{ color: "var(--text-2)" }}>{request.outputCharCount} chars</div>
|
||||
<Button size="small" tone="ghost" onClick={cancel} disabled={!canCancel}>
|
||||
<span className="row" style={{ gap: 5 }}>
|
||||
<Icon name="x" size={12} />
|
||||
Cancel
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function backgroundAwareOptions(canRunBackground: boolean): ReadonlyArray<{ id: ProviderExecutionMode; label: string }> {
|
||||
return canRunBackground ? MODE_OPTIONS : [MODE_OPTIONS[0]];
|
||||
}
|
||||
|
||||
function isBackgroundReady(provider: ProviderRecord, catalog: ProviderCatalogEntry[]): boolean {
|
||||
const entry = catalog.find((item) => item.key === provider.providerKey);
|
||||
if (!entry) throw new Error(`Provider catalog entry missing for ${provider.providerKey}.`);
|
||||
return entry.capabilityTags.includes("background")
|
||||
&& provider.backgroundEnabled
|
||||
&& provider.dataPolicy === PROVIDER_DATA_POLICY.STANDARD;
|
||||
}
|
||||
|
||||
function resolveSelectedProvider(providers: ProviderRecord[], providerId: string): ProviderRecord | null {
|
||||
if (providers.length === 0) return null;
|
||||
const provider = providers.find((item) => item.id === providerId);
|
||||
return provider ?? providers[0] ?? null;
|
||||
}
|
||||
|
||||
function requireProviderId(provider: ProviderRecord | null): string {
|
||||
if (!provider) throw new Error("Provider is required.");
|
||||
return provider.id;
|
||||
}
|
||||
|
||||
function requireRequestProvider(providers: ProviderRecord[], request: ProviderRequestRecord): ProviderRecord {
|
||||
const provider = providers.find((item) => item.id === request.providerId);
|
||||
if (!provider) throw new Error(`Provider missing for request ${request.id}.`);
|
||||
return provider;
|
||||
}
|
||||
|
||||
function stateTone(state: ProviderRequestRecord["state"]): "amber" | "green" | "violet" | "red" | undefined {
|
||||
if (state === PROVIDER_REQUEST_STATE.PENDING) return "amber";
|
||||
if (state === PROVIDER_REQUEST_STATE.STREAMING) return "violet";
|
||||
if (state === PROVIDER_REQUEST_STATE.SUCCEEDED) return "green";
|
||||
if (state === PROVIDER_REQUEST_STATE.FAILED) return "red";
|
||||
return undefined;
|
||||
}
|
||||
@@ -6,9 +6,11 @@ import type {
|
||||
ProviderCatalogEntry,
|
||||
ProviderRateLimitStateRecord,
|
||||
ProviderRecord,
|
||||
ProviderRequestRecord,
|
||||
} from "../../../../shared/domain";
|
||||
import { PROVIDER_DATA_POLICY } from "../../../../shared/domain";
|
||||
import { ProviderLogo } from "./ProviderLogo";
|
||||
import { ProviderRequestPanel } from "./ProviderRequestPanel";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
|
||||
interface ProvidersPanelProps {
|
||||
@@ -17,6 +19,7 @@ interface ProvidersPanelProps {
|
||||
catalog: ProviderCatalogEntry[];
|
||||
agents: AgentRecord[];
|
||||
providerRateLimits: ProviderRateLimitStateRecord[];
|
||||
providerRequests: ProviderRequestRecord[];
|
||||
actions: SnapshotActions;
|
||||
}
|
||||
|
||||
@@ -29,7 +32,7 @@ interface CloudFields {
|
||||
|
||||
const CLOUD_DEFAULTS: CloudFields = { baseUrl: "", region: "", projectId: "", deployment: "" };
|
||||
|
||||
export function ProvidersPanel({ orgId, providers, catalog, agents, providerRateLimits, actions }: ProvidersPanelProps) {
|
||||
export function ProvidersPanel({ orgId, providers, catalog, agents, providerRateLimits, providerRequests, actions }: ProvidersPanelProps) {
|
||||
const [active, setActive] = useState<ProviderRecord | null>(null);
|
||||
const [signInLaunched, setSignInLaunched] = useState(false);
|
||||
const connected = providers.filter((provider) => provider.enabled);
|
||||
@@ -86,6 +89,13 @@ export function ProvidersPanel({ orgId, providers, catalog, agents, providerRate
|
||||
providerRateLimits={providerRateLimits}
|
||||
actions={actions}
|
||||
/>
|
||||
<ProviderRequestPanel
|
||||
orgId={orgId}
|
||||
providers={connected}
|
||||
catalog={catalog}
|
||||
providerRequests={providerRequests}
|
||||
actions={actions}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -121,6 +121,13 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
|
||||
|
||||
await page.getByRole("button", { name: "Settings" }).click();
|
||||
await page.getByRole("button", { name: "Providers" }).click();
|
||||
await page.getByLabel("OpenAI background execution").check();
|
||||
await page.getByRole("button", { name: "Save OpenAI policy" }).click();
|
||||
await expect(page.getByText("Requests", { exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel("Request provider")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Background" })).toBeVisible();
|
||||
await page.getByLabel("Request prompt").fill("Draft a provider control memo.");
|
||||
await page.getByLabel("Request instructions").fill("Use direct language.");
|
||||
await page.getByLabel("OpenAI data policy").selectOption("zero_data_retention");
|
||||
await expect(page.getByLabel("OpenAI background execution")).not.toBeChecked();
|
||||
await page.getByLabel("OpenAI monthly request limit").fill("25");
|
||||
|
||||
Reference in new issue
Block a user