Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions desktop/src/features/agents/ui/WhereToRunSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { useBackendProvidersQuery } from "@/features/agents/hooks";
import { probeBackendProvider } from "@/shared/api/tauri";

import { ProviderConfigFields } from "./ProviderConfigFields";
import { emptyWhereToRunDraft, type WhereToRunDraft } from "./whereToRunIntent";
import {
applyProbeResult,
emptyWhereToRunDraft,
type WhereToRunDraft,
} from "./whereToRunIntent";

/** Optional remote-backend selector. Buzz shared compute is an LLM provider, not a run destination. */
export function WhereToRunSection({
Expand All @@ -26,32 +30,28 @@ export function WhereToRunSection({
[backendProviders, draft.runOn],
);

// Read live draft + callback through refs so the probe effect can key on the
// *provider identity* alone. Depending on `draft` here re-fired the effect on
// every keystroke, and each async probe resolution then reset the config —
// wiping whatever the user was typing.
const draftRef = React.useRef(draft);
draftRef.current = draft;
const onDraftChangeRef = React.useRef(onDraftChange);
onDraftChangeRef.current = onDraftChange;

const selectedBinaryPath = selectedBackendProvider?.binaryPath;

React.useEffect(() => {
if (!isProviderMode || !selectedBackendProvider) {
if (!isProviderMode || !selectedBinaryPath) {
setProbeError(null);
return;
}
let cancelled = false;
setProbeError(null);
void probeBackendProvider(selectedBackendProvider.binaryPath)
void probeBackendProvider(selectedBinaryPath)
.then((result) => {
if (cancelled) return;
const defaults: Record<string, string> = {};
const properties =
(result.config_schema as Record<string, unknown> | undefined)
?.properties ?? {};
for (const [key, property] of Object.entries(properties) as [
string,
Record<string, unknown>,
][]) {
if (property.default != null)
defaults[key] = String(property.default);
}
onDraftChange({
...draft,
probedProvider: result,
providerConfig: defaults,
});
onDraftChangeRef.current(applyProbeResult(draftRef.current, result));
})
.catch((error: unknown) => {
if (!cancelled) {
Expand All @@ -61,7 +61,7 @@ export function WhereToRunSection({
return () => {
cancelled = true;
};
}, [draft, isProviderMode, onDraftChange, selectedBackendProvider]);
}, [isProviderMode, selectedBinaryPath]);

if (backendProviders.length === 0) return null;

Expand Down
53 changes: 53 additions & 0 deletions desktop/src/features/agents/ui/whereToRunIntent.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";

import {
applyProbeResult,
canSubmitWhereToRun,
emptyWhereToRunDraft,
providerConfigComplete,
Expand Down Expand Up @@ -59,3 +60,55 @@ test("provider draft resolves with coerced config values", () => {
config: { region: "us", size: 3 },
});
});

// Regression: a probe resolving after the user has typed must NOT wipe the
// user's input. Previously the probe handler set `providerConfig` to the schema
// defaults unconditionally, so an async probe completing mid-typing cleared the
// field (e.g. the Blox workstation_name input reset itself while typing).
test("applyProbeResult preserves user-entered config when probe resolves", () => {
const draft = {
...emptyWhereToRunDraft,
runOn: "blox",
providerConfig: { region: "234" },
};
const next = applyProbeResult(draft, probed);
assert.equal(next.providerConfig.region, "234");
assert.equal(next.probedProvider, probed);
});

test("applyProbeResult seeds schema defaults on fresh provider selection", () => {
const probedWithDefault = {
ok: true,
config_schema: {
properties: {
workstation_name: { type: "string" },
bundle_tag: { type: "string", default: "sprig-latest" },
},
required: ["workstation_name"],
},
};
const next = applyProbeResult(
{ ...emptyWhereToRunDraft, runOn: "blox", providerConfig: {} },
probedWithDefault,
);
assert.equal(next.providerConfig.bundle_tag, "sprig-latest");
});

test("applyProbeResult: user-entered value beats a schema default", () => {
const probedWithDefault = {
ok: true,
config_schema: {
properties: { bundle_tag: { type: "string", default: "sprig-latest" } },
required: [],
},
};
const next = applyProbeResult(
{
...emptyWhereToRunDraft,
runOn: "blox",
providerConfig: { bundle_tag: "custom" },
},
probedWithDefault,
);
assert.equal(next.providerConfig.bundle_tag, "custom");
});
30 changes: 30 additions & 0 deletions desktop/src/features/agents/ui/whereToRunIntent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,36 @@ export const emptyWhereToRunDraft: WhereToRunDraft = {
probedProvider: null,
};

/**
* Merge a completed provider probe into the draft. Seeds schema defaults for
* config keys the user hasn't set yet, but preserves any value the user has
* already entered.
*
* This ordering matters: the probe is async, so it can resolve *after* the user
* has started typing. Overwriting `providerConfig` with the raw defaults there
* wiped the input mid-typing (e.g. the Blox `workstation_name` field resetting
* itself). Defaults are spread first so user-entered values win.
*/
export function applyProbeResult(
draft: WhereToRunDraft,
result: BackendProviderProbeResult,
): WhereToRunDraft {
const defaults: Record<string, string> = {};
const schema = result.config_schema as Record<string, unknown> | undefined;
const properties =
(schema?.properties as
| Record<string, Record<string, unknown>>
| undefined) ?? {};
for (const [key, property] of Object.entries(properties)) {
if (property.default != null) defaults[key] = String(property.default);
}
return {
...draft,
probedProvider: result,
providerConfig: { ...defaults, ...draft.providerConfig },
};
}

export function providerConfigComplete(draft: WhereToRunDraft): boolean {
if (draft.runOn === "local") return true;
if (!draft.probedProvider) return false;
Expand Down