mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 14:18:12 +00:00
feat(ui): clarify image-analysis target status
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>.
|
||||
|
||||
Reference in New Issue
Block a user