mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
feat(ui): clarify image-analysis target status
This commit is contained in:
1 parent
3b61673ad2
commit
0246e327fe
10 files changed
+495
-84
No files matched your search
@@ -30,7 +30,7 @@ const singleProviderSchema = z.object({
|
|||||||
provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }),
|
provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }),
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
account: z.string().optional(),
|
account: z.string().optional(),
|
||||||
target: z.enum(['claude', 'droid']),
|
target: z.enum(['claude', 'droid', 'codex']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const compositeSchema = z.object({
|
const compositeSchema = z.object({
|
||||||
@@ -39,7 +39,7 @@ const compositeSchema = z.object({
|
|||||||
.min(1, 'Name is required')
|
.min(1, 'Name is required')
|
||||||
.regex(/^[a-zA-Z][a-zA-Z0-9._-]*$/, 'Invalid variant name'),
|
.regex(/^[a-zA-Z][a-zA-Z0-9._-]*$/, 'Invalid variant name'),
|
||||||
default_tier: z.enum(['opus', 'sonnet', 'haiku'], { message: 'Default tier is required' }),
|
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({
|
tiers: z.object({
|
||||||
opus: z.object({
|
opus: z.object({
|
||||||
provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }),
|
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="claude">{t('cliproxyDialog.claudeCode')}</option>
|
||||||
<option value="droid">{t('cliproxyDialog.factoryDroid')}</option>
|
<option value="droid">{t('cliproxyDialog.factoryDroid')}</option>
|
||||||
|
<option value="codex">Codex CLI</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -353,6 +354,7 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
|
|||||||
>
|
>
|
||||||
<option value="claude">{t('cliproxyDialog.claudeCode')}</option>
|
<option value="claude">{t('cliproxyDialog.claudeCode')}</option>
|
||||||
<option value="droid">{t('cliproxyDialog.factoryDroid')}</option>
|
<option value="droid">{t('cliproxyDialog.factoryDroid')}</option>
|
||||||
|
<option value="codex">Codex CLI</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ const singleProviderSchema = z.object({
|
|||||||
provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }),
|
provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }),
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
account: z.string().optional(),
|
account: z.string().optional(),
|
||||||
target: z.enum(['claude', 'droid']),
|
target: z.enum(['claude', 'droid', 'codex']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const compositeSchema = z.object({
|
const compositeSchema = z.object({
|
||||||
default_tier: z.enum(['opus', 'sonnet', 'haiku'], { message: 'Default tier is required' }),
|
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({
|
tiers: z.object({
|
||||||
opus: z.object({
|
opus: z.object({
|
||||||
provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }),
|
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="claude">{t('cliproxyDialog.claudeCode')}</option>
|
||||||
<option value="droid">{t('cliproxyDialog.factoryDroid')}</option>
|
<option value="droid">{t('cliproxyDialog.factoryDroid')}</option>
|
||||||
|
<option value="codex">Codex CLI</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -433,6 +434,7 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
|
|||||||
>
|
>
|
||||||
<option value="claude">{t('cliproxyDialog.claudeCode')}</option>
|
<option value="claude">{t('cliproxyDialog.claudeCode')}</option>
|
||||||
<option value="droid">{t('cliproxyDialog.factoryDroid')}</option>
|
<option value="droid">{t('cliproxyDialog.factoryDroid')}</option>
|
||||||
|
<option value="codex">Codex CLI</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export function HeaderSection({
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="claude">Claude Code</SelectItem>
|
<SelectItem value="claude">Claude Code</SelectItem>
|
||||||
<SelectItem value="droid">Factory Droid</SelectItem>
|
<SelectItem value="droid">Factory Droid</SelectItem>
|
||||||
|
<SelectItem value="codex">Codex CLI</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{isTargetSaving && <Loader2 className="w-3.5 h-3.5 animate-spin text-muted-foreground" />}
|
{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 { AlertTriangle, Image as ImageIcon, Route } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { ImageAnalysisStatus } from '@/lib/api-client';
|
import type { CliTarget, ImageAnalysisStatus } from '@/lib/api-client';
|
||||||
|
|
||||||
interface ImageAnalysisStatusSectionProps {
|
interface ImageAnalysisStatusSectionProps {
|
||||||
status?: ImageAnalysisStatus | null;
|
status?: ImageAnalysisStatus | null;
|
||||||
|
target?: CliTarget;
|
||||||
source?: 'saved' | 'editor';
|
source?: 'saved' | 'editor';
|
||||||
previewState?: 'saved' | 'preview' | 'refreshing' | 'invalid';
|
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 (!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 === 'disabled') return { label: 'Disabled', variant: 'outline' as const };
|
||||||
if (status.status === 'hook-missing')
|
if (status.status === 'hook-missing')
|
||||||
return { label: 'Setup needed', variant: 'destructive' as const };
|
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 };
|
return { label: 'Starts on launch', variant: 'secondary' as const };
|
||||||
}
|
}
|
||||||
if (status.effectiveRuntimeMode === 'cliproxy-image-analysis') {
|
if (status.effectiveRuntimeMode === 'cliproxy-image-analysis') {
|
||||||
return {
|
return { label: 'CLIProxy Active', variant: 'default' as const };
|
||||||
label: status.resolutionSource === 'profile-backend' ? 'Ready via mapping' : 'Ready',
|
|
||||||
variant: 'default' as const,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
status.authReadiness === 'unknown' ||
|
status.authReadiness === 'unknown' ||
|
||||||
@@ -37,47 +82,72 @@ function getBadge(status: ImageAnalysisStatus | null | undefined) {
|
|||||||
) {
|
) {
|
||||||
return { label: 'Needs review', variant: 'outline' as const };
|
return { label: 'Needs review', variant: 'outline' as const };
|
||||||
}
|
}
|
||||||
if (status.status === 'skipped' && status.reason?.includes('native file access')) {
|
return { label: 'Native Access', variant: 'outline' as const };
|
||||||
return { label: 'Native Claude', variant: 'secondary' as const };
|
|
||||||
}
|
|
||||||
return { label: 'Native Read', 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 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') {
|
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) {
|
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') {
|
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') {
|
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') {
|
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') {
|
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) {
|
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') {
|
if (status.effectiveRuntimeMode === 'native-read') {
|
||||||
return 'Read -> native file access';
|
return 'Read -> native file access';
|
||||||
}
|
}
|
||||||
@@ -93,6 +163,69 @@ function getRuntimeLine(status: ImageAnalysisStatus): string {
|
|||||||
return `Read -> image-analysis hook -> ${status.runtimePath || 'CLIProxy'}`;
|
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 {
|
function getAuthLine(status: ImageAnalysisStatus): string {
|
||||||
if (status.authReadiness === 'not-needed') return 'Not required';
|
if (status.authReadiness === 'not-needed') return 'Not required';
|
||||||
if (status.authReadiness === 'ready')
|
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.';
|
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 {
|
function getPersistenceValue(status: ImageAnalysisStatus): string {
|
||||||
if (!status.shouldPersistHook || !status.persistencePath)
|
if (!status.shouldPersistHook || !status.persistencePath) {
|
||||||
return 'Not persisted for this profile type';
|
return 'Not persisted';
|
||||||
return status.hookInstalled
|
}
|
||||||
? `${status.persistencePath} hook`
|
|
||||||
: `${status.persistencePath} hook missing`;
|
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({
|
export function ImageAnalysisStatusSection({
|
||||||
status,
|
status,
|
||||||
|
target = 'claude',
|
||||||
source = 'saved',
|
source = 'saved',
|
||||||
previewState = 'saved',
|
previewState = 'saved',
|
||||||
}: ImageAnalysisStatusSectionProps) {
|
}: ImageAnalysisStatusSectionProps) {
|
||||||
@@ -147,9 +289,12 @@ export function ImageAnalysisStatusSection({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const badge = getBadge(status);
|
const previewBadge = getPreviewBadge(source, previewState);
|
||||||
const notice =
|
const runtimeBadge = getRuntimeBadge(status, target);
|
||||||
status.effectiveRuntimeMode === 'native-read'
|
const bypassedOnCurrentTarget = target !== 'claude';
|
||||||
|
const notice = bypassedOnCurrentTarget
|
||||||
|
? null
|
||||||
|
: status.effectiveRuntimeMode === 'native-read'
|
||||||
? status.effectiveRuntimeReason
|
? status.effectiveRuntimeReason
|
||||||
: status.status === 'attention' || status.status === 'hook-missing'
|
: status.status === 'attention' || status.status === 'hook-missing'
|
||||||
? status.reason
|
? status.reason
|
||||||
@@ -157,43 +302,82 @@ export function ImageAnalysisStatusSection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-md border bg-muted/20 p-4">
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ImageIcon className="h-4 w-4 text-sky-600" />
|
<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>
|
</div>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{getStatusContext(source, previewState)}
|
{getStatusContext(source, previewState)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={badge.variant} className="shrink-0 text-[11px]">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{badge.label}
|
<Badge variant={previewBadge.variant} className="h-5 shrink-0 px-1.5 text-[10px]">
|
||||||
</Badge>
|
{previewBadge.label}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={runtimeBadge.variant} className="h-5 shrink-0 px-1.5 text-[10px]">
|
||||||
|
{runtimeBadge.label}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p aria-live="polite" className="mt-3 text-sm leading-6 text-muted-foreground">
|
<p aria-live="polite" className="mt-3 text-sm leading-6 text-muted-foreground">
|
||||||
{getSummary(status)}
|
{getSummary(status, target)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<dl className="mt-4 grid gap-x-4 gap-y-3 sm:grid-cols-2">
|
{bypassedOnCurrentTarget && (
|
||||||
<div className="space-y-1">
|
<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">
|
||||||
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
<Route className="mt-0.5 h-4 w-4 shrink-0 text-sky-600 dark:text-sky-300" />
|
||||||
Backend
|
<span>
|
||||||
</dt>
|
Current default target: {TARGET_LABELS[target]}. The diagnostics below describe the
|
||||||
<dd className="text-sm font-medium">{status.backendDisplayName || 'Unresolved'}</dd>
|
saved Claude-side Image Analysis hook and apply again if you switch back to Claude Code.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
)}
|
||||||
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
|
||||||
Runtime
|
<div className="mt-4 grid gap-3 md:grid-cols-3">
|
||||||
</dt>
|
<div className="rounded-md border bg-background/70 p-3">
|
||||||
<dd
|
<div className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||||
className="font-mono text-xs leading-5 text-foreground"
|
Configured backend
|
||||||
title={getRuntimeLine(status)}
|
</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)}
|
{getPersistenceDetail(status)}
|
||||||
</dd>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl className="mt-4 grid gap-x-4 gap-y-3 sm:grid-cols-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||||
Auth
|
Auth
|
||||||
@@ -206,17 +390,6 @@ export function ImageAnalysisStatusSection({
|
|||||||
</dt>
|
</dt>
|
||||||
<dd className="text-sm text-foreground">{getProxyLine(status)}</dd>
|
<dd className="text-sm text-foreground">{getProxyLine(status)}</dd>
|
||||||
</div>
|
</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">
|
<div className="space-y-1">
|
||||||
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||||
Model
|
Model
|
||||||
@@ -227,6 +400,18 @@ export function ImageAnalysisStatusSection({
|
|||||||
</div>
|
</div>
|
||||||
</dl>
|
</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 && (
|
{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">
|
<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" />
|
<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">
|
<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" />
|
<Route className="h-3.5 w-3.5" />
|
||||||
<span>
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export function ProfileEditor({
|
|||||||
data: previewStatusResponse,
|
data: previewStatusResponse,
|
||||||
isFetching: isPreviewStatusFetching,
|
isFetching: isPreviewStatusFetching,
|
||||||
isError: isPreviewStatusError,
|
isError: isPreviewStatusError,
|
||||||
|
isPlaceholderData: isPreviewStatusPlaceholderData,
|
||||||
} = useQuery<{ imageAnalysisStatus: SettingsResponse['imageAnalysisStatus'] }>({
|
} = useQuery<{ imageAnalysisStatus: SettingsResponse['imageAnalysisStatus'] }>({
|
||||||
queryKey: ['settings', profileName, 'image-analysis-status-preview', deferredPreviewJson],
|
queryKey: ['settings', profileName, 'image-analysis-status-preview', deferredPreviewJson],
|
||||||
enabled: previewSettings !== null,
|
enabled: previewSettings !== null,
|
||||||
@@ -160,7 +161,8 @@ export function ProfileEditor({
|
|||||||
? 'invalid'
|
? 'invalid'
|
||||||
: isPreviewStatusError
|
: isPreviewStatusError
|
||||||
? 'saved'
|
? 'saved'
|
||||||
: isPreviewStatusFetching && !previewStatusResponse?.imageAnalysisStatus
|
: isPreviewStatusFetching &&
|
||||||
|
(!previewStatusResponse?.imageAnalysisStatus || isPreviewStatusPlaceholderData)
|
||||||
? 'refreshing'
|
? 'refreshing'
|
||||||
: 'preview';
|
: 'preview';
|
||||||
|
|
||||||
@@ -219,7 +221,8 @@ export function ProfileEditor({
|
|||||||
toast.success(i18n.t('commonToast.defaultTargetUpdated'));
|
toast.success(i18n.t('commonToast.defaultTargetUpdated'));
|
||||||
},
|
},
|
||||||
onError: (error: Error, target: CliTarget) => {
|
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}` : '';
|
const suffix = error.message.trim() ? `: ${error.message}` : '';
|
||||||
toast.error(i18n.t('commonToast.failedUpdateDefaultTarget', { target: targetLabel, suffix }));
|
toast.error(i18n.t('commonToast.failedUpdateDefaultTarget', { target: targetLabel, suffix }));
|
||||||
},
|
},
|
||||||
@@ -311,6 +314,7 @@ export function ProfileEditor({
|
|||||||
isRawJsonValid={computedIsRawJsonValid}
|
isRawJsonValid={computedIsRawJsonValid}
|
||||||
rawJsonEdits={rawJsonEdits}
|
rawJsonEdits={rawJsonEdits}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
|
profileTarget={resolvedTarget}
|
||||||
imageAnalysisStatus={imageAnalysisStatus}
|
imageAnalysisStatus={imageAnalysisStatus}
|
||||||
imageAnalysisStatusSource={imageAnalysisStatusSource}
|
imageAnalysisStatusSource={imageAnalysisStatusSource}
|
||||||
imageAnalysisStatusPreviewState={imageAnalysisStatusPreviewState}
|
imageAnalysisStatusPreviewState={imageAnalysisStatusPreviewState}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Loader2, X, AlertTriangle } from 'lucide-react';
|
|||||||
import { GlobalEnvIndicator } from '@/components/shared/global-env-indicator';
|
import { GlobalEnvIndicator } from '@/components/shared/global-env-indicator';
|
||||||
import { ImageAnalysisStatusSection } from './image-analysis-status-section';
|
import { ImageAnalysisStatusSection } from './image-analysis-status-section';
|
||||||
import type { Settings } from './types';
|
import type { Settings } from './types';
|
||||||
import type { ImageAnalysisStatus } from '@/lib/api-client';
|
import type { CliTarget, ImageAnalysisStatus } from '@/lib/api-client';
|
||||||
|
|
||||||
// Lazy load CodeEditor
|
// Lazy load CodeEditor
|
||||||
const CodeEditor = lazy(() =>
|
const CodeEditor = lazy(() =>
|
||||||
@@ -20,6 +20,7 @@ interface RawEditorSectionProps {
|
|||||||
isRawJsonValid: boolean;
|
isRawJsonValid: boolean;
|
||||||
rawJsonEdits: string | null;
|
rawJsonEdits: string | null;
|
||||||
settings: Settings | undefined;
|
settings: Settings | undefined;
|
||||||
|
profileTarget?: CliTarget;
|
||||||
imageAnalysisStatus?: ImageAnalysisStatus | null;
|
imageAnalysisStatus?: ImageAnalysisStatus | null;
|
||||||
imageAnalysisStatusSource?: 'saved' | 'editor';
|
imageAnalysisStatusSource?: 'saved' | 'editor';
|
||||||
imageAnalysisStatusPreviewState?: 'saved' | 'preview' | 'refreshing' | 'invalid';
|
imageAnalysisStatusPreviewState?: 'saved' | 'preview' | 'refreshing' | 'invalid';
|
||||||
@@ -32,6 +33,7 @@ export function RawEditorSection({
|
|||||||
isRawJsonValid,
|
isRawJsonValid,
|
||||||
rawJsonEdits,
|
rawJsonEdits,
|
||||||
settings,
|
settings,
|
||||||
|
profileTarget = 'claude',
|
||||||
imageAnalysisStatus,
|
imageAnalysisStatus,
|
||||||
imageAnalysisStatusSource = 'saved',
|
imageAnalysisStatusSource = 'saved',
|
||||||
imageAnalysisStatusPreviewState = 'saved',
|
imageAnalysisStatusPreviewState = 'saved',
|
||||||
@@ -86,6 +88,7 @@ export function RawEditorSection({
|
|||||||
<div className="mx-6 mb-4">
|
<div className="mx-6 mb-4">
|
||||||
<ImageAnalysisStatusSection
|
<ImageAnalysisStatusSection
|
||||||
status={imageAnalysisStatus}
|
status={imageAnalysisStatus}
|
||||||
|
target={profileTarget}
|
||||||
source={imageAnalysisStatusSource}
|
source={imageAnalysisStatusSource}
|
||||||
previewState={imageAnalysisStatusPreviewState}
|
previewState={imageAnalysisStatusPreviewState}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const schema = z.object({
|
|||||||
opusModel: z.string().optional(),
|
opusModel: z.string().optional(),
|
||||||
sonnetModel: z.string().optional(),
|
sonnetModel: z.string().optional(),
|
||||||
haikuModel: z.string().optional(),
|
haikuModel: z.string().optional(),
|
||||||
target: z.enum(['claude', 'droid']),
|
target: z.enum(['claude', 'droid', 'codex']),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof schema>;
|
type FormData = z.infer<typeof schema>;
|
||||||
@@ -521,6 +521,7 @@ export function ProfileCreateDialog({
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="claude">Claude Code (default)</SelectItem>
|
<SelectItem value="claude">Claude Code (default)</SelectItem>
|
||||||
<SelectItem value="droid">Factory Droid</SelectItem>
|
<SelectItem value="droid">Factory Droid</SelectItem>
|
||||||
|
<SelectItem value="codex">Codex CLI</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
@@ -529,6 +530,11 @@ export function ProfileCreateDialog({
|
|||||||
{t('profileEditor.targetHintPreferredAlias')}{' '}
|
{t('profileEditor.targetHintPreferredAlias')}{' '}
|
||||||
<code className="bg-muted px-1 rounded text-[10px]">ccs-droid</code>.
|
<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')}{' '}
|
{t('profileEditor.targetHintClaudeDefault')}{' '}
|
||||||
@@ -541,6 +547,12 @@ export function ProfileCreateDialog({
|
|||||||
{t('profileEditor.targetHintLegacyAlias')}{' '}
|
{t('profileEditor.targetHintLegacyAlias')}{' '}
|
||||||
<code className="bg-muted px-1 rounded text-[10px]">ccsd</code>.
|
<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}{' '}
|
) : null}{' '}
|
||||||
{t('profileEditor.targetHintOverride')}{' '}
|
{t('profileEditor.targetHintOverride')}{' '}
|
||||||
<code className="bg-muted px-1 rounded text-[10px]">--target</code>.
|
<code className="bg-muted px-1 rounded text-[10px]">--target</code>.
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ async function request<T>(url: string, options?: RequestInit): Promise<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type CliTarget = 'claude' | 'droid';
|
export type CliTarget = 'claude' | 'droid' | 'codex';
|
||||||
|
|
||||||
export interface CliproxyBridgeMetadata {
|
export interface CliproxyBridgeMetadata {
|
||||||
provider: CLIProxyProvider;
|
provider: CLIProxyProvider;
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
|
|||||||
'Use ccs-codex or ccsx for native Codex runs.',
|
'Use ccs-codex or ccsx for native Codex runs.',
|
||||||
'Use ccsxp for the built-in CCS Codex provider shortcut on native Codex.',
|
'Use ccsxp for the built-in CCS Codex provider shortcut on native Codex.',
|
||||||
'Built-in Codex and Codex bridge profiles can run on native Codex with --target codex.',
|
'Built-in Codex and Codex bridge profiles can run on native Codex with --target codex.',
|
||||||
'Saved default targets for API profiles and variants remain claude or droid.',
|
'Saved default targets for API profiles and variants can now be claude, droid, or codex.',
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
@@ -256,7 +256,7 @@ export const CLI_SUPPORT_ENTRIES: CliSupportEntry[] = [
|
|||||||
routes: [{ label: 'Codex CLI', path: '/codex' }],
|
routes: [{ label: 'Codex CLI', path: '/codex' }],
|
||||||
commands: ['ccs-codex', 'ccsx', 'ccs codex --target codex', 'ccs codex-api --target codex'],
|
commands: ['ccs-codex', 'ccsx', 'ccs codex --target codex', 'ccs codex-api --target codex'],
|
||||||
notes:
|
notes:
|
||||||
'Runtime-only in v1. Saved default targets for API profiles and CLIProxy variants remain claude or droid.',
|
'Saved default targets for API profiles and CLIProxy variants can now be claude, droid, or codex.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'codex-cliproxy',
|
id: 'codex-cliproxy',
|
||||||
|
|||||||
@@ -80,17 +80,25 @@ describe('ImageAnalysisStatusSection', () => {
|
|||||||
it('renders saved diagnostics for an active backend', () => {
|
it('renders saved diagnostics for an active backend', () => {
|
||||||
render(<ImageAnalysisStatusSection status={createStatus()} />);
|
render(<ImageAnalysisStatusSection status={createStatus()} />);
|
||||||
|
|
||||||
expect(screen.getByText('Image-analysis backend')).toBeInTheDocument();
|
expect(screen.getByText('Image Analysis')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(
|
screen.getByText(
|
||||||
/Saved runtime status for this profile\. Config stays in the JSON editor above; auth and proxy readiness are derived at runtime\./i
|
/Saved runtime status for this profile\. Config stays in the JSON editor above; auth and proxy readiness are derived at runtime\./i
|
||||||
)
|
)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText('Ready')).toBeInTheDocument();
|
expect(screen.getByText('Saved')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('CLIProxy Active')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/Configured via Google Gemini\. Image and PDF reads use CLIProxy/i)
|
screen.getByText(
|
||||||
|
/Images and PDFs for this profile resolve through Google Gemini via CLIProxy\./i
|
||||||
|
)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Configured backend')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Effective runtime')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Hook persistence')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Google Gemini')).toBeInTheDocument();
|
expect(screen.getByText('Google Gemini')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('CLIProxy image analysis')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Hook saved to profile')).toBeInTheDocument();
|
||||||
expect(screen.getByTitle(/\/api\/provider\/gemini/)).toBeInTheDocument();
|
expect(screen.getByTitle(/\/api\/provider\/gemini/)).toBeInTheDocument();
|
||||||
expect(screen.getAllByText('Google Gemini ready')).toHaveLength(1);
|
expect(screen.getAllByText('Google Gemini ready')).toHaveLength(1);
|
||||||
expect(screen.getByText('Local CLIProxy ready')).toBeInTheDocument();
|
expect(screen.getByText('Local CLIProxy ready')).toBeInTheDocument();
|
||||||
@@ -112,13 +120,14 @@ describe('ImageAnalysisStatusSection', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('Ready via mapping')).toBeInTheDocument();
|
expect(screen.getByText('CLIProxy Active')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(
|
screen.getByText(
|
||||||
/Configured via saved GitHub Copilot \(OAuth\) mapping\. Auth and runtime are ready/i
|
/Images and PDFs for this profile resolve through GitHub Copilot \(OAuth\) via a saved Image Analysis mapping\./i
|
||||||
)
|
)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText('GitHub Copilot (OAuth)')).toBeInTheDocument();
|
expect(screen.getByText('GitHub Copilot (OAuth)')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Saved Image Analysis mapping')).toBeInTheDocument();
|
||||||
expect(screen.getByText('claude-haiku-4.5')).toBeInTheDocument();
|
expect(screen.getByText('claude-haiku-4.5')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -136,8 +145,11 @@ describe('ImageAnalysisStatusSection', () => {
|
|||||||
|
|
||||||
expect(screen.getByText('Setup needed')).toBeInTheDocument();
|
expect(screen.getByText('Setup needed')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/Configured for Google Gemini, but Profile hook is missing/i)
|
screen.getByText(
|
||||||
|
/Images and PDFs are configured for Google Gemini, but Profile hook is missing/i
|
||||||
|
)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Native file access')).toBeInTheDocument();
|
||||||
expect(screen.getByTitle(/native file access/i)).toBeInTheDocument();
|
expect(screen.getByTitle(/native file access/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,11 +175,12 @@ describe('ImageAnalysisStatusSection', () => {
|
|||||||
|
|
||||||
expect(screen.getByText('Needs auth')).toBeInTheDocument();
|
expect(screen.getByText('Needs auth')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(
|
screen.getByText(/This profile currently falls back to native file access\./i)
|
||||||
/Configured via GitHub Copilot \(OAuth\), but GitHub Copilot \(OAuth\) auth is missing/i
|
|
||||||
)
|
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getAllByText(/Run "ccs ghcp --auth" to enable image analysis/i)).toHaveLength(3);
|
expect(screen.getByText('Native file access')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(/Run "ccs ghcp --auth" to enable image analysis/i).length
|
||||||
|
).toBeGreaterThanOrEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats an idle local proxy as launchable instead of unavailable', () => {
|
it('treats an idle local proxy as launchable instead of unavailable', () => {
|
||||||
@@ -183,12 +196,81 @@ describe('ImageAnalysisStatusSection', () => {
|
|||||||
|
|
||||||
expect(screen.getByText('Starts on launch')).toBeInTheDocument();
|
expect(screen.getByText('Starts on launch')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/Auth is ready and CCS will start the local CLIProxy runtime on launch/i)
|
screen.getByText(
|
||||||
|
/Images and PDFs for this profile resolve through Google Gemini\. Auth is ready and CCS will start the local CLIProxy runtime when needed\./i
|
||||||
|
)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText('Local CLIProxy idle; starts on launch')).toBeInTheDocument();
|
expect(screen.getByText('Local CLIProxy idle; starts on launch')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Auth ready • Local CLIProxy starts on demand')).toBeInTheDocument();
|
||||||
expect(screen.getByTitle(/start local CLIProxy/i)).toBeInTheDocument();
|
expect(screen.getByTitle(/start local CLIProxy/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows saved Claude-side diagnostics when the current target bypasses the hook', () => {
|
||||||
|
render(<ImageAnalysisStatusSection status={createStatus()} target="droid" />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Target bypassed')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
/Images and PDFs for this profile are configured to resolve through Google Gemini when the profile runs on Claude Code\./i
|
||||||
|
)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
/Current default target: Factory Droid\. The diagnostics below describe the saved Claude-side Image Analysis hook/i
|
||||||
|
)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Not active on Factory Droid')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
/Factory Droid bypasses the Claude Read hook\. Switch the target back to Claude Code to use the saved backend shown here\./i
|
||||||
|
)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByTitle(/Factory Droid launch -> no Claude Read hook/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps saved Claude-side auth failures visible when another target bypasses the hook', () => {
|
||||||
|
render(
|
||||||
|
<ImageAnalysisStatusSection
|
||||||
|
status={createStatus({
|
||||||
|
backendId: 'ghcp',
|
||||||
|
backendDisplayName: 'GitHub Copilot (OAuth)',
|
||||||
|
authReadiness: 'missing',
|
||||||
|
authProvider: 'ghcp',
|
||||||
|
authDisplayName: 'GitHub Copilot (OAuth)',
|
||||||
|
authReason:
|
||||||
|
'GitHub Copilot (OAuth) auth is missing. Run "ccs ghcp --auth" to enable image analysis.',
|
||||||
|
effectiveRuntimeMode: 'native-read',
|
||||||
|
effectiveRuntimeReason:
|
||||||
|
'GitHub Copilot (OAuth) auth is missing. Run "ccs ghcp --auth" to enable image analysis.',
|
||||||
|
})}
|
||||||
|
target="codex"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Needs auth')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
/Current default target: Codex CLI\. This launch path bypasses the Claude Read hook, and the saved Claude-side setup for GitHub Copilot \(OAuth\) still falls back to native file access\./i
|
||||||
|
)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(/Run "ccs ghcp --auth" to enable image analysis/i).length
|
||||||
|
).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to backend id when a display name is unavailable', () => {
|
||||||
|
render(
|
||||||
|
<ImageAnalysisStatusSection
|
||||||
|
status={createStatus({
|
||||||
|
backendDisplayName: null,
|
||||||
|
backendId: 'ghcp',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('ghcp')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('switches the panel to a live preview when the current editor JSON changes', async () => {
|
it('switches the panel to a live preview when the current editor JSON changes', async () => {
|
||||||
const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
const url = String(input);
|
const url = String(input);
|
||||||
@@ -305,4 +387,123 @@ describe('ImageAnalysisStatusSection', () => {
|
|||||||
});
|
});
|
||||||
expect(screen.getByText('Google Gemini')).toBeInTheDocument();
|
expect(screen.getByText('Google Gemini')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('marks the preview as refreshing when a newer editor preview is still loading', async () => {
|
||||||
|
let secondPreviewResolver: ((value: Response) => void) | null = null;
|
||||||
|
const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
|
const url = String(input);
|
||||||
|
|
||||||
|
if (url.includes('/api/settings/glm/raw')) {
|
||||||
|
return Promise.resolve(
|
||||||
|
createJsonResponse({
|
||||||
|
profile: 'glm',
|
||||||
|
settings: {
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'https://api.z.ai/v1',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'saved-token',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mtime: 1,
|
||||||
|
path: '/tmp/glm.settings.json',
|
||||||
|
imageAnalysisStatus: createStatus(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.includes('/api/settings/glm/image-analysis-status')) {
|
||||||
|
expect(init?.method).toBe('POST');
|
||||||
|
const body = JSON.parse(String(init?.body ?? '{}')) as {
|
||||||
|
settings?: { env?: Record<string, string> };
|
||||||
|
};
|
||||||
|
const baseUrl = body.settings?.env?.ANTHROPIC_BASE_URL ?? '';
|
||||||
|
|
||||||
|
if (baseUrl.includes('/ghcp')) {
|
||||||
|
return Promise.resolve(
|
||||||
|
createJsonResponse({
|
||||||
|
imageAnalysisStatus: createStatus({
|
||||||
|
backendId: 'ghcp',
|
||||||
|
backendDisplayName: 'GitHub Copilot (OAuth)',
|
||||||
|
model: 'claude-haiku-4.5',
|
||||||
|
runtimePath: '/api/provider/ghcp',
|
||||||
|
authReadiness: 'ready',
|
||||||
|
authProvider: 'ghcp',
|
||||||
|
authDisplayName: 'GitHub Copilot (OAuth)',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.includes('/codex')) {
|
||||||
|
return new Promise<Response>((resolve) => {
|
||||||
|
secondPreviewResolver = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
render(<ProfileEditor profileName="glm" profileTarget="claude" />);
|
||||||
|
|
||||||
|
expect(await screen.findByText('Google Gemini')).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('raw config editor'), {
|
||||||
|
target: {
|
||||||
|
value: JSON.stringify(
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'https://proxy.example/api/provider/ghcp',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'preview-token',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await screen.findByText('GitHub Copilot (OAuth)')).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('raw config editor'), {
|
||||||
|
target: {
|
||||||
|
value: JSON.stringify(
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'https://proxy.example/api/provider/codex',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'preview-token-2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Refreshing')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen.getByText(/Refreshing the live preview from the current editor state\./i)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
secondPreviewResolver?.(
|
||||||
|
createJsonResponse({
|
||||||
|
imageAnalysisStatus: createStatus({
|
||||||
|
backendId: 'codex',
|
||||||
|
backendDisplayName: 'Codex',
|
||||||
|
model: 'gpt-5.4',
|
||||||
|
runtimePath: '/api/provider/codex',
|
||||||
|
authReadiness: 'ready',
|
||||||
|
authProvider: 'codex',
|
||||||
|
authDisplayName: 'Codex',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Codex')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user