diff --git a/desktop/src/__tests__/pages.test.tsx b/desktop/src/__tests__/pages.test.tsx
index fd14d03c..221d760f 100644
--- a/desktop/src/__tests__/pages.test.tsx
+++ b/desktop/src/__tests__/pages.test.tsx
@@ -765,7 +765,7 @@ describe('Content-only pages render without errors', () => {
useChatStore.setState({ sessions: {} })
})
- it('ActiveSession treats an empty idle session without a running CLI as a 0% placeholder', async () => {
+ it('ActiveSession treats an empty idle session without a running CLI as pending context', async () => {
const SESSION_ID = 'context-empty-idle-session'
vi.mocked(sessionsApi.getInspection).mockResolvedValueOnce({
active: false,
@@ -833,6 +833,85 @@ describe('Content-only pages render without errors', () => {
useChatStore.setState({ sessions: {} })
})
+ it('ActiveSession treats an empty live zero-token context as pending instead of 0%', async () => {
+ const SESSION_ID = 'context-empty-live-session'
+ vi.mocked(sessionsApi.getInspection).mockResolvedValueOnce({
+ active: true,
+ status: {
+ sessionId: SESSION_ID,
+ workDir: '/workspace/project',
+ cwd: '/workspace/project',
+ permissionMode: 'bypassPermissions',
+ model: 'kimi-k2.6',
+ },
+ context: {
+ categories: [
+ { name: 'Free space', tokens: 128_000, color: '#9B928C', isDeferred: true },
+ ],
+ totalTokens: 0,
+ maxTokens: 128_000,
+ rawMaxTokens: 128_000,
+ percentage: 0,
+ gridRows: [],
+ model: 'kimi-k2.6',
+ memoryFiles: [],
+ mcpTools: [],
+ agents: [],
+ },
+ })
+
+ 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,
+ },
+ },
+ })
+
+ render()
+
+ const indicator = await screen.findByLabelText('Context usage not calculated')
+ expect(indicator).toHaveTextContent('--')
+ expect(screen.queryByLabelText('Context usage 0%')).not.toBeInTheDocument()
+ expect(screen.getAllByText('kimi-k2.6').length).toBeGreaterThan(0)
+ expect(screen.getByText('Context usage will be calculated after the session starts.')).toBeInTheDocument()
+
+ useTabStore.setState({ tabs: [], activeTabId: null })
+ useSessionStore.setState({ sessions: [], activeSessionId: null, isLoading: false, error: null })
+ useChatStore.setState({ sessions: {} })
+ })
+
it('ActiveSession shows context estimate during compaction or reconnect fallback', async () => {
const SESSION_ID = 'context-estimate-session'
vi.mocked(sessionsApi.getInspection).mockResolvedValueOnce({
diff --git a/desktop/src/components/chat/ContextUsageIndicator.tsx b/desktop/src/components/chat/ContextUsageIndicator.tsx
index bb9e7f7e..9bae4ac1 100644
--- a/desktop/src/components/chat/ContextUsageIndicator.tsx
+++ b/desktop/src/components/chat/ContextUsageIndicator.tsx
@@ -53,6 +53,11 @@ function shouldFetchContext(sessionId: string | undefined, draft: boolean) {
return Boolean(sessionId) && !draft
}
+function isEmptyContextSnapshot(context: SessionContextSnapshot | null, messageCount: number) {
+ if (!context || messageCount > 0) return false
+ return context.totalTokens === 0 && context.percentage === 0
+}
+
export function ContextUsageIndicator({
sessionId,
chatState,
@@ -134,17 +139,18 @@ export function ContextUsageIndicator({
}, [chatState, messageCount, refresh])
const details = useMemo(() => {
- if (!context) return []
+ if (!context || isEmptyContextSnapshot(context, messageCount)) return []
return pickUsedContextCategory(context)
- }, [context])
+ }, [context, messageCount])
- const hasPlaceholderContext = !context && (
+ const displayContext = isEmptyContextSnapshot(context, messageCount) ? null : context
+ const hasPlaceholderContext = !displayContext && (
draft || (!loading && messageCount === 0 && (!error || isCliNotRunningError(error)))
)
- const isPendingContext = hasPlaceholderContext && !context
- const percentage = context ? Math.max(0, Math.min(100, context.percentage)) : 0
- const usedTokens = context?.totalTokens ?? 0
- const maxTokens = context?.rawMaxTokens ?? 0
+ const isPendingContext = hasPlaceholderContext && !displayContext
+ const percentage = displayContext ? Math.max(0, Math.min(100, displayContext.percentage)) : 0
+ const usedTokens = displayContext?.totalTokens ?? 0
+ const maxTokens = displayContext?.rawMaxTokens ?? 0
const freeTokens = Math.max(0, maxTokens - usedTokens)
const strokeColor = percentage >= 90
? 'var(--color-error)'
@@ -152,13 +158,13 @@ export function ContextUsageIndicator({
? 'var(--color-warning)'
: 'var(--color-secondary)'
const ringStyle = {
- background: context
+ background: displayContext
? `conic-gradient(${strokeColor} ${percentage * 3.6}deg, var(--color-surface-container-high) 0deg)`
: 'var(--color-surface-container-high)',
}
- const displayPercent = context ? formatPercent(percentage) : '--'
+ const displayPercent = displayContext ? formatPercent(percentage) : '--'
const displayModel = firstNonEmpty(context?.model, inspectionModel, fallbackModelLabel)
- const ariaLabel = context
+ const ariaLabel = displayContext
? t('contextIndicator.ariaLabel', { percent: formatPercent(percentage) })
: isPendingContext
? t('contextIndicator.pendingAria')
@@ -179,7 +185,7 @@ export function ContextUsageIndicator({
}`}
>
- {loading && !context ? (
+ {loading && !displayContext ? (
) : (
)}
@@ -210,11 +216,11 @@ export function ContextUsageIndicator({
- {context ? formatPercent(percentage) : '--'}
+ {displayContext ? formatPercent(percentage) : '--'}
- {context ? (
+ {displayContext ? (
<>