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
2 2
3import { CheckIcon } from "lucide-react";3import { CheckIcon } from "lucide-react";
4import { Radio as RadioPrimitive } from "@base-ui/react/radio";4import { Radio as RadioPrimitive } from "@base-ui/react/radio";
5import { useCallback, useEffect, useMemo, useState } from "react";5import { useCallback, useMemo, useState } from "react";
6import {6import {
7 ProviderInstanceId,7 ProviderInstanceId,
8 ProviderDriverKind,8 ProviderDriverKind,
@@ -114,6 +114,18 @@ interface AddProviderInstanceDialogProps { · 105 unmodified lines
114}114}
115 115
116export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderInstanceDialogProps) {116export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderInstanceDialogProps) {
117 return (
118 <Dialog open={open} onOpenChange={onOpenChange}>
119 {open ? <AddProviderInstanceDialogContent onOpenChange={onOpenChange} /> : null}
120 </Dialog>
121 );
122}
123
124function AddProviderInstanceDialogContent({
125 onOpenChange,
126}: {
127 readonly onOpenChange: (open: boolean) => void;
128}) {
117 const settings = useSettings();129 const settings = useSettings();
118 const { updateSettings } = useUpdateSettings();130 const { updateSettings } = useUpdateSettings();
119 131
@@ -121,8 +133,7 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns · 1 unmodified line
121 const [driver, setDriver] = useState<ProviderDriverKind>(DEFAULT_DRIVER_KIND);133 const [driver, setDriver] = useState<ProviderDriverKind>(DEFAULT_DRIVER_KIND);
122 const [label, setLabel] = useState("");134 const [label, setLabel] = useState("");
123 const [accentColor, setAccentColor] = useState<string>("");135 const [accentColor, setAccentColor] = useState<string>("");
124 const [instanceId, setInstanceId] = useState("");136 const [manualInstanceId, setManualInstanceId] = useState<string | null>(null);
125 const [instanceIdDirty, setInstanceIdDirty] = useState(false);
126 // Driver-specific config drafts keyed by driver so toggling between drivers137 // Driver-specific config drafts keyed by driver so toggling between drivers
127 // during the same dialog session does not lose in-progress input.138 // during the same dialog session does not lose in-progress input.
128 const [configByDriver, setConfigByDriver] = useState<Record<string, Record<string, unknown>>>({});139 const [configByDriver, setConfigByDriver] = useState<Record<string, Record<string, unknown>>>({});
@@ -135,32 +146,12 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns · 6 unmodified lines
135 [settings.providerInstances],146 [settings.providerInstances],
136 );147 );
137 148
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
159 const driverOption = DRIVER_OPTION_BY_VALUE[driver] ?? DEFAULT_DRIVER_OPTION;149 const driverOption = DRIVER_OPTION_BY_VALUE[driver] ?? DEFAULT_DRIVER_OPTION;
160 const driverSettingsFields = useMemo(150 const driverSettingsFields = useMemo(
161 () => deriveProviderSettingsFields(driverOption),151 () => deriveProviderSettingsFields(driverOption),
162 [driverOption],152 [driverOption],
163 );153 );
154 const instanceId = manualInstanceId ?? deriveInstanceId(driver, label);
164 const instanceIdError = validateInstanceId(instanceId, existingIds);155 const instanceIdError = validateInstanceId(instanceId, existingIds);
165 const showInstanceIdError = hasAttemptedSubmit && instanceIdError !== null;156 const showInstanceIdError = hasAttemptedSubmit && instanceIdError !== null;
166 const previewLabel = label.trim() || `${driverOption.label} Workspace`;157 const previewLabel = label.trim() || `${driverOption.label} Workspace`;
@@ -236,256 +227,250 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns · 69 unmodified lines
236 ]);227 ]);
237 228
238 return (229 return (
239 <Dialog open={open} onOpenChange={onOpenChange}>230 <DialogPopup className="max-w-xl overflow-hidden">
240 <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">
241 <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">
242 <DialogHeader className="border-b border-border/70 bg-background">233 <DialogTitle>Add provider instance</DialogTitle>
243 <DialogTitle>Add provider instance</DialogTitle>234 <DialogDescription>
244 <DialogDescription>235 Configure an additional provider instance — for example, a second Codex install pointed
245 Configure an additional provider instance — for example, a second Codex install236 at a different workspace.
246 pointed at a different workspace.237 </DialogDescription>
247 </DialogDescription>238 <div className="grid grid-cols-3 gap-2">
248 <div className="grid grid-cols-3 gap-2">239 {wizardSteps.map((step, index) => (
249 {wizardSteps.map((step, index) => (240 <button
250 <button241 key={step}
251 key={step}242 type="button"
252 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
253 className={cn(254 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 "row-span-2 mt-0.5 grid size-4 place-items-center rounded-full border",
255 index === wizardStep256 index < wizardStep
256 ? "border-primary bg-primary/10 ring-1 ring-primary/25"257 ? "border-primary bg-primary text-primary-foreground"
257 : index < wizardStep258 : index === wizardStep
258 ? "border-border bg-background"259 ? "border-primary bg-background"
259 : "border-border bg-muted/40",260 : "border-muted-foreground/35 bg-background",
260 )}261 )}
261 onClick={() => setWizardStep(index)}262 aria-hidden
262 >263 >
263 <span264 {index < wizardStep ? <CheckIcon className="size-3" /> : null}
264 className={cn(265 </span>
265 "row-span-2 mt-0.5 grid size-4 place-items-center rounded-full border",266 <span className="text-[10px] font-medium uppercase text-muted-foreground">
266 index < wizardStep267 Step {index + 1}
267 ? "border-primary bg-primary text-primary-foreground"268 </span>
268 : index === wizardStep269 <span className="truncate text-xs font-semibold text-foreground">
269 ? "border-primary bg-background"270 {step}
270 : "border-muted-foreground/35 bg-background",271 {index < wizardStep && wizardStepSummaries[index]
271 )}272 ? `: ${wizardStepSummaries[index]}`
272 aria-hidden273 : ""}
273 >274 </span>
274 {index < wizardStep ? <CheckIcon className="size-3" /> : null}275 </button>
275 </span>276 ))}
276 <span className="text-[10px] font-medium uppercase text-muted-foreground">277 </div>
277 Step {index + 1}278 </DialogHeader>
278 </span>279
279 <span className="truncate text-xs font-semibold text-foreground">280 <div
280 {step}281 data-slot="dialog-panel"
281 {index < wizardStep && wizardStepSummaries[index]282 className="space-y-4 border-b border-border/70 bg-muted/20 px-6 py-5"
282 ? `: ${wizardStepSummaries[index]}`283 >
283 : ""}284 <AnimatedHeight>
284 </span>285 <div className={cn("grid gap-2", wizardStep !== 0 && "hidden")}>
285 </button>286 <span id="add-instance-driver-label" className="text-xs font-medium text-foreground">
286 ))}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>
287 </div>347 </div>
288 </DialogHeader>
289 348
290 <div349 <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}>
291 data-slot="dialog-panel"350 <span className="text-xs font-medium text-foreground">Label</span>
292 className="space-y-4 border-b border-border/70 bg-muted/20 px-6 py-5"351 <Input
293 >352 className="bg-background"
294 <AnimatedHeight>353 placeholder="e.g. Work"
295 <div className={cn("grid gap-2", wizardStep !== 0 && "hidden")}>354 value={label}
296 <span355 onChange={(event) => setLabel(event.target.value)}
297 id="add-instance-driver-label"356 />
298 className="text-xs font-medium text-foreground"357 <span className="text-[11px] text-muted-foreground">
299 >358 Shown in the provider list. Optional.
300 Driver359 </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 '_'.
301 </span>378 </span>
302 <RadioGroup379 )}
303 value={driver}380 </label>
304 onValueChange={(value) => setDriver(ProviderDriverKind.make(value))}381
305 aria-labelledby="add-instance-driver-label"382 <div className={cn("grid gap-2", wizardStep !== 1 && "hidden")}>
306 className="grid grid-cols-2 gap-2.5"383 <span className="text-xs font-medium text-foreground">Accent color</span>
307 >384 <div className="flex min-w-0 flex-wrap items-center gap-2">
308 {DRIVER_OPTIONS.map((option) => {385 <input
309 const IconComponent = option.icon;386 type="color"
310 const isSelected = option.value === driver;387 value={normalizeProviderAccentColor(accentColor) ?? PROVIDER_ACCENT_SWATCHES[0]}
311 return (388 onChange={(event) => setAccentColor(event.target.value)}
312 <RadioPrimitive.Root389 aria-label="Provider instance accent color"
313 key={option.value}390 className="h-8 w-10 cursor-pointer rounded-xl border border-input bg-background p-0.5"
314 value={option.value}391 />
315 className={cn(392 <div className="flex flex-wrap gap-1.5">
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]",393 {PROVIDER_ACCENT_SWATCHES.map((swatch) => {
317 "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",394 const selected = accentColor.toLowerCase() === swatch;
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;
337 return (395 return (
338 <RadioPrimitive.Root396 <button
339 key={option.value}397 key={swatch}
340 value={option.value}398 type="button"
341 disabled
342 className={cn(399 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",
344 )}404 )}
345 >405 style={{ backgroundColor: swatch }}
346 <IconComponent406 onClick={() => setAccentColor(swatch)}
347 className="size-5 shrink-0 text-muted-foreground"407 aria-label={`Use ${swatch} accent`}
348 aria-hidden408 />
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>
357 );409 );
358 })}410 })}
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}
360 </div>423 </div>
424 <span className="text-[11px] text-muted-foreground">
425 Optional marker shown in the picker.
426 </span>
427 </div>
361 428
362 <label className={cn("grid gap-2", wizardStep !== 1 && "hidden")}>429 {driverSettingsFields.length > 0 ? (
363 <span className="text-xs font-medium text-foreground">Label</span>430 <div className={cn("grid gap-4", wizardStep !== 2 && "hidden")}>
364 <Input431 <ProviderSettingsForm
365 className="bg-background"432 definition={driverOption}
366 placeholder="e.g. Work"433 value={configDraft}
367 value={label}434 idPrefix={`add-provider-${driver}`}
368 onChange={(event) => setLabel(event.target.value)}435 variant="dialog"
369 />436 onChange={setConfigDraft}
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}
386 />437 />
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>
441 </div>438 </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>
442 448
443 {driverSettingsFields.length > 0 ? (449 <DialogFooter className="border-t bg-background">
444 <div className={cn("grid gap-4", wizardStep !== 2 && "hidden")}>450 <Button
445 <ProviderSettingsForm451 variant="outline"
446 definition={driverOption}452 size="sm"
447 value={configDraft}453 onClick={() => {
448 idPrefix={`add-provider-${driver}`}454 if (wizardStep === 0) {
449 variant="dialog"455 onOpenChange(false);
450 onChange={setConfigDraft}456 return;
451 />457 }
452 </div>458 setWizardStep((step) => Math.max(0, step - 1));
453 ) : wizardStep === 2 ? (459 }}
454 <div className="grid gap-2">460 >
455 <p className="text-sm text-muted-foreground">461 {wizardStep === 0 ? "Cancel" : "Back"}
456 This driver has no required configuration. You can add the instance now.462 </Button>
457 </p>463 {wizardStep < wizardSteps.length - 1 ? (
458 </div>464 <Button size="sm" onClick={() => setWizardStep((step) => Math.min(2, step + 1))}>
459 ) : null}465 Next
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"}
476 </Button>466 </Button>
477 {wizardStep < wizardSteps.length - 1 ? (467 ) : (
478 <Button size="sm" onClick={() => setWizardStep((step) => Math.min(2, step + 1))}>468 <Button size="sm" onClick={handleSave}>
479 Next469 Add instance
480 </Button>470 </Button>
481 ) : (471 )}
482 <Button size="sm" onClick={handleSave}>472 </DialogFooter>
483 Add instance473 </div>
484 </Button>474 </DialogPopup>
485 )}
486 </DialogFooter>
487 </div>
488 </DialogPopup>
489 </Dialog>
490 );475 );
491}476}