feat(ui): clarify image-analysis target status

This commit is contained in:
Tam Nhu Tran
2026-04-01 15:32:37 -04:00
parent 3b61673ad2
commit 0246e327fe
10 changed files with 495 additions and 84 deletions
@@ -30,7 +30,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']),
target: z.enum(['claude', 'droid', 'codex']),
});
const compositeSchema = z.object({
@@ -39,7 +39,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']),
target: z.enum(['claude', 'droid', 'codex']),
tiers: z.object({
opus: z.object({
provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }),
@@ -249,6 +249,7 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
>
<option value="claude">{t('cliproxyDialog.claudeCode')}</option>
<option value="droid">{t('cliproxyDialog.factoryDroid')}</option>
<option value="codex">Codex CLI</option>
</select>
</div>
@@ -353,6 +354,7 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
>
<option value="claude">{t('cliproxyDialog.claudeCode')}</option>
<option value="droid">{t('cliproxyDialog.factoryDroid')}</option>
<option value="codex">Codex CLI</option>
</select>
</div>
@@ -23,12 +23,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']),
target: z.enum(['claude', 'droid', 'codex']),
});
const compositeSchema = z.object({
default_tier: z.enum(['opus', 'sonnet', 'haiku'], { message: 'Default tier is required' }),
target: z.enum(['claude', 'droid']),
target: z.enum(['claude', 'droid', 'codex']),
tiers: z.object({
opus: z.object({
provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }),
@@ -375,6 +375,7 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
>
<option value="claude">{t('cliproxyDialog.claudeCode')}</option>
<option value="droid">{t('cliproxyDialog.factoryDroid')}</option>
<option value="codex">Codex CLI</option>
</select>
</div>
@@ -433,6 +434,7 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
>
<option value="claude">{t('cliproxyDialog.claudeCode')}</option>
<option value="droid">{t('cliproxyDialog.factoryDroid')}</option>
<option value="codex">Codex CLI</option>
</select>
</div>
@@ -85,6 +85,7 @@ export function HeaderSection({
<SelectContent>
<SelectItem value="claude">Claude Code</SelectItem>
<SelectItem value="droid">Factory Droid</SelectItem>
<SelectItem value="codex">Codex CLI</SelectItem>
</SelectContent>
</Select>
{isTargetSaving && <Loader2 className="w-3.5 h-3.5 animate-spin text-muted-foreground" />}
@@ -1,16 +1,64 @@
import { AlertTriangle, Image as ImageIcon, Route } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import type { ImageAnalysisStatus } from '@/lib/api-client';
import type { CliTarget, ImageAnalysisStatus } from '@/lib/api-client';
interface ImageAnalysisStatusSectionProps {
status?: ImageAnalysisStatus | null;
target?: CliTarget;
source?: 'saved' | 'editor';
previewState?: 'saved' | 'preview' | 'refreshing' | 'invalid';
}
function getBadge(status: ImageAnalysisStatus | null | undefined) {
const RESOLUTION_SOURCE_LABELS: Record<ImageAnalysisStatus['resolutionSource'], string> = {
'cliproxy-provider': 'Direct provider route',
'cliproxy-variant': 'Variant route',
'cliproxy-composite': 'Composite route',
'copilot-alias': 'Copilot alias',
'cliproxy-bridge': 'Derived from profile API route',
'profile-backend': 'Saved Image Analysis mapping',
'fallback-backend': 'Fallback backend',
disabled: 'Disabled globally',
'unsupported-profile': 'Unsupported profile type',
unresolved: 'No backend mapped',
'missing-model': 'Missing model',
};
const TARGET_LABELS: Record<CliTarget, string> = {
claude: 'Claude Code',
droid: 'Factory Droid',
codex: 'Codex CLI',
};
function getPreviewBadge(
source: 'saved' | 'editor',
previewState: ImageAnalysisStatusSectionProps['previewState']
) {
if (previewState === 'refreshing') {
return { label: 'Refreshing', variant: 'outline' as const };
}
if (source === 'editor') {
return { label: 'Live Preview', variant: 'secondary' as const };
}
return { label: 'Saved', variant: 'outline' as const };
}
function getRuntimeBadge(status: ImageAnalysisStatus | null | undefined, target: CliTarget) {
if (!status) return { label: 'Checking', variant: 'outline' as const };
if (target !== 'claude') {
if (status.status === 'disabled') return { label: 'Disabled', variant: 'outline' as const };
if (status.status === 'hook-missing')
return { label: 'Setup needed', variant: 'destructive' as const };
if (status.authReadiness === 'missing')
return { label: 'Needs auth', variant: 'destructive' as const };
if (status.proxyReadiness === 'unavailable')
return { label: 'Needs proxy', variant: 'destructive' as const };
if (status.effectiveRuntimeMode === 'native-read' && status.backendId) {
return { label: 'Saved fallback', variant: 'outline' as const };
}
return { label: 'Target bypassed', variant: 'outline' as const };
}
if (status.status === 'disabled') return { label: 'Disabled', variant: 'outline' as const };
if (status.status === 'hook-missing')
return { label: 'Setup needed', variant: 'destructive' as const };
@@ -25,10 +73,7 @@ function getBadge(status: ImageAnalysisStatus | null | undefined) {
return { label: 'Starts on launch', variant: 'secondary' as const };
}
if (status.effectiveRuntimeMode === 'cliproxy-image-analysis') {
return {
label: status.resolutionSource === 'profile-backend' ? 'Ready via mapping' : 'Ready',
variant: 'default' as const,
};
return { label: 'CLIProxy Active', variant: 'default' as const };
}
if (
status.authReadiness === 'unknown' ||
@@ -37,47 +82,72 @@ function getBadge(status: ImageAnalysisStatus | null | undefined) {
) {
return { label: 'Needs review', variant: 'outline' as const };
}
if (status.status === 'skipped' && status.reason?.includes('native file access')) {
return { label: 'Native Claude', variant: 'secondary' as const };
}
return { label: 'Native Read', variant: 'outline' as const };
return { label: 'Native Access', variant: 'outline' as const };
}
function getSummary(status: ImageAnalysisStatus): string {
function getSummary(status: ImageAnalysisStatus, target: CliTarget): string {
const backendName = status.backendDisplayName || status.backendId || 'this backend';
const currentTarget = TARGET_LABELS[target];
if (target !== 'claude') {
if (!status.backendId) {
return (
status.reason ||
`Current default target: ${currentTarget}. This launch path bypasses the Claude Read hook, and no Claude-side Image Analysis backend is currently mapped.`
);
}
if (status.status === 'disabled') {
return `Current default target: ${currentTarget}. This launch path bypasses the Claude Read hook, and saved Claude-side Image Analysis is disabled for this profile.`;
}
if (status.status === 'hook-missing') {
return `Current default target: ${currentTarget}. This launch path bypasses the Claude Read hook, and the saved Claude-side setup for ${backendName} still falls back to native file access because ${status.reason || 'the profile hook is not fully installed yet.'}`;
}
if (status.effectiveRuntimeMode === 'native-read') {
return `Current default target: ${currentTarget}. This launch path bypasses the Claude Read hook, and the saved Claude-side setup for ${backendName} still falls back to native file access. ${status.effectiveRuntimeReason || status.reason || `Image Analysis via ${backendName} could not be confirmed.`}`;
}
return `Images and PDFs for this profile are configured to resolve through ${backendName} when the profile runs on Claude Code.`;
}
if (status.status === 'disabled') {
return "Disabled globally. This profile uses Claude's built-in file reading because CCS image analysis is turned off.";
return 'Image Analysis is disabled globally, so images and PDFs use built-in file access for this profile.';
}
if (!status.backendId) {
return status.reason || "This profile uses Claude's built-in file reading.";
return status.reason || 'This profile currently uses built-in file access for images and PDFs.';
}
if (status.status === 'hook-missing') {
return `Configured for ${backendName}, but ${status.reason || 'the image-analysis hook is not fully installed yet.'}`;
return `Images and PDFs are configured for ${backendName}, but ${status.reason || 'the profile hook is not fully installed yet.'}`;
}
if (status.effectiveRuntimeMode === 'native-read') {
return `Configured via ${backendName}, but ${status.effectiveRuntimeReason || status.reason || 'runtime readiness could not be confirmed.'}`;
return `This profile currently falls back to native file access. ${status.effectiveRuntimeReason || status.reason || `Image Analysis via ${backendName} could not be confirmed.`}`;
}
if (status.proxyReadiness === 'stopped') {
return `Configured via ${backendName}. Auth is ready and CCS will start the local CLIProxy runtime on launch, so image and PDF reads still use CLIProxy.`;
return `Images and PDFs for this profile resolve through ${backendName}. Auth is ready and CCS will start the local CLIProxy runtime when needed.`;
}
if (status.resolutionSource === 'profile-backend') {
return `Configured via saved ${backendName} mapping. Auth and runtime are ready, so image and PDF reads use CLIProxy.`;
return `Images and PDFs for this profile resolve through ${backendName} via a saved Image Analysis mapping.`;
}
if (status.status === 'attention' && status.reason) {
return `Configured via ${backendName}. Image and PDF reads use CLIProxy, but ${status.reason}`;
return `Images and PDFs for this profile resolve through ${backendName} via CLIProxy, but ${status.reason}`;
}
return `Configured via ${backendName}. Image and PDF reads use CLIProxy for this profile.`;
return `Images and PDFs for this profile resolve through ${backendName} via CLIProxy.`;
}
function getRuntimeLine(status: ImageAnalysisStatus): string {
function getRuntimeLine(status: ImageAnalysisStatus, target: CliTarget): string {
if (target !== 'claude') {
return `${TARGET_LABELS[target]} launch -> no Claude Read hook`;
}
if (status.effectiveRuntimeMode === 'native-read') {
return 'Read -> native file access';
}
@@ -93,6 +163,69 @@ function getRuntimeLine(status: ImageAnalysisStatus): string {
return `Read -> image-analysis hook -> ${status.runtimePath || 'CLIProxy'}`;
}
function getConfiguredBackendDetail(status: ImageAnalysisStatus): string {
if (!status.backendId) {
return status.reason || 'No backend mapped';
}
return RESOLUTION_SOURCE_LABELS[status.resolutionSource] || 'Configured';
}
function getEffectiveRuntimeValue(status: ImageAnalysisStatus, target: CliTarget): string {
if (target !== 'claude') {
return `Not active on ${TARGET_LABELS[target]}`;
}
if (status.effectiveRuntimeMode === 'native-read') {
return 'Native file access';
}
return 'CLIProxy image analysis';
}
function getEffectiveRuntimeDetail(status: ImageAnalysisStatus, target: CliTarget): string {
if (target !== 'claude') {
if (status.status === 'hook-missing') {
return (
status.reason ||
`${TARGET_LABELS[target]} bypasses the Claude Read hook, and the saved Claude-side profile hook is still missing.`
);
}
if (status.effectiveRuntimeMode === 'native-read') {
return (
status.effectiveRuntimeReason ||
status.reason ||
`${TARGET_LABELS[target]} bypasses the Claude Read hook, and the saved Claude-side setup currently falls back to native file access.`
);
}
return `${TARGET_LABELS[target]} bypasses the Claude Read hook. Switch the target back to Claude Code to use the saved backend shown here.`;
}
if (status.effectiveRuntimeMode === 'native-read') {
return (
status.effectiveRuntimeReason ||
status.reason ||
'Uses built-in file access for this profile.'
);
}
if (status.proxyReadiness === 'stopped') {
return 'Auth ready • Local CLIProxy starts on demand';
}
if (status.proxyReadiness === 'remote') {
return 'Auth ready • Remote CLIProxy reachable';
}
if (status.authReadiness === 'ready' && status.proxyReadiness === 'ready') {
return 'Auth ready • Local CLIProxy reachable';
}
return status.proxyReason || status.authReason || 'Runtime verified';
}
function getAuthLine(status: ImageAnalysisStatus): string {
if (status.authReadiness === 'not-needed') return 'Not required';
if (status.authReadiness === 'ready')
@@ -124,16 +257,25 @@ function getStatusContext(
return 'Saved runtime status for this profile. Config stays in the JSON editor above; auth and proxy readiness are derived at runtime.';
}
function getPersistenceLine(status: ImageAnalysisStatus): string {
if (!status.shouldPersistHook || !status.persistencePath)
return 'Not persisted for this profile type';
return status.hookInstalled
? `${status.persistencePath} hook`
: `${status.persistencePath} hook missing`;
function getPersistenceValue(status: ImageAnalysisStatus): string {
if (!status.shouldPersistHook || !status.persistencePath) {
return 'Not persisted';
}
return status.hookInstalled ? 'Hook saved to profile' : 'Hook missing from profile';
}
function getPersistenceDetail(status: ImageAnalysisStatus): string {
if (!status.shouldPersistHook || !status.persistencePath) {
return 'Not required for this profile type';
}
return status.persistencePath;
}
export function ImageAnalysisStatusSection({
status,
target = 'claude',
source = 'saved',
previewState = 'saved',
}: ImageAnalysisStatusSectionProps) {
@@ -147,9 +289,12 @@ export function ImageAnalysisStatusSection({
);
}
const badge = getBadge(status);
const notice =
status.effectiveRuntimeMode === 'native-read'
const previewBadge = getPreviewBadge(source, previewState);
const runtimeBadge = getRuntimeBadge(status, target);
const bypassedOnCurrentTarget = target !== 'claude';
const notice = bypassedOnCurrentTarget
? null
: status.effectiveRuntimeMode === 'native-read'
? status.effectiveRuntimeReason
: status.status === 'attention' || status.status === 'hook-missing'
? status.reason
@@ -157,43 +302,82 @@ export function ImageAnalysisStatusSection({
return (
<section className="rounded-md border bg-muted/20 p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<div className="flex items-center gap-2">
<ImageIcon className="h-4 w-4 text-sky-600" />
<h3 className="text-sm font-semibold">Image-analysis backend</h3>
<h3 className="text-sm font-semibold">Image Analysis</h3>
</div>
<p className="mt-1 text-xs text-muted-foreground">
{getStatusContext(source, previewState)}
</p>
</div>
<Badge variant={badge.variant} className="shrink-0 text-[11px]">
{badge.label}
</Badge>
<div className="flex flex-wrap items-center gap-2">
<Badge variant={previewBadge.variant} className="h-5 shrink-0 px-1.5 text-[10px]">
{previewBadge.label}
</Badge>
<Badge variant={runtimeBadge.variant} className="h-5 shrink-0 px-1.5 text-[10px]">
{runtimeBadge.label}
</Badge>
</div>
</div>
<p aria-live="polite" className="mt-3 text-sm leading-6 text-muted-foreground">
{getSummary(status)}
{getSummary(status, target)}
</p>
<dl className="mt-4 grid gap-x-4 gap-y-3 sm:grid-cols-2">
<div className="space-y-1">
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
Backend
</dt>
<dd className="text-sm font-medium">{status.backendDisplayName || 'Unresolved'}</dd>
{bypassedOnCurrentTarget && (
<div className="mt-4 flex items-start gap-2 rounded-md border border-sky-500/20 bg-sky-500/10 px-3 py-2 text-sm text-sky-900 dark:text-sky-200">
<Route className="mt-0.5 h-4 w-4 shrink-0 text-sky-600 dark:text-sky-300" />
<span>
Current default target: {TARGET_LABELS[target]}. The diagnostics below describe the
saved Claude-side Image Analysis hook and apply again if you switch back to Claude Code.
</span>
</div>
<div className="space-y-1">
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
Runtime
</dt>
<dd
className="font-mono text-xs leading-5 text-foreground"
title={getRuntimeLine(status)}
)}
<div className="mt-4 grid gap-3 md:grid-cols-3">
<div className="rounded-md border bg-background/70 p-3">
<div className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
Configured backend
</div>
<div className="mt-2 text-sm font-medium text-foreground">
{status.backendDisplayName || status.backendId || 'No backend mapped'}
</div>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
{getConfiguredBackendDetail(status)}
</p>
</div>
<div className="rounded-md border bg-background/70 p-3">
<div className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
Effective runtime
</div>
<div className="mt-2 text-sm font-medium text-foreground">
{getEffectiveRuntimeValue(status, target)}
</div>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
{getEffectiveRuntimeDetail(status, target)}
</p>
</div>
<div className="rounded-md border bg-background/70 p-3">
<div className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
Hook persistence
</div>
<div className="mt-2 text-sm font-medium text-foreground">
{getPersistenceValue(status)}
</div>
<p
className="mt-1 text-xs leading-5 text-muted-foreground"
title={status.persistencePath || 'Not persisted'}
>
{getRuntimeLine(status)}
</dd>
{getPersistenceDetail(status)}
</p>
</div>
</div>
<dl className="mt-4 grid gap-x-4 gap-y-3 sm:grid-cols-3">
<div className="space-y-1">
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
Auth
@@ -206,17 +390,6 @@ export function ImageAnalysisStatusSection({
</dt>
<dd className="text-sm text-foreground">{getProxyLine(status)}</dd>
</div>
<div className="space-y-1">
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
Persistence
</dt>
<dd
className="font-mono text-xs leading-5 text-foreground"
title={status.persistencePath || 'Not persisted'}
>
{getPersistenceLine(status)}
</dd>
</div>
<div className="space-y-1">
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
Model
@@ -227,6 +400,18 @@ export function ImageAnalysisStatusSection({
</div>
</dl>
<div className="mt-4 rounded-md border bg-background/60 px-3 py-2">
<div className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
Runtime path
</div>
<div
className="mt-1 font-mono text-xs leading-5 text-foreground"
title={getRuntimeLine(status, target)}
>
{getRuntimeLine(status, target)}
</div>
</div>
{notice && (
<div className="mt-4 flex items-start gap-2 rounded-md border border-amber-500/20 bg-amber-500/10 px-3 py-2 text-sm text-amber-900 dark:text-amber-200">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-500" />
@@ -237,7 +422,8 @@ export function ImageAnalysisStatusSection({
<div className="mt-4 flex items-center gap-2 border-t border-border/60 pt-3 text-xs text-muted-foreground">
<Route className="h-3.5 w-3.5" />
<span>
WebSearch stays managed separately and is not controlled by this backend status.
This panel covers the Image Analysis hook only. WebSearch stays managed separately and is
not controlled here.
</span>
</div>
</section>
+6 -2
View File
@@ -124,6 +124,7 @@ export function ProfileEditor({
data: previewStatusResponse,
isFetching: isPreviewStatusFetching,
isError: isPreviewStatusError,
isPlaceholderData: isPreviewStatusPlaceholderData,
} = useQuery<{ imageAnalysisStatus: SettingsResponse['imageAnalysisStatus'] }>({
queryKey: ['settings', profileName, 'image-analysis-status-preview', deferredPreviewJson],
enabled: previewSettings !== null,
@@ -160,7 +161,8 @@ export function ProfileEditor({
? 'invalid'
: isPreviewStatusError
? 'saved'
: isPreviewStatusFetching && !previewStatusResponse?.imageAnalysisStatus
: isPreviewStatusFetching &&
(!previewStatusResponse?.imageAnalysisStatus || isPreviewStatusPlaceholderData)
? 'refreshing'
: 'preview';
@@ -219,7 +221,8 @@ export function ProfileEditor({
toast.success(i18n.t('commonToast.defaultTargetUpdated'));
},
onError: (error: Error, target: CliTarget) => {
const targetLabel = target === 'droid' ? 'Factory Droid' : 'Claude Code';
const targetLabel =
target === 'droid' ? 'Factory Droid' : target === 'codex' ? 'Codex CLI' : 'Claude Code';
const suffix = error.message.trim() ? `: ${error.message}` : '';
toast.error(i18n.t('commonToast.failedUpdateDefaultTarget', { target: targetLabel, suffix }));
},
@@ -311,6 +314,7 @@ export function ProfileEditor({
isRawJsonValid={computedIsRawJsonValid}
rawJsonEdits={rawJsonEdits}
settings={settings}
profileTarget={resolvedTarget}
imageAnalysisStatus={imageAnalysisStatus}
imageAnalysisStatusSource={imageAnalysisStatusSource}
imageAnalysisStatusPreviewState={imageAnalysisStatusPreviewState}
@@ -8,7 +8,7 @@ import { Loader2, X, AlertTriangle } from 'lucide-react';
import { GlobalEnvIndicator } from '@/components/shared/global-env-indicator';
import { ImageAnalysisStatusSection } from './image-analysis-status-section';
import type { Settings } from './types';
import type { ImageAnalysisStatus } from '@/lib/api-client';
import type { CliTarget, ImageAnalysisStatus } from '@/lib/api-client';
// Lazy load CodeEditor
const CodeEditor = lazy(() =>
@@ -20,6 +20,7 @@ interface RawEditorSectionProps {
isRawJsonValid: boolean;
rawJsonEdits: string | null;
settings: Settings | undefined;
profileTarget?: CliTarget;
imageAnalysisStatus?: ImageAnalysisStatus | null;
imageAnalysisStatusSource?: 'saved' | 'editor';
imageAnalysisStatusPreviewState?: 'saved' | 'preview' | 'refreshing' | 'invalid';
@@ -32,6 +33,7 @@ export function RawEditorSection({
isRawJsonValid,
rawJsonEdits,
settings,
profileTarget = 'claude',
imageAnalysisStatus,
imageAnalysisStatusSource = 'saved',
imageAnalysisStatusPreviewState = 'saved',
@@ -86,6 +88,7 @@ export function RawEditorSection({
<div className="mx-6 mb-4">
<ImageAnalysisStatusSection
status={imageAnalysisStatus}
target={profileTarget}
source={imageAnalysisStatusSource}
previewState={imageAnalysisStatusPreviewState}
/>
@@ -80,7 +80,7 @@ const schema = z.object({
opusModel: z.string().optional(),
sonnetModel: z.string().optional(),
haikuModel: z.string().optional(),
target: z.enum(['claude', 'droid']),
target: z.enum(['claude', 'droid', 'codex']),
});
type FormData = z.infer<typeof schema>;
@@ -521,6 +521,7 @@ export function ProfileCreateDialog({
<SelectContent>
<SelectItem value="claude">Claude Code (default)</SelectItem>
<SelectItem value="droid">Factory Droid</SelectItem>
<SelectItem value="codex">Codex CLI</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
@@ -529,6 +530,11 @@ export function ProfileCreateDialog({
{t('profileEditor.targetHintPreferredAlias')}{' '}
<code className="bg-muted px-1 rounded text-[10px]">ccs-droid</code>.
</>
) : targetValue === 'codex' ? (
<>
{t('profileEditor.targetHintPreferredAlias')}{' '}
<code className="bg-muted px-1 rounded text-[10px]">ccsx</code>.
</>
) : (
<>
{t('profileEditor.targetHintClaudeDefault')}{' '}
@@ -541,6 +547,12 @@ export function ProfileCreateDialog({
{t('profileEditor.targetHintLegacyAlias')}{' '}
<code className="bg-muted px-1 rounded text-[10px]">ccsd</code>.
</>
) : targetValue === 'codex' ? (
<>
{' '}
{t('profileEditor.targetHintLegacyAlias')}{' '}
<code className="bg-muted px-1 rounded text-[10px]">ccs-codex</code>.
</>
) : null}{' '}
{t('profileEditor.targetHintOverride')}{' '}
<code className="bg-muted px-1 rounded text-[10px]">--target</code>.