diff --git a/desktop/src/__tests__/pages.test.tsx b/desktop/src/__tests__/pages.test.tsx index 01beb273..6372070d 100644 --- a/desktop/src/__tests__/pages.test.tsx +++ b/desktop/src/__tests__/pages.test.tsx @@ -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() + 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() + 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() expect(container.innerHTML).toContain('Architect') diff --git a/desktop/src/components/chat/LocalSlashCommandPanel.tsx b/desktop/src/components/chat/LocalSlashCommandPanel.tsx index a56a1d5d..9ea2cd02 100644 --- a/desktop/src/components/chat/LocalSlashCommandPanel.tsx +++ b/desktop/src/components/chat/LocalSlashCommandPanel.tsx @@ -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 (
-
{children}
+
{children}
{action}
) @@ -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 ( -
-
{label}
-
{value}
- {detail &&
{detail}
} +
+
{label}
+
{value}
+ {detail &&
{detail}
}
) } function InspectorNotice({ children }: { children: React.ReactNode }) { return ( -
- info +
+ info {children}
) @@ -206,13 +193,13 @@ function InspectorNotice({ children }: { children: React.ReactNode }) { function KeyValueRows({ rows }: { rows: Array<[string, React.ReactNode]> }) { return ( -
+
{rows.map(([label, value]) => ( -
-
+
+
{label}
-
{value}
+
{value}
))}
@@ -278,7 +265,7 @@ function UsageTab({ )} {usage.source === 'transcript' && ( -
+
{t('slash.inspector.usage.transcriptNotice')}
)} @@ -302,30 +289,30 @@ function UsageTab({
-
{t('slash.inspector.usage.byModel')}
+
{t('slash.inspector.usage.byModel')}
{models.length === 0 ? ( ) : ( -
+
{models.map((model) => ( -
-
-
{model.displayName || model.model}
-
{t('slash.inspector.usage.tokens')}
+
+
+
{model.displayName || model.model}
+
{t('slash.inspector.usage.tokens')}
-
-
{t('slash.inspector.usage.input')}
-
-
+
+
{t('slash.inspector.usage.input')}
+
+
-
{formatNumber(model.inputTokens)}
+
{formatNumber(model.inputTokens)}
-
{t('slash.inspector.usage.output')}
-
-
+
{t('slash.inspector.usage.output')}
+
+
-
{formatNumber(model.outputTokens)}
+
{formatNumber(model.outputTokens)}
))} @@ -348,7 +335,7 @@ function ContextStackedBar({ categories, rawMaxTokens }: { categories: ContextCa if (activeCategories.length === 0) return null return ( -
+
{activeCategories.map((category) => (
+
{t('slash.inspector.context.categoryTitle')}
{visibleCategories.map((category) => { @@ -385,21 +372,21 @@ function CategoryBreakdown({ categories, rawMaxTokens, t }: { categories: Contex >
- + {category.name}
-
{formatNumber(category.tokens)}
-
{formatPercent(percent)}
+
{formatNumber(category.tokens)}
+
{formatPercent(percent)}
-
+
@@ -414,9 +401,9 @@ function CategoryBreakdown({ categories, rawMaxTokens, t }: { categories: Contex function ContextStatPill({ label, value, detail }: { label: string; value: string; detail?: string }) { return (
-
{label}
-
{value}
- {detail &&
{detail}
} +
{label}
+
{value}
+ {detail &&
{detail}
}
) } @@ -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 ( @@ -451,7 +438,7 @@ function McpServerIcon({ status }: { status: string }) { const isFailed = status === 'failed' const icon = isFailed ? 'power_off' : 'dns' return ( - + {icon} ) @@ -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 ( -
+
{t('slash.inspector.context.windowUsage')} - {context.model} + {context.model}
-
+
{formatNumber(context.totalTokens)} - / + / {formatNumber(context.rawMaxTokens)} - [{formatPercent(usedPercent)} {t('slash.inspector.context.used')}] + [{formatPercent(usedPercent)} {t('slash.inspector.context.used')}]
-
+
-
+
-
+
-
+
@@ -544,7 +531,7 @@ function StatusTab({ label={t('slash.inspector.status.cliStatus')} value={( - + {data.active ? t('slash.inspector.status.running') : t('slash.inspector.status.notRunning')} )} @@ -554,16 +541,16 @@ function StatusTab({ label={t('slash.inspector.status.mcpConnections')} value={( - {formatNumber(connectedMcp)} - / - {formatNumber(failedMcp)} + {formatNumber(connectedMcp)} + / + {formatNumber(failedMcp)} )} detail={( - {t('slash.inspector.status.connected')} - - {t('slash.inspector.status.failed')} + {t('slash.inspector.status.connected')} + + {t('slash.inspector.status.failed')} )} /> @@ -576,7 +563,7 @@ function StatusTab({ [t('slash.inspector.status.version'), data.status.version ?? t('slash.inspector.status.unknown')], [t('slash.inspector.status.sessionId'), {data.status.sessionId}], [t('slash.inspector.status.workingDirectory'), {data.status.cwd ?? data.status.workDir}], - [t('slash.inspector.status.permissionMode'), {data.status.permissionMode}], + [t('slash.inspector.status.permissionMode'), {data.status.permissionMode}], [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 && (
↻ {t('slash.inspector.status.refresh')}} + action={} > {t('slash.inspector.status.mcpServers')} @@ -594,12 +581,12 @@ function StatusTab({
- {server.name} + {server.name}
@@ -628,11 +615,10 @@ function SessionInspectorShell({ }) { return (
-
-
{t('slash.inspector.title')}
+
+
{t('slash.inspector.title')}
{tabs.map((tab) => ( ))}
@@ -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)]" > close
-
{children}
+
{children}
) } diff --git a/desktop/src/pages/McpSettings.tsx b/desktop/src/pages/McpSettings.tsx index f63eb2a3..5c8cb39b 100644 --- a/desktop/src/pages/McpSettings.tsx +++ b/desktop/src/pages/McpSettings.tsx @@ -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'}`} > diff --git a/desktop/src/theme/globals.css b/desktop/src/theme/globals.css index f73540e9..eb36dc86 100644 --- a/desktop/src/theme/globals.css +++ b/desktop/src/theme/globals.css @@ -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;