mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-08 20:21:46 +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 { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { Settings, SettingsResponse } from './types';
|
import type { Settings, SettingsResponse } from './types';
|
||||||
|
import type { CliTarget } from '@/lib/api-client';
|
||||||
|
|
||||||
interface FriendlyUISectionProps {
|
interface FriendlyUISectionProps {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
target: CliTarget;
|
||||||
data: SettingsResponse | undefined;
|
data: SettingsResponse | undefined;
|
||||||
currentSettings: Settings | undefined;
|
currentSettings: Settings | undefined;
|
||||||
newEnvKey: string;
|
newEnvKey: string;
|
||||||
@@ -36,6 +38,7 @@ interface FriendlyUISectionProps {
|
|||||||
|
|
||||||
export function FriendlyUISection({
|
export function FriendlyUISection({
|
||||||
profileName,
|
profileName,
|
||||||
|
target,
|
||||||
data,
|
data,
|
||||||
currentSettings,
|
currentSettings,
|
||||||
newEnvKey,
|
newEnvKey,
|
||||||
@@ -263,7 +266,7 @@ export function FriendlyUISection({
|
|||||||
value="info"
|
value="info"
|
||||||
className="h-full mt-0 border-0 p-0 data-[state=inactive]:hidden"
|
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>
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -5,19 +5,30 @@
|
|||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
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 { Save, Loader2, Trash2, RefreshCw } from 'lucide-react';
|
||||||
import { OpenRouterBadge } from '@/components/profiles/openrouter-badge';
|
import { OpenRouterBadge } from '@/components/profiles/openrouter-badge';
|
||||||
import { isOpenRouterProfile } from './utils';
|
import { isOpenRouterProfile } from './utils';
|
||||||
import type { Settings } from './types';
|
import type { Settings } from './types';
|
||||||
|
import type { CliTarget } from '@/lib/api-client';
|
||||||
|
|
||||||
interface HeaderSectionProps {
|
interface HeaderSectionProps {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
target: CliTarget;
|
||||||
data: { path?: string; mtime: number } | undefined;
|
data: { path?: string; mtime: number } | undefined;
|
||||||
settings?: Settings;
|
settings?: Settings;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
|
isTargetSaving: boolean;
|
||||||
hasChanges: boolean;
|
hasChanges: boolean;
|
||||||
isRawJsonValid: boolean;
|
isRawJsonValid: boolean;
|
||||||
|
onTargetChange: (target: CliTarget) => void;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
@@ -25,12 +36,15 @@ interface HeaderSectionProps {
|
|||||||
|
|
||||||
export function HeaderSection({
|
export function HeaderSection({
|
||||||
profileName,
|
profileName,
|
||||||
|
target,
|
||||||
data,
|
data,
|
||||||
settings,
|
settings,
|
||||||
isLoading,
|
isLoading,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
isTargetSaving,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
isRawJsonValid,
|
isRawJsonValid,
|
||||||
|
onTargetChange,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onDelete,
|
onDelete,
|
||||||
onSave,
|
onSave,
|
||||||
@@ -52,6 +66,19 @@ export function HeaderSection({
|
|||||||
Last modified: {new Date(data.mtime).toLocaleString()}
|
Last modified: {new Date(data.mtime).toLocaleString()}
|
||||||
</p>
|
</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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="ghost" size="sm" onClick={onRefresh} disabled={isLoading}>
|
<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 { FriendlyUISection } from './friendly-ui-section';
|
||||||
import { RawEditorSection } from './raw-editor-section';
|
import { RawEditorSection } from './raw-editor-section';
|
||||||
import type { ProfileEditorProps, Settings, SettingsResponse } from './types';
|
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 [localEdits, setLocalEdits] = useState<Record<string, string>>({});
|
||||||
const [conflictDialog, setConflictDialog] = useState(false);
|
const [conflictDialog, setConflictDialog] = useState(false);
|
||||||
const [rawJsonEdits, setRawJsonEdits] = useState<string | null>(null);
|
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) => {
|
const handleConflictResolve = async (overwrite: boolean) => {
|
||||||
setConflictDialog(false);
|
setConflictDialog(false);
|
||||||
if (overwrite) {
|
if (overwrite) {
|
||||||
@@ -160,12 +200,18 @@ export function ProfileEditor({ profileName, onDelete, onHasChangesUpdate }: Pro
|
|||||||
<div key={profileName} className="flex-1 flex flex-col overflow-hidden">
|
<div key={profileName} className="flex-1 flex flex-col overflow-hidden">
|
||||||
<HeaderSection
|
<HeaderSection
|
||||||
profileName={profileName}
|
profileName={profileName}
|
||||||
|
target={resolvedTarget}
|
||||||
data={data}
|
data={data}
|
||||||
settings={currentSettings}
|
settings={currentSettings}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isSaving={saveMutation.isPending}
|
isSaving={saveMutation.isPending}
|
||||||
|
isTargetSaving={targetMutation.isPending}
|
||||||
hasChanges={computedHasChanges}
|
hasChanges={computedHasChanges}
|
||||||
isRawJsonValid={computedIsRawJsonValid}
|
isRawJsonValid={computedIsRawJsonValid}
|
||||||
|
onTargetChange={(target) => {
|
||||||
|
if (target === resolvedTarget) return;
|
||||||
|
targetMutation.mutate(target);
|
||||||
|
}}
|
||||||
onRefresh={() => refetch()}
|
onRefresh={() => refetch()}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onSave={() => saveMutation.mutate()}
|
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">
|
<div className="flex flex-col overflow-hidden bg-muted/5 min-w-0">
|
||||||
<FriendlyUISection
|
<FriendlyUISection
|
||||||
profileName={profileName}
|
profileName={profileName}
|
||||||
|
target={resolvedTarget}
|
||||||
data={data}
|
data={data}
|
||||||
currentSettings={currentSettings}
|
currentSettings={currentSettings}
|
||||||
newEnvKey={newEnvKey}
|
newEnvKey={newEnvKey}
|
||||||
|
|||||||
@@ -8,13 +8,17 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { CopyButton } from '@/components/ui/copy-button';
|
import { CopyButton } from '@/components/ui/copy-button';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
import type { SettingsResponse } from './types';
|
import type { SettingsResponse } from './types';
|
||||||
|
import type { CliTarget } from '@/lib/api-client';
|
||||||
|
|
||||||
interface InfoSectionProps {
|
interface InfoSectionProps {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
target: CliTarget;
|
||||||
data: SettingsResponse | undefined;
|
data: SettingsResponse | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfoSection({ profileName, data }: InfoSectionProps) {
|
export function InfoSection({ profileName, target, data }: InfoSectionProps) {
|
||||||
|
const isDroidTarget = target === 'droid';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
<div className="p-4 space-y-6">
|
<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="font-medium text-muted-foreground">Last Modified</span>
|
||||||
<span className="text-xs">{new Date(data.mtime).toLocaleString()}</span>
|
<span className="text-xs">{new Date(data.mtime).toLocaleString()}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
@@ -62,6 +70,42 @@ export function InfoSection({ profileName, data }: InfoSectionProps) {
|
|||||||
<CopyButton value={`ccs ${profileName} "prompt"`} size="icon" className="h-6 w-6" />
|
<CopyButton value={`ccs ${profileName} "prompt"`} size="icon" className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<div>
|
||||||
<Label className="text-xs text-muted-foreground">Set as default</Label>
|
<Label className="text-xs text-muted-foreground">Set as default</Label>
|
||||||
<div className="mt-1 flex gap-2">
|
<div className="mt-1 flex gap-2">
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
* Types for Profile Editor
|
* Types for Profile Editor
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { CliTarget } from '@/lib/api-client';
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
}
|
}
|
||||||
@@ -15,6 +17,7 @@ export interface SettingsResponse {
|
|||||||
|
|
||||||
export interface ProfileEditorProps {
|
export interface ProfileEditorProps {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profileTarget?: CliTarget;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onHasChangesUpdate?: (hasChanges: boolean) => void;
|
onHasChangesUpdate?: (hasChanges: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user