feat(ui): add target controls to cliproxy variants

- add default target selectors in create/edit dialogs

- include target in variant create/update payloads

- display target badge column in variants table
This commit is contained in:
Tam Nhu Tran
2026-02-25 15:53:40 +07:00
parent db38ccc117
commit 543ec5f050
3 changed files with 67 additions and 0 deletions
@@ -26,6 +26,7 @@ const singleProviderSchema = z.object({
provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }),
model: z.string().optional(),
account: z.string().optional(),
target: z.enum(['claude', 'droid']).default('claude'),
});
const compositeSchema = z.object({
@@ -34,6 +35,7 @@ const compositeSchema = z.object({
.min(1, 'Name is required')
.regex(/^[a-zA-Z][a-zA-Z0-9._-]*$/, 'Invalid variant name'),
default_tier: z.enum(['opus', 'sonnet', 'haiku'], { message: 'Default tier is required' }),
target: z.enum(['claude', 'droid']).default('claude'),
tiers: z.object({
opus: z.object({
provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }),
@@ -74,12 +76,14 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
const singleForm = useForm<SingleProviderFormData>({
resolver: zodResolver(singleProviderSchema),
defaultValues: { target: 'claude' },
});
const compositeForm = useForm<CompositeFormData>({
resolver: zodResolver(compositeSchema),
defaultValues: {
default_tier: 'opus',
target: 'claude',
tiers: {
opus: { provider: 'gemini', model: '' },
sonnet: { provider: 'gemini', model: '' },
@@ -107,6 +111,7 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
await createMutation.mutateAsync({
name: data.name,
provider: data.tiers[data.default_tier].provider,
target: data.target,
type: 'composite',
default_tier: data.default_tier,
tiers: data.tiers,
@@ -200,6 +205,18 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
<Input id="model" {...singleForm.register('model')} placeholder="gemini-2.5-pro" />
</div>
<div>
<Label htmlFor="target">Default Target</Label>
<select
id="target"
{...singleForm.register('target')}
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<option value="claude">Claude Code</option>
<option value="droid">Factory Droid</option>
</select>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="outline" onClick={onClose}>
Cancel
@@ -288,6 +305,18 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
</select>
</div>
<div>
<Label htmlFor="composite-target">Default Target</Label>
<select
id="composite-target"
{...compositeForm.register('target')}
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<option value="claude">Claude Code</option>
<option value="droid">Factory Droid</option>
</select>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="outline" onClick={onClose}>
Cancel
@@ -20,10 +20,12 @@ const singleProviderSchema = z.object({
provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }),
model: z.string().optional(),
account: z.string().optional(),
target: z.enum(['claude', 'droid']).default('claude'),
});
const compositeSchema = z.object({
default_tier: z.enum(['opus', 'sonnet', 'haiku'], { message: 'Default tier is required' }),
target: z.enum(['claude', 'droid']).default('claude'),
tiers: z.object({
opus: z.object({
provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }),
@@ -81,6 +83,7 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
});
compositeForm.reset({
default_tier: variant.default_tier,
target: variant.target || 'claude',
tiers: {
opus: mapTier(variant.tiers.opus),
sonnet: mapTier(variant.tiers.sonnet),
@@ -92,6 +95,7 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
provider: variant.provider,
model: variant.model ?? undefined,
account: variant.account ?? undefined,
target: variant.target || 'claude',
});
}
}, [variant, isComposite, singleForm, compositeForm]);
@@ -117,6 +121,7 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
name: variant.name,
data: {
default_tier: data.default_tier,
target: data.target,
tiers: data.tiers,
},
});
@@ -202,6 +207,18 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
</select>
</div>
<div>
<Label htmlFor="edit-composite-target">Default Target</Label>
<select
id="edit-composite-target"
{...compositeForm.register('target')}
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<option value="claude">Claude Code</option>
<option value="droid">Factory Droid</option>
</select>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
@@ -242,6 +259,18 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
/>
</div>
<div>
<Label htmlFor="edit-target">Default Target</Label>
<select
id="edit-target"
{...singleForm.register('target')}
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<option value="claude">Claude Code</option>
<option value="droid">Factory Droid</option>
</select>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
@@ -69,6 +69,15 @@ export function CliproxyTable({ data }: CliproxyTableProps) {
);
},
},
{
accessorKey: 'target',
header: 'Target',
cell: ({ row }) => (
<Badge variant="outline" className="text-[10px] h-5 px-1.5 uppercase">
{row.original.target || 'claude'}
</Badge>
),
},
{
accessorKey: 'settings',
header: 'Settings Path',