Merge branch 'fix/dark-inspector-theme'

This commit is contained in:
程序员阿江(Relakkes) 2026-05-02 11:46:50 +08:00
commit 48e37be013
4 changed files with 204 additions and 85 deletions

View File

@ -4,6 +4,7 @@ import '@testing-library/jest-dom'
import { skillsApi } from '../api/skills'
import { mcpApi } from '../api/mcp'
import { sessionsApi } from '../api/sessions'
import { useUIStore } from '../stores/uiStore'
vi.mock('../api/skills', () => ({
@ -35,6 +36,35 @@ vi.mock('../api/mcp', () => ({
},
}))
vi.mock('../api/sessions', async (importOriginal) => {
const actual = await importOriginal<typeof import('../api/sessions')>()
return {
...actual,
sessionsApi: {
...actual.sessionsApi,
getInspection: vi.fn(async () => ({
active: true,
status: {
sessionId: 'status-panel-session',
workDir: '/workspace/project',
cwd: '/workspace/project',
permissionMode: 'bypassPermissions',
model: 'kimi-k2.6',
version: '999.0.0-local',
apiKeySource: 'ANTHROPIC_API_KEY',
outputStyle: 'default',
mcpServers: [
{ name: 'deepwiki', status: 'connected' },
{ name: 'chatLog', status: 'failed' },
],
tools: [{ name: 'Read' }, { name: 'Bash' }],
slashCommandCount: 3,
},
})),
},
}
})
// Import all pages
import { EmptySession } from '../pages/EmptySession'
import { ActiveSession } from '../pages/ActiveSession'
@ -510,6 +540,67 @@ describe('Content-only pages render without errors', () => {
useChatStore.setState({ sessions: {} })
})
it('ActiveSession /status inspector uses theme tokens instead of fixed light colors', async () => {
const SESSION_ID = 'status-panel-session'
const sendMessage = vi.fn()
useTabStore.setState({ tabs: [{ sessionId: SESSION_ID, title: 'Test', type: 'session' as const, status: 'idle' }], activeTabId: SESSION_ID })
useSessionStore.setState({
sessions: [{
id: SESSION_ID,
title: 'Test',
createdAt: '2026-04-10T00:00:00.000Z',
modifiedAt: '2026-04-10T00:00:00.000Z',
messageCount: 0,
projectPath: '/workspace/project',
workDir: '/workspace/project',
workDirExists: true,
}],
activeSessionId: SESSION_ID,
isLoading: false,
error: null,
})
useChatStore.setState({
sessions: {
[SESSION_ID]: {
messages: [],
chatState: 'idle',
connectionState: 'connected',
streamingText: '',
streamingToolInput: '',
activeToolUseId: null,
activeToolName: null,
activeThinkingId: null,
pendingPermission: null,
pendingComputerUsePermission: null,
tokenUsage: { input_tokens: 0, output_tokens: 0 },
elapsedSeconds: 0,
statusVerb: '',
slashCommands: [],
agentTaskNotifications: {},
elapsedTimer: null,
},
},
sendMessage,
})
const { container } = render(<ActiveSession />)
const textarea = screen.getByRole('textbox')
fireEvent.change(textarea, { target: { value: '/status', selectionStart: 7 } })
fireEvent.keyDown(textarea, { key: 'Enter', code: 'Enter' })
expect(sendMessage).not.toHaveBeenCalled()
expect(await screen.findByText('Session inspector')).toBeInTheDocument()
expect(vi.mocked(sessionsApi.getInspection)).toHaveBeenCalledWith(SESSION_ID, { includeContext: false })
expect(container.innerHTML).toContain('bg-[var(--color-inspector-surface)]')
expect(container.innerHTML).not.toContain('bg-[#fbfaf6]')
expect(container.innerHTML).not.toContain('bg-[#f4f2ed]')
expect(container.innerHTML).not.toContain('border-[#d8b3a8]')
useTabStore.setState({ tabs: [], activeTabId: null })
useSessionStore.setState({ sessions: [], activeSessionId: null, isLoading: false, error: null })
useChatStore.setState({ sessions: {} })
})
it('AgentTeams renders team strip and members', () => {
const { container } = render(<AgentTeams />)
expect(container.innerHTML).toContain('Architect')

View File

@ -120,19 +120,6 @@ function ErrorState({ message }: { message: string }) {
)
}
const inspector = {
bg: '#fbfaf6',
panel: '#f6f4ee',
line: '#d8b3a8',
rust: '#8f3217',
ink: '#1f1713',
muted: '#7b665f',
green: '#25451b',
greenBg: '#d8f2b6',
red: '#c51616',
redBg: '#ffd9d3',
}
function formatNumber(value: number | undefined) {
return new Intl.NumberFormat().format(value ?? 0)
}
@ -179,7 +166,7 @@ function assertSessionInspectionResponse(value: unknown, t: Translate): SessionI
function InspectorSectionTitle({ children, action }: { children: React.ReactNode; action?: React.ReactNode }) {
return (
<div className="mb-3 flex items-center justify-between gap-4">
<div className="font-mono text-[12px] font-semibold uppercase tracking-[0.24em] text-[#2b1b15]">{children}</div>
<div className="font-mono text-[12px] font-semibold uppercase tracking-[0.24em] text-[var(--color-inspector-heading)]">{children}</div>
{action}
</div>
)
@ -187,18 +174,18 @@ function InspectorSectionTitle({ children, action }: { children: React.ReactNode
function MetricCard({ label, value, detail }: { label: string; value: React.ReactNode; detail?: React.ReactNode }) {
return (
<div className="min-h-[82px] rounded-md border border-[#d8b3a8] bg-[#f4f2ed] px-4 py-4 font-mono">
<div className="text-[12px] uppercase tracking-[0.2em] text-[#2b1b15]">{label}</div>
<div className="mt-3 whitespace-pre-line text-[15px] leading-6 text-[#1f1713]">{value}</div>
{detail && <div className="mt-1 text-[13px] leading-5 text-[#7b665f]">{detail}</div>}
<div className="min-h-[82px] rounded-md border border-[var(--color-inspector-border)] bg-[var(--color-inspector-panel)] px-4 py-4 font-mono">
<div className="text-[12px] uppercase tracking-[0.2em] text-[var(--color-inspector-heading)]">{label}</div>
<div className="mt-3 whitespace-pre-line text-[15px] leading-6 text-[var(--color-inspector-text)]">{value}</div>
{detail && <div className="mt-1 text-[13px] leading-5 text-[var(--color-inspector-muted)]">{detail}</div>}
</div>
)
}
function InspectorNotice({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-3 rounded-md border border-[#d8b3a8] bg-[#fbfaf6] px-4 py-3 text-[14px] text-[#2b1b15]">
<span className="material-symbols-outlined text-[18px] text-[#7b665f]">info</span>
<div className="flex items-center gap-3 rounded-md border border-[var(--color-inspector-border)] bg-[var(--color-inspector-surface)] px-4 py-3 text-[14px] text-[var(--color-inspector-heading)]">
<span className="material-symbols-outlined text-[18px] text-[var(--color-inspector-muted)]">info</span>
<span>{children}</span>
</div>
)
@ -206,13 +193,13 @@ function InspectorNotice({ children }: { children: React.ReactNode }) {
function KeyValueRows({ rows }: { rows: Array<[string, React.ReactNode]> }) {
return (
<div className="overflow-hidden rounded-md border border-[#d8b3a8] bg-[#fbfaf6] font-mono">
<div className="overflow-hidden rounded-md border border-[var(--color-inspector-border)] bg-[var(--color-inspector-surface)] font-mono">
{rows.map(([label, value]) => (
<div key={label} className="grid grid-cols-[220px_minmax(0,1fr)] border-t border-[#d8b3a8] first:border-t-0">
<div className="border-r border-[#d8b3a8] bg-[#f4f2ed] px-4 py-3 text-[12px] font-semibold uppercase tracking-[0.24em] text-[#2b1b15]">
<div key={label} className="grid grid-cols-[220px_minmax(0,1fr)] border-t border-[var(--color-inspector-border)] first:border-t-0">
<div className="border-r border-[var(--color-inspector-border)] bg-[var(--color-inspector-panel)] px-4 py-3 text-[12px] font-semibold uppercase tracking-[0.24em] text-[var(--color-inspector-heading)]">
{label}
</div>
<div className="min-w-0 break-words px-4 py-3 text-[14px] text-[#1f1713]">{value}</div>
<div className="min-w-0 break-words px-4 py-3 text-[14px] text-[var(--color-inspector-text)]">{value}</div>
</div>
))}
</div>
@ -278,7 +265,7 @@ function UsageTab({
</InspectorNotice>
)}
{usage.source === 'transcript' && (
<div className="rounded-md border border-[#d8b3a8] bg-[#fbfaf6] px-4 py-3 text-sm text-[#5f514c]">
<div className="rounded-md border border-[var(--color-inspector-border)] bg-[var(--color-inspector-surface)] px-4 py-3 text-sm text-[var(--color-inspector-muted-strong)]">
{t('slash.inspector.usage.transcriptNotice')}
</div>
)}
@ -302,30 +289,30 @@ function UsageTab({
<MetricCard label={t('slash.inspector.usage.webSearch')} value={formatNumber(usage.totalWebSearchRequests)} />
</div>
<section>
<div className="mb-3 text-[22px] font-semibold text-[#1f1713]">{t('slash.inspector.usage.byModel')}</div>
<div className="mb-3 text-[22px] font-semibold text-[var(--color-inspector-text)]">{t('slash.inspector.usage.byModel')}</div>
{models.length === 0 ? (
<EmptyState title={t('slash.inspector.usage.noModelTitle')} body={t('slash.inspector.usage.noModelBody')} />
) : (
<div className="overflow-hidden rounded-md border border-[#d8b3a8] bg-[#fbfaf6] font-mono">
<div className="overflow-hidden rounded-md border border-[var(--color-inspector-border)] bg-[var(--color-inspector-surface)] font-mono">
{models.map((model) => (
<div key={model.model} className="border-t border-[#d8b3a8] first:border-t-0">
<div className="grid grid-cols-[minmax(0,1fr)_120px] items-center gap-4 border-b border-[#d8b3a8] px-4 py-3">
<div className="min-w-0 truncate text-[13px] font-semibold text-[#1f1713]">{model.displayName || model.model}</div>
<div className="text-right text-[12px] font-semibold uppercase tracking-[0.18em] text-[#2b1b15]">{t('slash.inspector.usage.tokens')}</div>
<div key={model.model} className="border-t border-[var(--color-inspector-border)] first:border-t-0">
<div className="grid grid-cols-[minmax(0,1fr)_120px] items-center gap-4 border-b border-[var(--color-inspector-border)] px-4 py-3">
<div className="min-w-0 truncate text-[13px] font-semibold text-[var(--color-inspector-text)]">{model.displayName || model.model}</div>
<div className="text-right text-[12px] font-semibold uppercase tracking-[0.18em] text-[var(--color-inspector-heading)]">{t('slash.inspector.usage.tokens')}</div>
</div>
<div className="grid grid-cols-[160px_minmax(0,1fr)_120px] items-center gap-4 border-b border-[#d8b3a8] px-4 py-3 last:border-b-0">
<div className="text-[12px] uppercase tracking-[0.18em] text-[#2b1b15]">{t('slash.inspector.usage.input')}</div>
<div className="h-1 overflow-hidden rounded-full bg-[#ebe7df]">
<div className="h-full rounded-full bg-[#8f3217]" style={{ width: '95%' }} />
<div className="grid grid-cols-[160px_minmax(0,1fr)_120px] items-center gap-4 border-b border-[var(--color-inspector-border)] px-4 py-3 last:border-b-0">
<div className="text-[12px] uppercase tracking-[0.18em] text-[var(--color-inspector-heading)]">{t('slash.inspector.usage.input')}</div>
<div className="h-1 overflow-hidden rounded-full bg-[var(--color-inspector-chip)]">
<div className="h-full rounded-full bg-[var(--color-inspector-accent)]" style={{ width: '95%' }} />
</div>
<div className="text-right text-[13px] text-[#1f1713]">{formatNumber(model.inputTokens)}</div>
<div className="text-right text-[13px] text-[var(--color-inspector-text)]">{formatNumber(model.inputTokens)}</div>
</div>
<div className="grid grid-cols-[160px_minmax(0,1fr)_120px] items-center gap-4 px-4 py-3">
<div className="text-[12px] uppercase tracking-[0.18em] text-[#2b1b15]">{t('slash.inspector.usage.output')}</div>
<div className="h-1 overflow-hidden rounded-full bg-[#ebe7df]">
<div className="h-full rounded-full bg-[#0f5c8f]" style={{ width: `${Math.max(4, Math.min(100, (model.outputTokens / Math.max(1, model.inputTokens)) * 100))}%` }} />
<div className="text-[12px] uppercase tracking-[0.18em] text-[var(--color-inspector-heading)]">{t('slash.inspector.usage.output')}</div>
<div className="h-1 overflow-hidden rounded-full bg-[var(--color-inspector-chip)]">
<div className="h-full rounded-full bg-[var(--color-inspector-accent-secondary)]" style={{ width: `${Math.max(4, Math.min(100, (model.outputTokens / Math.max(1, model.inputTokens)) * 100))}%` }} />
</div>
<div className="text-right text-[13px] text-[#1f1713]">{formatNumber(model.outputTokens)}</div>
<div className="text-right text-[13px] text-[var(--color-inspector-text)]">{formatNumber(model.outputTokens)}</div>
</div>
</div>
))}
@ -348,7 +335,7 @@ function ContextStackedBar({ categories, rawMaxTokens }: { categories: ContextCa
if (activeCategories.length === 0) return null
return (
<div className="overflow-hidden rounded-full bg-[#ebe7df]">
<div className="overflow-hidden rounded-full bg-[var(--color-inspector-chip)]">
<div className="flex h-2.5 w-full">
{activeCategories.map((category) => (
<div
@ -372,7 +359,7 @@ function CategoryBreakdown({ categories, rawMaxTokens, t }: { categories: Contex
}
return (
<div className="rounded-md border border-[#d8b3a8] bg-[#f4f2ed] px-5 py-5 font-mono">
<div className="rounded-md border border-[var(--color-inspector-border)] bg-[var(--color-inspector-panel)] px-5 py-5 font-mono">
<InspectorSectionTitle>{t('slash.inspector.context.categoryTitle')}</InspectorSectionTitle>
<div className="grid gap-x-10 gap-y-5 sm:grid-cols-2">
{visibleCategories.map((category) => {
@ -385,21 +372,21 @@ function CategoryBreakdown({ categories, rawMaxTokens, t }: { categories: Contex
>
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3">
<div className="flex min-w-0 items-center gap-2">
<span className={`min-w-0 truncate text-[14px] font-semibold ${muted ? 'text-[#5f514c]' : 'text-[#1f1713]'}`}>
<span className={`min-w-0 truncate text-[14px] font-semibold ${muted ? 'text-[var(--color-inspector-muted-strong)]' : 'text-[var(--color-inspector-text)]'}`}>
{category.name}
</span>
</div>
<div className="shrink-0 text-right leading-tight">
<div className="text-sm text-[#1f1713]">{formatNumber(category.tokens)}</div>
<div className="mt-0.5 text-[12px] text-[#7b665f]">{formatPercent(percent)}</div>
<div className="text-sm text-[var(--color-inspector-text)]">{formatNumber(category.tokens)}</div>
<div className="mt-0.5 text-[12px] text-[var(--color-inspector-muted)]">{formatPercent(percent)}</div>
</div>
</div>
<div className="mt-2 h-1 overflow-hidden rounded-full bg-[#ebe7df]">
<div className="mt-2 h-1 overflow-hidden rounded-full bg-[var(--color-inspector-chip)]">
<div
className={muted ? 'h-full rounded-full opacity-65' : 'h-full rounded-full'}
style={{
width: `${Math.min(100, Math.max(0.5, percent))}%`,
backgroundColor: muted ? '#9b928c' : '#8f3217',
backgroundColor: muted ? 'var(--color-inspector-capacity)' : 'var(--color-inspector-accent)',
}}
/>
</div>
@ -414,9 +401,9 @@ function CategoryBreakdown({ categories, rawMaxTokens, t }: { categories: Contex
function ContextStatPill({ label, value, detail }: { label: string; value: string; detail?: string }) {
return (
<div className="min-w-0 font-mono">
<div className="truncate text-[12px] font-semibold uppercase tracking-[0.22em] text-[#7b665f]">{label}</div>
<div className="mt-2 truncate text-[16px] font-semibold text-[#1f1713]">{value}</div>
{detail && <div className="mt-1 truncate text-[13px] text-[#7b665f]">{detail}</div>}
<div className="truncate text-[12px] font-semibold uppercase tracking-[0.22em] text-[var(--color-inspector-muted)]">{label}</div>
<div className="mt-2 truncate text-[16px] font-semibold text-[var(--color-inspector-text)]">{value}</div>
{detail && <div className="mt-1 truncate text-[13px] text-[var(--color-inspector-muted)]">{detail}</div>}
</div>
)
}
@ -433,11 +420,11 @@ function InspectorStatusBadge({ status, t }: { status: string; t: Translate }) {
const isConnected = normalized === 'connected'
const isFailed = normalized === 'failed'
const badgeClass = isConnected
? 'bg-[#d8f2b6] text-[#25451b]'
? 'bg-[var(--color-inspector-success-bg)] text-[var(--color-inspector-success)]'
: isFailed
? 'bg-[#ffd9d3] text-[#c51616]'
: 'bg-[#ebe7df] text-[#5f514c]'
const dotClass = isConnected ? 'bg-[#25451b]' : isFailed ? 'bg-[#c51616]' : 'bg-[#7b665f]'
? 'bg-[var(--color-inspector-danger-bg)] text-[var(--color-inspector-danger)]'
: 'bg-[var(--color-inspector-chip)] text-[var(--color-inspector-muted-strong)]'
const dotClass = isConnected ? 'bg-[var(--color-inspector-success)]' : isFailed ? 'bg-[var(--color-inspector-danger)]' : 'bg-[var(--color-inspector-muted)]'
return (
<span className={`inline-flex items-center gap-1.5 rounded-sm px-2.5 py-1 font-mono text-[10px] font-semibold uppercase tracking-[0.08em] ${badgeClass}`}>
@ -451,7 +438,7 @@ function McpServerIcon({ status }: { status: string }) {
const isFailed = status === 'failed'
const icon = isFailed ? 'power_off' : 'dns'
return (
<span className={`material-symbols-outlined text-[20px] ${isFailed ? 'text-[#c51616]' : 'text-[#25451b]'}`}>
<span className={`material-symbols-outlined text-[20px] ${isFailed ? 'text-[var(--color-inspector-danger)]' : 'text-[var(--color-inspector-success)]'}`}>
{icon}
</span>
)
@ -462,31 +449,31 @@ function ContextOverview({ context, categories, t }: { context: SessionContextSn
const freeTokens = Math.max(0, context.rawMaxTokens - context.totalTokens)
const freePercent = context.rawMaxTokens > 0 ? (freeTokens / context.rawMaxTokens) * 100 : 0
return (
<div className="rounded-md border border-[#d8b3a8] bg-[#f4f2ed] px-5 py-6">
<div className="rounded-md border border-[var(--color-inspector-border)] bg-[var(--color-inspector-panel)] px-5 py-6">
<div className="mb-8 flex items-start justify-between gap-4">
<InspectorSectionTitle>{t('slash.inspector.context.windowUsage')}</InspectorSectionTitle>
<span className="rounded-sm border border-[#d8b3a8] bg-[#ebe7df] px-2 py-1 font-mono text-xs text-[#5f514c]">{context.model}</span>
<span className="rounded-sm border border-[var(--color-inspector-border)] bg-[var(--color-inspector-chip)] px-2 py-1 font-mono text-xs text-[var(--color-inspector-muted-strong)]">{context.model}</span>
</div>
<div className="font-mono text-[24px] font-semibold text-[#1f1713]">
<div className="font-mono text-[24px] font-semibold text-[var(--color-inspector-text)]">
{formatNumber(context.totalTokens)}
<span className="mx-1.5 text-[#1f1713]">/</span>
<span className="mx-1.5 text-[var(--color-inspector-text)]">/</span>
<span>{formatNumber(context.rawMaxTokens)}</span>
<span className="ml-3 align-middle text-sm font-normal text-[#0f5c8f]">[{formatPercent(usedPercent)} {t('slash.inspector.context.used')}]</span>
<span className="ml-3 align-middle text-sm font-normal text-[var(--color-inspector-accent-secondary)]">[{formatPercent(usedPercent)} {t('slash.inspector.context.used')}]</span>
</div>
<div className="mt-7">
<ContextStackedBar categories={categories} rawMaxTokens={context.rawMaxTokens} />
</div>
<div className="mt-8 grid grid-cols-2 gap-3 lg:grid-cols-4">
<div className="rounded-md border border-[#d8b3a8] bg-[#fbfaf6] px-4 py-3">
<div className="rounded-md border border-[var(--color-inspector-border)] bg-[var(--color-inspector-surface)] px-4 py-3">
<ContextStatPill label={t('slash.inspector.context.free')} value={formatNumber(freeTokens)} detail={formatPercent(freePercent)} />
</div>
<div className="rounded-md border border-[#d8b3a8] bg-[#fbfaf6] px-4 py-3">
<div className="rounded-md border border-[var(--color-inspector-border)] bg-[var(--color-inspector-surface)] px-4 py-3">
<ContextStatPill label={t('slash.inspector.context.messages')} value={formatNumber(context.messageBreakdown?.assistantMessageTokens ?? 0)} detail={t('slash.inspector.context.assistant')} />
</div>
<div className="rounded-md border border-[#d8b3a8] bg-[#fbfaf6] px-4 py-3">
<div className="rounded-md border border-[var(--color-inspector-border)] bg-[var(--color-inspector-surface)] px-4 py-3">
<ContextStatPill label={t('slash.inspector.context.toolResults')} value={formatNumber(context.messageBreakdown?.toolResultTokens ?? 0)} />
</div>
<div className="rounded-md border border-[#d8b3a8] bg-[#fbfaf6] px-4 py-3">
<div className="rounded-md border border-[var(--color-inspector-border)] bg-[var(--color-inspector-surface)] px-4 py-3">
<ContextStatPill label={t('slash.inspector.context.context')} value={formatPercent(usedPercent)} />
</div>
</div>
@ -544,7 +531,7 @@ function StatusTab({
label={t('slash.inspector.status.cliStatus')}
value={(
<span className="inline-flex items-center gap-2">
<span className={`h-2 w-2 rounded-full ${data.active ? 'bg-[#25451b]' : 'bg-[#c51616]'}`} />
<span className={`h-2 w-2 rounded-full ${data.active ? 'bg-[var(--color-inspector-success)]' : 'bg-[var(--color-inspector-danger)]'}`} />
{data.active ? t('slash.inspector.status.running') : t('slash.inspector.status.notRunning')}
</span>
)}
@ -554,16 +541,16 @@ function StatusTab({
label={t('slash.inspector.status.mcpConnections')}
value={(
<span>
<span className="text-[#25451b]">{formatNumber(connectedMcp)}</span>
<span className="mx-5 text-[#1f1713]">/</span>
<span className="text-[#c51616]">{formatNumber(failedMcp)}</span>
<span className="text-[var(--color-inspector-success)]">{formatNumber(connectedMcp)}</span>
<span className="mx-5 text-[var(--color-inspector-text)]">/</span>
<span className="text-[var(--color-inspector-danger)]">{formatNumber(failedMcp)}</span>
</span>
)}
detail={(
<span>
<span className="text-[#25451b]">{t('slash.inspector.status.connected')}</span>
<span className="mx-5 text-[#1f1713]" />
<span className="text-[#c51616]">{t('slash.inspector.status.failed')}</span>
<span className="text-[var(--color-inspector-success)]">{t('slash.inspector.status.connected')}</span>
<span className="mx-5 text-[var(--color-inspector-text)]" />
<span className="text-[var(--color-inspector-danger)]">{t('slash.inspector.status.failed')}</span>
</span>
)}
/>
@ -576,7 +563,7 @@ function StatusTab({
[t('slash.inspector.status.version'), data.status.version ?? t('slash.inspector.status.unknown')],
[t('slash.inspector.status.sessionId'), <span className="font-mono text-[13px]">{data.status.sessionId}</span>],
[t('slash.inspector.status.workingDirectory'), <span className="font-mono text-[13px]">{data.status.cwd ?? data.status.workDir}</span>],
[t('slash.inspector.status.permissionMode'), <span className="rounded-sm bg-[#ebe7df] px-1.5 py-1">{data.status.permissionMode}</span>],
[t('slash.inspector.status.permissionMode'), <span className="rounded-sm bg-[var(--color-inspector-chip)] px-1.5 py-1">{data.status.permissionMode}</span>],
[t('slash.inspector.status.authToken'), data.status.apiKeySource ?? t('slash.inspector.status.unknown')],
[t('slash.inspector.status.outputStyle'), data.status.outputStyle ?? t('slash.inspector.status.default')],
]}
@ -585,7 +572,7 @@ function StatusTab({
{mcpServers.length > 0 && (
<section>
<InspectorSectionTitle
action={<button type="button" className="font-mono text-[12px] tracking-[0.18em] text-[#8f3217] hover:text-[#5b1e0d]"> {t('slash.inspector.status.refresh')}</button>}
action={<button type="button" className="font-mono text-[12px] tracking-[0.18em] text-[var(--color-inspector-accent)] hover:text-[var(--color-inspector-accent-hover)]"> {t('slash.inspector.status.refresh')}</button>}
>
{t('slash.inspector.status.mcpServers')}
</InspectorSectionTitle>
@ -594,12 +581,12 @@ function StatusTab({
<div
key={`${server.name}:${server.status}`}
className={`flex min-h-[48px] items-center justify-between gap-4 rounded-md border px-4 py-3 font-mono ${
server.status === 'failed' ? 'border-[#f1b8b0] bg-[#fff7f5]' : 'border-[#d8b3a8] bg-[#f4f2ed]'
server.status === 'failed' ? 'border-[var(--color-inspector-danger-border)] bg-[var(--color-inspector-danger-surface)]' : 'border-[var(--color-inspector-border)] bg-[var(--color-inspector-panel)]'
}`}
>
<div className="flex min-w-0 items-center gap-3">
<McpServerIcon status={server.status} />
<span className="min-w-0 truncate text-[14px] text-[#1f1713]">{server.name}</span>
<span className="min-w-0 truncate text-[14px] text-[var(--color-inspector-text)]">{server.name}</span>
</div>
<InspectorStatusBadge status={server.status} t={t} />
</div>
@ -628,11 +615,10 @@ function SessionInspectorShell({
}) {
return (
<div
className="absolute bottom-full left-0 right-0 z-50 mb-4 overflow-hidden rounded-[10px] border bg-[#fbfaf6] shadow-[0_28px_80px_rgba(65,54,48,0.22)]"
style={{ borderColor: inspector.line, color: inspector.ink }}
className="absolute bottom-full left-0 right-0 z-50 mb-4 overflow-hidden rounded-[10px] border border-[var(--color-inspector-border)] bg-[var(--color-inspector-surface)] text-[var(--color-inspector-text)] shadow-[var(--shadow-inspector)]"
>
<div className="grid min-h-[64px] grid-cols-[1fr_auto_1fr] items-center border-b border-[#d8b3a8] bg-[#fbfaf6] px-6">
<div className="font-mono text-[16px] font-semibold uppercase text-[#8f3217]">{t('slash.inspector.title')}</div>
<div className="grid min-h-[64px] grid-cols-[1fr_auto_1fr] items-center border-b border-[var(--color-inspector-border)] bg-[var(--color-inspector-surface)] px-6">
<div className="font-mono text-[16px] font-semibold uppercase text-[var(--color-inspector-accent)]">{t('slash.inspector.title')}</div>
<div className="flex items-center gap-8">
{tabs.map((tab) => (
<button
@ -640,11 +626,11 @@ function SessionInspectorShell({
type="button"
onClick={() => onSelectTab(tab.id)}
className={`relative h-10 px-0 font-sans text-sm transition-colors ${
selectedTab === tab.id ? 'text-[#8f3217]' : 'text-[#5f514c] hover:text-[#8f3217]'
selectedTab === tab.id ? 'text-[var(--color-inspector-accent)]' : 'text-[var(--color-inspector-muted-strong)] hover:text-[var(--color-inspector-accent)]'
}`}
>
{tab.label}
{selectedTab === tab.id && <span className="absolute bottom-1 left-0 right-0 h-[2px] bg-[#8f3217]" />}
{selectedTab === tab.id && <span className="absolute bottom-1 left-0 right-0 h-[2px] bg-[var(--color-inspector-accent)]" />}
</button>
))}
</div>
@ -653,13 +639,13 @@ function SessionInspectorShell({
type="button"
onClick={onClose}
aria-label={t('slash.inspector.close')}
className="flex h-10 w-10 items-center justify-center text-[#8f3217] transition-colors hover:text-[#5b1e0d]"
className="flex h-10 w-10 items-center justify-center text-[var(--color-inspector-accent)] transition-colors hover:text-[var(--color-inspector-accent-hover)]"
>
<span className="material-symbols-outlined text-[24px]">close</span>
</button>
</div>
</div>
<div className="max-h-[min(540px,58vh)] overflow-y-auto bg-[#fbfaf6] px-6 py-6">{children}</div>
<div className="max-h-[min(540px,58vh)] overflow-y-auto bg-[var(--color-inspector-surface)] px-6 py-6">{children}</div>
</div>
)
}

View File

@ -262,11 +262,11 @@ function ToggleSwitch({
disabled={disabled}
onClick={onChange}
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-colors ${
checked ? 'bg-[#90c1f7]' : 'bg-[var(--color-border)]'
checked ? 'bg-[var(--color-switch-checked-bg)]' : 'bg-[var(--color-border)]'
} ${disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'}`}
>
<span
className={`inline-block h-6 w-6 transform rounded-full bg-white transition-transform ${
className={`inline-block h-6 w-6 transform rounded-full bg-[var(--color-switch-thumb)] shadow-sm transition-transform ${
checked ? 'translate-x-7' : 'translate-x-1'
}`}
/>

View File

@ -175,6 +175,27 @@
--color-surface-selected: var(--color-surface-container);
--color-model-option-selected-bg: var(--color-primary-fixed);
--color-model-option-selected-border: rgba(143, 72, 47, 0.2);
--color-inspector-surface: #FBFAF6;
--color-inspector-panel: #F4F2ED;
--color-inspector-chip: #EBE7DF;
--color-inspector-border: #D8B3A8;
--color-inspector-accent: #8F3217;
--color-inspector-accent-hover: #5B1E0D;
--color-inspector-accent-secondary: #0F5C8F;
--color-inspector-text: #1F1713;
--color-inspector-heading: #2B1B15;
--color-inspector-muted: #7B665F;
--color-inspector-muted-strong: #5F514C;
--color-inspector-success: #25451B;
--color-inspector-success-bg: #D8F2B6;
--color-inspector-danger: #C51616;
--color-inspector-danger-bg: #FFD9D3;
--color-inspector-danger-surface: #FFF7F5;
--color-inspector-danger-border: #F1B8B0;
--color-inspector-capacity: #9B928C;
--shadow-inspector: 0 28px 80px rgba(65, 54, 48, 0.22);
--color-switch-checked-bg: #90C1F7;
--color-switch-thumb: #FFFFFF;
--color-sidebar-filter-bg: transparent;
--color-sidebar-filter-border: rgba(218, 193, 186, 0.92);
--color-sidebar-filter-icon-bg: transparent;
@ -321,6 +342,27 @@
--color-surface-selected: var(--color-surface-container);
--color-model-option-selected-bg: rgba(255, 181, 159, 0.13);
--color-model-option-selected-border: rgba(255, 181, 159, 0.34);
--color-inspector-surface: #131211;
--color-inspector-panel: #1C1B1B;
--color-inspector-chip: #252120;
--color-inspector-border: rgba(255, 181, 159, 0.18);
--color-inspector-accent: #FFB59F;
--color-inspector-accent-hover: #FFD4C8;
--color-inspector-accent-secondary: #74E0F7;
--color-inspector-text: #E5E2E1;
--color-inspector-heading: #F3D6CC;
--color-inspector-muted: #A89A95;
--color-inspector-muted-strong: #C5B8B2;
--color-inspector-success: #7EDB8B;
--color-inspector-success-bg: rgba(126, 219, 139, 0.16);
--color-inspector-danger: #FFB4AB;
--color-inspector-danger-bg: rgba(255, 180, 171, 0.16);
--color-inspector-danger-surface: rgba(255, 180, 171, 0.08);
--color-inspector-danger-border: rgba(255, 180, 171, 0.22);
--color-inspector-capacity: #8F8683;
--shadow-inspector: 0 28px 80px rgba(0, 0, 0, 0.58);
--color-switch-checked-bg: rgba(116, 224, 247, 0.36);
--color-switch-thumb: #E5E2E1;
--color-sidebar-filter-bg: rgba(255, 255, 255, 0.01);
--color-sidebar-filter-border: rgba(255, 255, 255, 0.08);
--color-sidebar-filter-icon-bg: transparent;