/** * Official-slot entries for the Codex true-gateway: a session-header action * (direct-connect badge), a composer-dock status line, and an input-dock * queue strip. Status display lives in the official slots; controls live in * the floating window (`ControlPanel`). * * @module dsh-subagent-codex-plus/client/components */ import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import { runGatewayAction, togglePanel, useGatewayView, usePanelState, } from './gateway-store.ts' import type { GatewaySessionView } from '../shared/types.ts' import { THEME } from './theme.ts' /** Short stable thread label for badges. */ function shortThread(threadId: string): string { return threadId.length > 12 ? `${threadId.slice(0, 10)}…` : threadId } const BADGE_BASE = { display: 'inline-flex', alignItems: 'center', gap: 4, height: 24, padding: '0 8px', borderRadius: 6, border: `1px solid ${THEME.borderStrong}`, background: 'transparent', color: THEME.textPrimary, fontSize: 12, lineHeight: '24px', whiteSpace: 'nowrap' as const, cursor: 'pointer', } function dotColor(view: GatewaySessionView | null): string { if (view?.attached) return view.running ? '#e6a23c' : '#2ea043' if (view?.threadId !== undefined) return '#9aa4af' return '#9aa4af' } function badgeLabel(view: GatewaySessionView | null): string { if (view?.attached) { return `CDX-${(view.threadId ?? '').slice(0, 4)}` } if (view?.threadId !== undefined) { return `CDX-${view.threadId.slice(0, 4)}` } return '直连 CDX' } /** Session-header action: the direct-connect badge (status display). */ export function HeaderAction(props: PropsRuntime<'conversation.session.header.actions'>) { const { view } = useGatewayView(props.sessionId) const panel = usePanelState() // 只在存在绑定(含已保存未连接)时显示标题栏徽标;未绑定的会话不占槽位。 if (view?.threadId === undefined) return null return ( ) } const STATUS_LINE = { boxSizing: 'border-box' as const, color: THEME.textTertiary, fontSize: 12, fontVariantNumeric: 'tabular-nums', lineHeight: '20px', margin: '0 auto', maxWidth: 'var(--dsh-chat-content-width, 900px)', overflow: 'hidden', padding: '0 var(--dsh-composer-side-clearance, 16px)', textAlign: 'center' as const, textOverflow: 'ellipsis', whiteSpace: 'nowrap' as const, width: '100%', } /** * Long-tool heartbeat: shows the executing Codex tool in the dock status * line and, once it has run for a minute, the elapsed time — so a * multi-minute local command never looks like a hang. */ export function toolHeartbeat(view: GatewaySessionView | null): string | null { const tool = view?.tool if (view?.attached !== true || view.running !== true || tool === undefined) return null const elapsedSec = Math.max(0, Math.floor((Date.now() - tool.startedAt) / 1000)) const label = `正在执行 ${tool.name}` if (elapsedSec < 60) return label const minutes = Math.floor(elapsedSec / 60) const seconds = elapsedSec % 60 return `${label} · 已运行 ${minutes} 分 ${seconds} 秒` } function statusText(view: GatewaySessionView | null): string | null { if (view?.attached) { const parts = [ `Codex 直连 · ${shortThread(view.threadId ?? '')}`, view.running ? '运行中' : '空闲', ] const heartbeat = toolHeartbeat(view) if (heartbeat !== null) parts.push(heartbeat) if (view.queue.length > 0) parts.push(`队列 ${view.queue.length}`) return parts.join(' · ') } if (view?.threadId !== undefined) { return `Codex 直连已保存(线程 ${shortThread(view.threadId)}),重启后自动恢复` } return null } /** Composer-dock status line: only when the session is bound or attached. */ export function DockStatus(props: PropsRuntime<'conversation.composer.dock'>) { const { view } = useGatewayView(props.sessionId) const text = statusText(view) if (text === null) return null return (