mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
feat(ui): support editing profile default target
- add target selector in profile editor header - call profile update API to persist target changes - update info panel usage snippets for target-aware commands
This commit is contained in:
@@ -20,9 +20,11 @@ import { isOpenRouterProfile, extractTierMapping, applyTierMapping } from './uti
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Settings, SettingsResponse } from './types';
|
||||
import type { CliTarget } from '@/lib/api-client';
|
||||
|
||||
interface FriendlyUISectionProps {
|
||||
profileName: string;
|
||||
target: CliTarget;
|
||||
data: SettingsResponse | undefined;
|
||||
currentSettings: Settings | undefined;
|
||||
newEnvKey: string;
|
||||
@@ -36,6 +38,7 @@ interface FriendlyUISectionProps {
|
||||
|
||||
export function FriendlyUISection({
|
||||
profileName,
|
||||
target,
|
||||
data,
|
||||
currentSettings,
|
||||
newEnvKey,
|
||||
@@ -263,7 +266,7 @@ export function FriendlyUISection({
|
||||
value="info"
|
||||
className="h-full mt-0 border-0 p-0 data-[state=inactive]:hidden"
|
||||
>
|
||||
<InfoSection profileName={profileName} data={data} />
|
||||
<InfoSection profileName={profileName} target={target} data={data} />
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
@@ -5,19 +5,30 @@
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Save, Loader2, Trash2, RefreshCw } from 'lucide-react';
|
||||
import { OpenRouterBadge } from '@/components/profiles/openrouter-badge';
|
||||
import { isOpenRouterProfile } from './utils';
|
||||
import type { Settings } from './types';
|
||||
import type { CliTarget } from '@/lib/api-client';
|
||||
|
||||
interface HeaderSectionProps {
|
||||
profileName: string;
|
||||
target: CliTarget;
|
||||
data: { path?: string; mtime: number } | undefined;
|
||||
settings?: Settings;
|
||||
isLoading: boolean;
|
||||
isSaving: boolean;
|
||||
isTargetSaving: boolean;
|
||||
hasChanges: boolean;
|
||||
isRawJsonValid: boolean;
|
||||
onTargetChange: (target: CliTarget) => void;
|
||||
onRefresh: () => void;
|
||||
onDelete?: () => void;
|
||||
onSave: () => void;
|
||||
@@ -25,12 +36,15 @@ interface HeaderSectionProps {
|
||||
|
||||
export function HeaderSection({
|
||||
profileName,
|
||||
target,
|
||||
data,
|
||||
settings,
|
||||
isLoading,
|
||||
isSaving,
|
||||
isTargetSaving,
|
||||
hasChanges,
|
||||
isRawJsonValid,
|
||||
onTargetChange,
|
||||
onRefresh,
|
||||
onDelete,
|
||||
onSave,
|
||||
@@ -52,6 +66,19 @@ export function HeaderSection({
|
||||
Last modified: {new Date(data.mtime).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">Default target:</span>
|
||||
<Select value={target} onValueChange={(value) => onTargetChange(value as CliTarget)}>
|
||||
<SelectTrigger className="h-7 w-[170px] text-xs" disabled={isTargetSaving}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="claude">Claude Code</SelectItem>
|
||||
<SelectItem value="droid">Factory Droid</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{isTargetSaving && <Loader2 className="w-3.5 h-3.5 animate-spin text-muted-foreground" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={onRefresh} disabled={isLoading}>
|
||||
|
||||
@@ -15,8 +15,14 @@ import { HeaderSection } from './header-section';
|
||||
import { FriendlyUISection } from './friendly-ui-section';
|
||||
import { RawEditorSection } from './raw-editor-section';
|
||||
import type { ProfileEditorProps, Settings, SettingsResponse } from './types';
|
||||
import type { CliTarget } from '@/lib/api-client';
|
||||
|
||||
export function ProfileEditor({ profileName, onDelete, onHasChangesUpdate }: ProfileEditorProps) {
|
||||
export function ProfileEditor({
|
||||
profileName,
|
||||
profileTarget,
|
||||
onDelete,
|
||||
onHasChangesUpdate,
|
||||
}: ProfileEditorProps) {
|
||||
const [localEdits, setLocalEdits] = useState<Record<string, string>>({});
|
||||
const [conflictDialog, setConflictDialog] = useState(false);
|
||||
const [rawJsonEdits, setRawJsonEdits] = useState<string | null>(null);
|
||||
@@ -145,6 +151,40 @@ export function ProfileEditor({ profileName, onDelete, onHasChangesUpdate }: Pro
|
||||
},
|
||||
});
|
||||
|
||||
const targetMutation = useMutation({
|
||||
mutationFn: async (target: CliTarget) => {
|
||||
const response = await fetch(`/api/profiles/${profileName}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ target }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = 'Failed to update target';
|
||||
try {
|
||||
const payload = (await response.json()) as { error?: string };
|
||||
if (payload.error) {
|
||||
errorMessage = payload.error;
|
||||
}
|
||||
} catch {
|
||||
// Keep fallback error message.
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['profiles'] });
|
||||
toast.success('Default target updated');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
const resolvedTarget: CliTarget = profileTarget || 'claude';
|
||||
|
||||
const handleConflictResolve = async (overwrite: boolean) => {
|
||||
setConflictDialog(false);
|
||||
if (overwrite) {
|
||||
@@ -160,12 +200,18 @@ export function ProfileEditor({ profileName, onDelete, onHasChangesUpdate }: Pro
|
||||
<div key={profileName} className="flex-1 flex flex-col overflow-hidden">
|
||||
<HeaderSection
|
||||
profileName={profileName}
|
||||
target={resolvedTarget}
|
||||
data={data}
|
||||
settings={currentSettings}
|
||||
isLoading={isLoading}
|
||||
isSaving={saveMutation.isPending}
|
||||
isTargetSaving={targetMutation.isPending}
|
||||
hasChanges={computedHasChanges}
|
||||
isRawJsonValid={computedIsRawJsonValid}
|
||||
onTargetChange={(target) => {
|
||||
if (target === resolvedTarget) return;
|
||||
targetMutation.mutate(target);
|
||||
}}
|
||||
onRefresh={() => refetch()}
|
||||
onDelete={onDelete}
|
||||
onSave={() => saveMutation.mutate()}
|
||||
@@ -191,6 +237,7 @@ export function ProfileEditor({ profileName, onDelete, onHasChangesUpdate }: Pro
|
||||
<div className="flex flex-col overflow-hidden bg-muted/5 min-w-0">
|
||||
<FriendlyUISection
|
||||
profileName={profileName}
|
||||
target={resolvedTarget}
|
||||
data={data}
|
||||
currentSettings={currentSettings}
|
||||
newEnvKey={newEnvKey}
|
||||
|
||||
@@ -8,13 +8,17 @@ import { Label } from '@/components/ui/label';
|
||||
import { CopyButton } from '@/components/ui/copy-button';
|
||||
import { Info } from 'lucide-react';
|
||||
import type { SettingsResponse } from './types';
|
||||
import type { CliTarget } from '@/lib/api-client';
|
||||
|
||||
interface InfoSectionProps {
|
||||
profileName: string;
|
||||
target: CliTarget;
|
||||
data: SettingsResponse | undefined;
|
||||
}
|
||||
|
||||
export function InfoSection({ profileName, data }: InfoSectionProps) {
|
||||
export function InfoSection({ profileName, target, data }: InfoSectionProps) {
|
||||
const isDroidTarget = target === 'droid';
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-4 space-y-6">
|
||||
@@ -44,6 +48,10 @@ export function InfoSection({ profileName, data }: InfoSectionProps) {
|
||||
<span className="font-medium text-muted-foreground">Last Modified</span>
|
||||
<span className="text-xs">{new Date(data.mtime).toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
|
||||
<span className="font-medium text-muted-foreground">Default Target</span>
|
||||
<span className="font-mono">{target}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -62,6 +70,42 @@ export function InfoSection({ profileName, data }: InfoSectionProps) {
|
||||
<CopyButton value={`ccs ${profileName} "prompt"`} size="icon" className="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{isDroidTarget ? 'Droid alias (explicit)' : 'Run on Droid'}
|
||||
</Label>
|
||||
<div className="mt-1 flex gap-2">
|
||||
<code className="flex-1 px-2 py-1.5 bg-muted rounded text-xs font-mono truncate">
|
||||
{isDroidTarget
|
||||
? `ccsd ${profileName} "prompt"`
|
||||
: `ccs ${profileName} --target droid "prompt"`}
|
||||
</code>
|
||||
<CopyButton
|
||||
value={
|
||||
isDroidTarget
|
||||
? `ccsd ${profileName} "prompt"`
|
||||
: `ccs ${profileName} --target droid "prompt"`
|
||||
}
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{isDroidTarget ? 'Override to Claude' : 'Override to Claude (explicit)'}
|
||||
</Label>
|
||||
<div className="mt-1 flex gap-2">
|
||||
<code className="flex-1 px-2 py-1.5 bg-muted rounded text-xs font-mono truncate">
|
||||
ccs {profileName} --target claude "prompt"
|
||||
</code>
|
||||
<CopyButton
|
||||
value={`ccs ${profileName} --target claude "prompt"`}
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Set as default</Label>
|
||||
<div className="mt-1 flex gap-2">
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Types for Profile Editor
|
||||
*/
|
||||
|
||||
import type { CliTarget } from '@/lib/api-client';
|
||||
|
||||
export interface Settings {
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
@@ -15,6 +17,7 @@ export interface SettingsResponse {
|
||||
|
||||
export interface ProfileEditorProps {
|
||||
profileName: string;
|
||||
profileTarget?: CliTarget;
|
||||
onDelete?: () => void;
|
||||
onHasChangesUpdate?: (hasChanges: boolean) => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user