History

Optimize add provider dialog state derivation

Cursor Agent committed 6 months ago

Commit bf48aa21 parent 131234b1 file · +243 −258Browse files

Unified · Split

apps/web/src/components/settings/AddProviderInstanceDialog.tsx+243−258
@@ -2,7 +2,7 @@ · 1 unmodified line
22
33 import { CheckIcon } from "lucide-react";
44 import { Radio as RadioPrimitive } from "@base-ui/react/radio";
5−import { useCallback, useEffect, useMemo, useState } from "react";
5+import { useCallback, useMemo, useState } from "react";
66 import {
77 ProviderInstanceId,
88 ProviderDriverKind,
@@ -114,6 +114,18 @@ interface AddProviderInstanceDialogProps { · 105 unmodified lines
114114 }
115115
116116 export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderInstanceDialogProps) {
117+ return (
118+ <Dialog open={open} onOpenChange={onOpenChange}>
119+ {open ? <AddProviderInstanceDialogContent onOpenChange={onOpenChange} /> : null}
120+ </Dialog>
121+ );
122+}
123+
124+function AddProviderInstanceDialogContent({
125+ onOpenChange,
126+}: {
127+ readonly onOpenChange: (open: boolean) => void;
128+}) {
117129 const settings = useSettings();
118130 const { updateSettings } = useUpdateSettings();
119131
@@ -121,8 +133,7 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns · 1 unmodified line
121133 const [driver, setDriver] = useState<ProviderDriverKind>(DEFAULT_DRIVER_KIND);
122134 const [label, setLabel] = useState("");
123135 const [accentColor, setAccentColor] = useState<string>("");
124− const [instanceId, setInstanceId] = useState("");
125− const [instanceIdDirty, setInstanceIdDirty] = useState(false);
136+ const [manualInstanceId, setManualInstanceId] = useState<string | null>(null);
126137 // Driver-specific config drafts keyed by driver so toggling between drivers
127138 // during the same dialog session does not lose in-progress input.
128139 const [configByDriver, setConfigByDriver] = useState<Record<string, Record<string, unknown>>>({});
@@ -135,32 +146,12 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns · 6 unmodified lines
135146 [settings.providerInstances],
136147 );
137148
138− // Reset the form every time the dialog opens so each creation starts
139− // from a clean slate.
140− useEffect(() => {
141− if (!open) return;
142− setDriver(DEFAULT_DRIVER_KIND);
143− setLabel("");
144− setAccentColor("");
145− setInstanceId("");
146− setWizardStep(0);
147− setInstanceIdDirty(false);
148− setConfigByDriver({});
149− setHasAttemptedSubmit(false);
150− }, [open]);
151−
152− // Auto-derive the instance id from driver + label until the user types
153− // in the Instance ID field directly (after which they own its value).
154− useEffect(() => {
155− if (instanceIdDirty) return;
156− setInstanceId(deriveInstanceId(driver, label));
157− }, [driver, label, instanceIdDirty]);
158−
159149 const driverOption = DRIVER_OPTION_BY_VALUE[driver] ?? DEFAULT_DRIVER_OPTION;
160150 const driverSettingsFields = useMemo(
161151 () => deriveProviderSettingsFields(driverOption),
162152 [driverOption],
163153 );
154+ const instanceId = manualInstanceId ?? deriveInstanceId(driver, label);
164155 const instanceIdError = validateInstanceId(instanceId, existingIds);
165156 const showInstanceIdError = hasAttemptedSubmit && instanceIdError !== null;
166157 const previewLabel = label.trim() || `${driverOption.label} Workspace`;
@@ -236,256 +227,250 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns · 69 unmodified lines
236227 ]);
237228
238229 return (
239− <Dialog open={open} onOpenChange={onOpenChange}>
240− <DialogPopup className="max-w-xl overflow-hidden">
241− <div className="flex min-h-0 flex-col overflow-hidden border-foreground/10 bg-background shadow-2xl">
242− <DialogHeader className="border-b border-border/70 bg-background">
243− <DialogTitle>Add provider instance</DialogTitle>
244− <DialogDescription>
245− Configure an additional provider instance — for example, a second Codex install
246− pointed at a different workspace.
247− </DialogDescription>
248− <div className="grid grid-cols-3 gap-2">
249− {wizardSteps.map((step, index) => (
250− <button
251− key={step}
252− type="button"
230+ <DialogPopup className="max-w-xl overflow-hidden">
231+ <div className="flex min-h-0 flex-col overflow-hidden border-foreground/10 bg-background shadow-2xl">
232+ <DialogHeader className="border-b border-border/70 bg-background">
233+ <DialogTitle>Add provider instance</DialogTitle>
234+ <DialogDescription>
235+ Configure an additional provider instance — for example, a second Codex install pointed
236+ at a different workspace.
237+ </DialogDescription>
238+ <div className="grid grid-cols-3 gap-2">
239+ {wizardSteps.map((step, index) => (
240+ <button
241+ key={step}
242+ type="button"
243+ className={cn(
244+ "grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] gap-x-2 rounded-lg border px-3 py-2 text-left",
245+ index === wizardStep
246+ ? "border-primary bg-primary/10 ring-1 ring-primary/25"
247+ : index < wizardStep
248+ ? "border-border bg-background"
249+ : "border-border bg-muted/40",
250+ )}
251+ onClick={() => setWizardStep(index)}
252+ >
253+ <span
253254 className={cn(
254− "grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] gap-x-2 rounded-lg border px-3 py-2 text-left",
255− index === wizardStep
256− ? "border-primary bg-primary/10 ring-1 ring-primary/25"
257− : index < wizardStep
258− ? "border-border bg-background"
259− : "border-border bg-muted/40",
255+ "row-span-2 mt-0.5 grid size-4 place-items-center rounded-full border",
256+ index < wizardStep
257+ ? "border-primary bg-primary text-primary-foreground"
258+ : index === wizardStep
259+ ? "border-primary bg-background"
260+ : "border-muted-foreground/35 bg-background",
260261 )}
261− onClick={() => setWizardStep(index)}
262+ aria-hidden
262263 >
263− <span
264− className={cn(
265− "row-span-2 mt-0.5 grid size-4 place-items-center rounded-full border",
266− index < wizardStep
267− ? "border-primary bg-primary text-primary-foreground"
268− : index === wizardStep
269− ? "border-primary bg-background"
270− : "border-muted-foreground/35 bg-background",
271− )}
272− aria-hidden
273− >
274− {index < wizardStep ? <CheckIcon className="size-3" /> : null}
275− </span>
276− <span className="text-[10px] font-medium uppercase text-muted-foreground">
277− Step {index + 1}
278− </span>
279− <span className="truncate text-xs font-semibold text-foreground">
280− {step}
281− {index < wizardStep && wizardStepSummaries[index]
282− ? `: ${wizardStepSummaries[index]}`
283− : ""}
284− </span>
285− </button>
286− ))}
264+ {index < wizardStep ? <CheckIcon className="size-3" /> : null}
265+ </span>
266+ <span className="text-[10px] font-medium uppercase text-muted-foreground">
267+ Step {index + 1}
268+ </span>
269+ <span className="truncate text-xs font-semibold text-foreground">
270+ {step}
271+ {index < wizardStep && wizardStepSummaries[index]
272+ ? `: ${wizardStepSummaries[index]}`
273+ : ""}
274+ </span>
275+ </button>
276+ ))}
277+ </div>
278+ </DialogHeader>
279+
280+ <div
281+ data-slot="dialog-panel"
282+ className="space-y-4 border-b border-border/70 bg-muted/20 px-6 py-5"
283+ >
284+ <AnimatedHeight>
285+ <div className={cn("grid gap-2", wizardStep !== 0 && "hidden")}>
286+ <span id="add-instance-driver-label" className="text-xs font-medium text-foreground">
287+ Driver
288+ </span>
289+ <RadioGroup
290+ value={driver}
291+ onValueChange={(value) => setDriver(ProviderDriverKind.make(value))}
292+ aria-labelledby="add-instance-driver-label"
293+ className="grid grid-cols-2 gap-2.5"
294+ >
295+ {DRIVER_OPTIONS.map((option) => {
296+ const IconComponent = option.icon;
297+ const isSelected = option.value === driver;
298+ return (
299+ <RadioPrimitive.Root
300+ key={option.value}
301+ value={option.value}
302+ className={cn(
303+ "relative flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-3 text-left outline-none transition-[background-color,border-color,box-shadow]",
304+ "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
305+ isSelected
306+ ? "border-primary bg-background shadow-sm ring-2 ring-primary/35"
307+ : "border-border bg-background hover:border-foreground/20 hover:bg-muted/50",
308+ )}
309+ >
310+ <IconComponent className="size-5 shrink-0" aria-hidden />
311+ <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
312+ {option.label}
313+ </span>
314+ {option.badgeLabel ? (
315+ <Badge variant="warning" size="sm">
316+ {option.badgeLabel}
317+ </Badge>
318+ ) : null}
319+ </RadioPrimitive.Root>
320+ );
321+ })}
322+ {COMING_SOON_DRIVER_OPTIONS.map((option) => {
323+ const IconComponent = option.icon;
324+ return (
325+ <RadioPrimitive.Root
326+ key={option.value}
327+ value={option.value}
328+ disabled
329+ className={cn(
330+ "relative flex cursor-not-allowed items-center gap-3 rounded-lg border border-border bg-background px-3 py-3 text-left opacity-55 outline-none",
331+ )}
332+ >
333+ <IconComponent
334+ className="size-5 shrink-0 text-muted-foreground"
335+ aria-hidden
336+ />
337+ <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
338+ {option.label}
339+ </span>
340+ <Badge variant="warning" size="sm">
341+ Coming Soon
342+ </Badge>
343+ </RadioPrimitive.Root>
344+ );
345+ })}
346+ </RadioGroup>
287347 </div>
288− </DialogHeader>
289348
290− <div
291− data-slot="dialog-panel"
292− className="space-y-4 border-b border-border/70 bg-muted/20 px-6 py-5"
293− >
294− <AnimatedHeight>
295− <div className={cn("grid gap-2", wizardStep !== 0 && "hidden")}>
296− <span
297− id="add-instance-driver-label"
298− className="text-xs font-medium text-foreground"
299− >
300− Driver
349+ <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}>
350+ <span className="text-xs font-medium text-foreground">Label</span>
351+ <Input
352+ className="bg-background"
353+ placeholder="e.g. Work"
354+ value={label}
355+ onChange={(event) => setLabel(event.target.value)}
356+ />
357+ <span className="text-[11px] text-muted-foreground">
358+ Shown in the provider list. Optional.
359+ </span>
360+ </label>
361+
362+ <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}>
363+ <span className="text-xs font-medium text-foreground">Instance ID</span>
364+ <Input
365+ className="bg-background"
366+ placeholder={`${driver}_work`}
367+ value={instanceId}
368+ onChange={(event) => {
369+ setManualInstanceId(event.target.value);
370+ }}
371+ aria-invalid={showInstanceIdError}
372+ />
373+ {showInstanceIdError ? (
374+ <span className="text-[11px] text-destructive">{instanceIdError}</span>
375+ ) : (
376+ <span className="text-[11px] text-muted-foreground">
377+ Routing key used by threads and sessions. Letters, digits, '-', or '_'.
301378 </span>
302− <RadioGroup
303− value={driver}
304− onValueChange={(value) => setDriver(ProviderDriverKind.make(value))}
305− aria-labelledby="add-instance-driver-label"
306− className="grid grid-cols-2 gap-2.5"
307− >
308− {DRIVER_OPTIONS.map((option) => {
309− const IconComponent = option.icon;
310− const isSelected = option.value === driver;
311− return (
312− <RadioPrimitive.Root
313− key={option.value}
314− value={option.value}
315− className={cn(
316− "relative flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-3 text-left outline-none transition-[background-color,border-color,box-shadow]",
317− "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
318− isSelected
319− ? "border-primary bg-background shadow-sm ring-2 ring-primary/35"
320− : "border-border bg-background hover:border-foreground/20 hover:bg-muted/50",
321− )}
322− >
323− <IconComponent className="size-5 shrink-0" aria-hidden />
324− <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
325− {option.label}
326− </span>
327− {option.badgeLabel ? (
328− <Badge variant="warning" size="sm">
329− {option.badgeLabel}
330− </Badge>
331− ) : null}
332− </RadioPrimitive.Root>
333− );
334− })}
335− {COMING_SOON_DRIVER_OPTIONS.map((option) => {
336− const IconComponent = option.icon;
379+ )}
380+ </label>
381+
382+ <div className={cn("grid gap-2", wizardStep !== 1 && "hidden")}>
383+ <span className="text-xs font-medium text-foreground">Accent color</span>
384+ <div className="flex min-w-0 flex-wrap items-center gap-2">
385+ <input
386+ type="color"
387+ value={normalizeProviderAccentColor(accentColor) ?? PROVIDER_ACCENT_SWATCHES[0]}
388+ onChange={(event) => setAccentColor(event.target.value)}
389+ aria-label="Provider instance accent color"
390+ className="h-8 w-10 cursor-pointer rounded-xl border border-input bg-background p-0.5"
391+ />
392+ <div className="flex flex-wrap gap-1.5">
393+ {PROVIDER_ACCENT_SWATCHES.map((swatch) => {
394+ const selected = accentColor.toLowerCase() === swatch;
337395 return (
338− <RadioPrimitive.Root
339− key={option.value}
340− value={option.value}
341− disabled
396+ <button
397+ key={swatch}
398+ type="button"
342399 className={cn(
343− "relative flex cursor-not-allowed items-center gap-3 rounded-lg border border-border bg-background px-3 py-3 text-left opacity-55 outline-none",
400+ "size-6 cursor-pointer rounded-full border transition",
401+ selected
402+ ? "scale-110 border-foreground ring-2 ring-ring ring-offset-1 ring-offset-background"
403+ : "border-black/10 hover:scale-105 dark:border-white/20",
344404 )}
345− >
346− <IconComponent
347− className="size-5 shrink-0 text-muted-foreground"
348− aria-hidden
349− />
350− <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
351− {option.label}
352− </span>
353− <Badge variant="warning" size="sm">
354− Coming Soon
355− </Badge>
356− </RadioPrimitive.Root>
405+ style={{ backgroundColor: swatch }}
406+ onClick={() => setAccentColor(swatch)}
407+ aria-label={`Use ${swatch} accent`}
408+ />
357409 );
358410 })}
359− </RadioGroup>
411+ </div>
412+ {accentColor ? (
413+ <Button
414+ type="button"
415+ size="sm"
416+ variant="ghost"
417+ className="h-7 px-2 text-xs text-muted-foreground"
418+ onClick={() => setAccentColor("")}
419+ >
420+ Clear
421+ </Button>
422+ ) : null}
360423 </div>
424+ <span className="text-[11px] text-muted-foreground">
425+ Optional marker shown in the picker.
426+ </span>
427+ </div>
361428
362− <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}>
363− <span className="text-xs font-medium text-foreground">Label</span>
364− <Input
365− className="bg-background"
366− placeholder="e.g. Work"
367− value={label}
368− onChange={(event) => setLabel(event.target.value)}
369− />
370− <span className="text-[11px] text-muted-foreground">
371− Shown in the provider list. Optional.
372− </span>
373− </label>
374−
375− <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}>
376− <span className="text-xs font-medium text-foreground">Instance ID</span>
377− <Input
378− className="bg-background"
379− placeholder={`${driver}_work`}
380− value={instanceId}
381− onChange={(event) => {
382− setInstanceIdDirty(true);
383− setInstanceId(event.target.value);
384− }}
385− aria-invalid={showInstanceIdError}
429+ {driverSettingsFields.length > 0 ? (
430+ <div className={cn("grid gap-4", wizardStep !== 2 && "hidden")}>
431+ <ProviderSettingsForm
432+ definition={driverOption}
433+ value={configDraft}
434+ idPrefix={`add-provider-${driver}`}
435+ variant="dialog"
436+ onChange={setConfigDraft}
386437 />
387− {showInstanceIdError ? (
388− <span className="text-[11px] text-destructive">{instanceIdError}</span>
389− ) : (
390− <span className="text-[11px] text-muted-foreground">
391− Routing key used by threads and sessions. Letters, digits, '-', or '_'.
392− </span>
393− )}
394− </label>
395−
396− <div className={cn("grid gap-2", wizardStep !== 1 && "hidden")}>
397− <span className="text-xs font-medium text-foreground">Accent color</span>
398− <div className="flex min-w-0 flex-wrap items-center gap-2">
399− <input
400− type="color"
401− value={normalizeProviderAccentColor(accentColor) ?? PROVIDER_ACCENT_SWATCHES[0]}
402− onChange={(event) => setAccentColor(event.target.value)}
403− aria-label="Provider instance accent color"
404− className="h-8 w-10 cursor-pointer rounded-xl border border-input bg-background p-0.5"
405− />
406− <div className="flex flex-wrap gap-1.5">
407− {PROVIDER_ACCENT_SWATCHES.map((swatch) => {
408− const selected = accentColor.toLowerCase() === swatch;
409− return (
410− <button
411− key={swatch}
412− type="button"
413− className={cn(
414− "size-6 cursor-pointer rounded-full border transition",
415− selected
416− ? "scale-110 border-foreground ring-2 ring-ring ring-offset-1 ring-offset-background"
417− : "border-black/10 hover:scale-105 dark:border-white/20",
418− )}
419− style={{ backgroundColor: swatch }}
420− onClick={() => setAccentColor(swatch)}
421− aria-label={`Use ${swatch} accent`}
422− />
423− );
424− })}
425− </div>
426− {accentColor ? (
427− <Button
428− type="button"
429− size="sm"
430− variant="ghost"
431− className="h-7 px-2 text-xs text-muted-foreground"
432− onClick={() => setAccentColor("")}
433− >
434− Clear
435− </Button>
436− ) : null}
437− </div>
438− <span className="text-[11px] text-muted-foreground">
439− Optional marker shown in the picker.
440− </span>
441438 </div>
439+ ) : wizardStep === 2 ? (
440+ <div className="grid gap-2">
441+ <p className="text-sm text-muted-foreground">
442+ This driver has no required configuration. You can add the instance now.
443+ </p>
444+ </div>
445+ ) : null}
446+ </AnimatedHeight>
447+ </div>
442448
443− {driverSettingsFields.length > 0 ? (
444− <div className={cn("grid gap-4", wizardStep !== 2 && "hidden")}>
445− <ProviderSettingsForm
446− definition={driverOption}
447− value={configDraft}
448− idPrefix={`add-provider-${driver}`}
449− variant="dialog"
450− onChange={setConfigDraft}
451− />
452− </div>
453− ) : wizardStep === 2 ? (
454− <div className="grid gap-2">
455− <p className="text-sm text-muted-foreground">
456− This driver has no required configuration. You can add the instance now.
457− </p>
458− </div>
459− ) : null}
460− </AnimatedHeight>
461− </div>
462−
463− <DialogFooter className="border-t bg-background">
464− <Button
465− variant="outline"
466− size="sm"
467− onClick={() => {
468− if (wizardStep === 0) {
469− onOpenChange(false);
470− return;
471− }
472− setWizardStep((step) => Math.max(0, step - 1));
473− }}
474− >
475− {wizardStep === 0 ? "Cancel" : "Back"}
449+ <DialogFooter className="border-t bg-background">
450+ <Button
451+ variant="outline"
452+ size="sm"
453+ onClick={() => {
454+ if (wizardStep === 0) {
455+ onOpenChange(false);
456+ return;
457+ }
458+ setWizardStep((step) => Math.max(0, step - 1));
459+ }}
460+ >
461+ {wizardStep === 0 ? "Cancel" : "Back"}
462+ </Button>
463+ {wizardStep < wizardSteps.length - 1 ? (
464+ <Button size="sm" onClick={() => setWizardStep((step) => Math.min(2, step + 1))}>
465+ Next
476466 </Button>
477− {wizardStep < wizardSteps.length - 1 ? (
478− <Button size="sm" onClick={() => setWizardStep((step) => Math.min(2, step + 1))}>
479− Next
480− </Button>
481− ) : (
482− <Button size="sm" onClick={handleSave}>
483− Add instance
484− </Button>
485− )}
486− </DialogFooter>
487− </div>
488− </DialogPopup>
489− </Dialog>
467+ ) : (
468+ <Button size="sm" onClick={handleSave}>
469+ Add instance
470+ </Button>
471+ )}
472+ </DialogFooter>
473+ </div>
474+ </DialogPopup>
490475 );
491476 }