Skip to content
Merged
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
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export default defineConfig({
"**/observer-archive-policy.spec.ts",
"**/harness-management.spec.ts",
"**/harness-catalog-screenshots.spec.ts",
"**/inline-custom-harness.spec.ts",
],
use: {
...devices["Desktop Chrome"],
Expand Down
10 changes: 8 additions & 2 deletions desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -697,12 +697,18 @@ const overrides = new Map([
// hidden-key projection keeps the top-level secret out of Advanced rows.
// +6 (1195 -> 1201): rebase onto main — this PR's model-source label wiring
// lands on top of main's dialog growth. Queued to split.
["src/features/agents/ui/AgentInstanceEditDialog.tsx", 1201],
// +28 (1201 -> 1229): inline "Add custom harness…" entry — sentinel option,
// modal state, and the AddCustomHarnessDialog mount. The shared routing and
// deferred-selection logic lives in addCustomHarness.ts to keep this minimal.
["src/features/agents/ui/AgentInstanceEditDialog.tsx", 1229],
// AgentDefinitionDialog grew past 1000 with the following load-bearing fixes:
// isRuntimeAutoSeededRef tracking for edit-mode seeding (Fizz shows models);
// runtimeSupportsLlmProviderSelection guard on discovery provider (codex fix);
// hideProviderIds computation for Databricks v1 gate. Queued to split.
["src/features/agents/ui/AgentDefinitionDialog.tsx", 1035],
// +28 (1020 -> 1048): inline "Add custom harness…" entry — sentinel option,
// modal state, and the AddCustomHarnessDialog mount. The shared routing and
// deferred-selection logic lives in addCustomHarness.ts to keep this minimal.
["src/features/agents/ui/AgentDefinitionDialog.tsx", 1048],
// #2630 emoji picker search: the shadow-root search-input autofocus effect
// (rAF retry loop) took this file 999 -> 1026 and landed without this entry,
// so main's Desktop Core went red. Queued to split with the rest of this list.
Expand Down
47 changes: 47 additions & 0 deletions desktop/src/features/agents/ui/AddCustomHarnessDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { CustomHarnessForm } from "@/features/settings/ui/CustomHarnessForm";
import { ChooserDialogContent } from "@/shared/ui/chooser-dialog-content";
import { Dialog } from "@/shared/ui/dialog";

/**
* Registers a custom ACP harness from inside an agent dialog, so "New agent"
* is a complete entry point and not a dead end that sends the user to
* Settings. Hosts the same `CustomHarnessForm` the harness catalog uses.
*/
export function AddCustomHarnessDialog({
onOpenChange,
onSaved,
open,
}: {
onOpenChange: (open: boolean) => void;
/** Called with the id of the harness that was just registered. */
onSaved: (id: string) => void;
open: boolean;
}) {
return (
<Dialog onOpenChange={onOpenChange} open={open}>
<ChooserDialogContent
className="h-[42rem] max-w-2xl"
contentClassName="flex min-h-0 flex-1 p-0"
data-testid="add-custom-harness-dialog"
scrollAreaClassName="flex min-h-0 overflow-hidden px-0"
title="Add custom harness"
>
<CustomHarnessForm
chromeless
header={
<p className="text-sm text-muted-foreground">
Register any ACP-speaking agent tool as a selectable harness.
</p>
}
onCancel={() => onOpenChange(false)}
onSaved={(id) => {
// Dismiss on save as well as cancel — both exits belong to this
// dialog, so callers only handle the resulting selection.
onOpenChange(false);
onSaved(id);
}}
/>
</ChooserDialogContent>
</Dialog>
);
}
32 changes: 30 additions & 2 deletions desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ import {
import { useProviderApiKeyFieldState } from "./providerApiKeyFieldState";
import { buildRuntimeModelProviderPayload } from "./agentDefinitionSubmitPayload";
import { AgentDefinitionDialogFooter } from "./AgentDefinitionDialogFooter";
import { AddCustomHarnessDialog } from "./AddCustomHarnessDialog";
import {
ADD_CUSTOM_HARNESS_OPTION,
runtimeDropdownAction,
usePendingHarnessSelection,
} from "./addCustomHarness";

type AgentDefinitionDialogProps = {
open: boolean;
Expand Down Expand Up @@ -167,6 +173,7 @@ export function AgentDefinitionDialog({
const [isAvatarUploadPending, setIsAvatarUploadPending] =
React.useState(false);
const [hasUserChanges, setHasUserChanges] = React.useState(false);
const [isAddHarnessOpen, setIsAddHarnessOpen] = React.useState(false);
const {
globalConfig,
inheritedDefaults: {
Expand Down Expand Up @@ -308,6 +315,7 @@ export function AgentDefinitionDialog({
setShowAdvancedFields(false);
setIsAvatarUploadPending(false);
setHasUserChanges(false);
setIsAddHarnessOpen(false);
// isRuntimeAutoSeededRef and hasSeededForOpenRef are NOT reset here — the
// [initialValues, open] effect resets both when the dialog re-opens.
}
Expand Down Expand Up @@ -578,6 +586,7 @@ export function AgentDefinitionDialog({
runtimes,
runtimesLoading,
});
runtimeDropdownOptions.push(ADD_CUSTOM_HARNESS_OPTION);
const runtimeSummaryLabel = selectedRuntime
? formatRuntimeOptionLabel(selectedRuntime)
: runtime.trim() || "Not configured";
Expand Down Expand Up @@ -662,9 +671,13 @@ export function AgentDefinitionDialog({
}

function handleRuntimeDropdownChange(nextValue: string) {
const action = runtimeDropdownAction(nextValue);
if (action.kind === "add-custom-harness") {
setIsAddHarnessOpen(true);
return;
}
setHasUserChanges(true);
const nextRuntime =
nextValue === NO_RUNTIME_DROPDOWN_VALUE ? "" : nextValue;
const nextRuntime = action.runtimeId;
// The user made an explicit choice — no longer auto-seeded.
isRuntimeAutoSeededRef.current = false;
setRuntime(nextRuntime);
Expand All @@ -680,6 +693,15 @@ export function AgentDefinitionDialog({
);
}

// Routed through the normal change handler so a harness registered inline
// resets model/provider exactly as a hand-picked one would. Scoped to `open`
// so a pending id can't outlive the dialog that started the registration.
const selectSavedHarness = usePendingHarnessSelection(
runtimes,
handleRuntimeDropdownChange,
open,
);

function handleProviderDropdownChange(nextValue: string) {
setHasUserChanges(true);
const nextProvider =
Expand Down Expand Up @@ -944,6 +966,12 @@ export function AgentDefinitionDialog({
returnFocusRef={aiDefaultsTriggerRef}
/>

<AddCustomHarnessDialog
onOpenChange={setIsAddHarnessOpen}
onSaved={selectSavedHarness}
open={isAddHarnessOpen}
/>

{isCreateMode ? createRunSection : null}

<div className="space-y-3">
Expand Down
32 changes: 30 additions & 2 deletions desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ import { useProviderApiKeyFieldState } from "./providerApiKeyFieldState";
import { resolveModelFieldStatusMessage } from "./agentConfigControls";
import { AdvancedRequiredBadge } from "./AdvancedRequiredBadge";
import { showAgentProfileSyncWarning } from "./agentProfileSyncWarning";
import { AddCustomHarnessDialog } from "./AddCustomHarnessDialog";
import {
ADD_CUSTOM_HARNESS_OPTION,
runtimeDropdownAction,
usePendingHarnessSelection,
} from "./addCustomHarness";

const ADVANCED_FIELDS_MOTION_TRANSITION = {
duration: 0.18,
Expand Down Expand Up @@ -157,6 +163,7 @@ export function AgentInstanceEditDialog({
const [avatarUrl, setAvatarUrl] = React.useState(agent.avatarUrl ?? "");
const [isAvatarUploadPending, setIsAvatarUploadPending] =
React.useState(false);
const [isAddHarnessOpen, setIsAddHarnessOpen] = React.useState(false);
const shouldReduceMotion = useReducedMotion();

// Runtime selector: defaults to "custom" until the dialog opens and the
Expand Down Expand Up @@ -191,6 +198,7 @@ export function AgentInstanceEditDialog({
setAvatarUrl(agent.avatarUrl ?? "");
setShowAdvancedFields(false);
setIsAvatarUploadPending(false);
setIsAddHarnessOpen(false);
runtimeTouched.current = false;
const matched =
runtimes.find((r) => r.command?.trim() === agent.agentCommand.trim()) ??
Expand Down Expand Up @@ -244,6 +252,7 @@ export function AgentInstanceEditDialog({
value: selectedRuntimeId,
});
}
options.push(ADD_CUSTOM_HARNESS_OPTION);
return options;
}, [sortedRuntimes, selectedRuntimeId]);

Expand Down Expand Up @@ -484,8 +493,12 @@ export function AgentInstanceEditDialog({
}

function handleRuntimeDropdownChange(nextValue: string) {
const nextRuntimeId =
nextValue === NO_RUNTIME_DROPDOWN_VALUE ? "" : nextValue;
const action = runtimeDropdownAction(nextValue);
if (action.kind === "add-custom-harness") {
setIsAddHarnessOpen(true);
return;
}
const nextRuntimeId = action.runtimeId;
const previousRuntimeId = selectedRuntimeId;
const nextRuntime = runtimes.find((r) => r.id === nextRuntimeId);

Expand Down Expand Up @@ -532,6 +545,16 @@ export function AgentInstanceEditDialog({
);
}

// Routed through the normal change handler so a harness registered inline
// pins its command and resets model/provider like a hand-picked one. Scoped
// to `open` so a pending id can't outlive the dialog that started the
// registration.
const selectSavedHarness = usePendingHarnessSelection(
runtimes,
handleRuntimeDropdownChange,
open,
);

function handleProviderDropdownChange(nextValue: string) {
const nextProvider =
nextValue === AUTO_PROVIDER_DROPDOWN_VALUE ? "" : nextValue;
Expand Down Expand Up @@ -949,6 +972,11 @@ export function AgentInstanceEditDialog({
</span>
</p>
) : null}
<AddCustomHarnessDialog
onOpenChange={setIsAddHarnessOpen}
onSaved={selectSavedHarness}
open={isAddHarnessOpen}
/>
</div>
{selectedRuntimeId === "custom" && !inheritHarness ? (
<div className="space-y-1.5">
Expand Down
Loading
Loading