diff --git a/desktop/src/components/controls/ReasoningEffortPopover.test.tsx b/desktop/src/components/controls/ReasoningEffortPopover.test.tsx index 3896960c..09ef39c8 100644 --- a/desktop/src/components/controls/ReasoningEffortPopover.test.tsx +++ b/desktop/src/components/controls/ReasoningEffortPopover.test.tsx @@ -47,7 +47,10 @@ describe('ReasoningEffortPopover', () => { expect(popover).toHaveStyle({ width: '280px' }) expect(popover).toHaveClass('px-4', 'pb-4', 'pt-3.5') expect(popover.querySelectorAll('svg')).toHaveLength(0) - expect(screen.getByTestId('reasoning-effort-label')).toHaveClass('mb-3', 'text-[15px]') + expect(screen.getByTestId('reasoning-effort-header')).toHaveClass('mb-3', 'justify-between') + expect(screen.getByTestId('reasoning-effort-label')).toHaveClass('text-[15px]') + expect(screen.getByTestId('reasoning-effort-context-label')).toHaveClass('text-[11px]') + expect(screen.getByTestId('reasoning-effort-context-label')).toHaveTextContent('推理强度') expect(screen.getByRole('slider', { name: '推理强度' })).toHaveClass('h-11') expect(screen.getByTestId('reasoning-effort-track')).toHaveClass('h-[30px]') expect(screen.getByTestId('reasoning-effort-thumb')).toHaveClass('h-10', 'w-10') @@ -63,7 +66,8 @@ describe('ReasoningEffortPopover', () => { expect(slider).toHaveAttribute('aria-valuetext', '极高') expect(screen.getAllByTestId('reasoning-effort-stop')).toHaveLength(5) expect(screen.getByText('极高')).toBeInTheDocument() - expect(screen.getByTestId('reasoning-effort-fill')).toHaveClass('bg-[#3798f7]') + expect(screen.getByTestId('reasoning-effort-fill')).toHaveClass('bg-[var(--color-brand)]') + expect(slider).toHaveClass('focus-visible:ring-[var(--color-brand)]') }) it('selects a discrete stop from the track', () => { diff --git a/desktop/src/components/controls/ReasoningEffortPopover.tsx b/desktop/src/components/controls/ReasoningEffortPopover.tsx index c26dff8f..cfaf3cd9 100644 --- a/desktop/src/components/controls/ReasoningEffortPopover.tsx +++ b/desktop/src/components/controls/ReasoningEffortPopover.tsx @@ -111,10 +111,21 @@ export function ReasoningEffortPopover({ style={{ bottom: position.bottom, left: position.left, width: position.width }} >
- {labels[value]} +
+ {labels[value]} +
+
+ {ariaLabel} +
selectFromClientX(event.clientX)} onPointerDown={(event) => { draggingRef.current = true @@ -175,7 +186,7 @@ export function ReasoningEffortPopover({ >
diff --git a/desktop/src/stores/sessionRuntimeStore.ts b/desktop/src/stores/sessionRuntimeStore.ts index 0a0baee7..d6d864f0 100644 --- a/desktop/src/stores/sessionRuntimeStore.ts +++ b/desktop/src/stores/sessionRuntimeStore.ts @@ -1,5 +1,6 @@ import { create } from 'zustand' import type { RuntimeSelection } from '../types/runtime' +import type { SessionListItem } from '../types/session' const STORAGE_KEY = 'cc-haha-session-runtime' @@ -10,6 +11,7 @@ type SessionRuntimeStore = { setSelection: (key: string, selection: RuntimeSelection) => void clearSelection: (key: string) => void moveSelection: (fromKey: string, toKey: string) => void + syncFromSessions: (sessions: SessionListItem[]) => void } function loadSelections(): Record { @@ -66,4 +68,30 @@ export const useSessionRuntimeStore = create((set) => ({ persistSelections(selections) return { selections } }), + + syncFromSessions: (sessions) => + set((state) => { + let selections = state.selections + for (const session of sessions) { + if (!session.runtimeModelId || session.runtimeProviderId === undefined) continue + const selection: RuntimeSelection = { + providerId: session.runtimeProviderId, + modelId: session.runtimeModelId, + ...(session.effortLevel ? { effortLevel: session.effortLevel } : {}), + } + const current = selections[session.id] + if ( + current?.providerId === selection.providerId && + current.modelId === selection.modelId && + current.effortLevel === selection.effortLevel + ) { + continue + } + if (selections === state.selections) selections = { ...state.selections } + selections[session.id] = selection + } + if (selections === state.selections) return state + persistSelections(selections) + return { selections } + }), })) diff --git a/desktop/src/stores/sessionStore.test.ts b/desktop/src/stores/sessionStore.test.ts index 8d3fab12..427cc53d 100644 --- a/desktop/src/stores/sessionStore.test.ts +++ b/desktop/src/stores/sessionStore.test.ts @@ -25,6 +25,7 @@ vi.mock('../lib/recentProjectsCache', () => ({ })) import { useSessionStore } from './sessionStore' +import { useSessionRuntimeStore } from './sessionRuntimeStore' import { useSettingsStore } from './settingsStore' import { useTabStore } from './tabStore' @@ -73,12 +74,14 @@ describe('sessionStore', () => { }) useSettingsStore.setState({ permissionMode: 'default' }) useTabStore.setState({ tabs: [], activeTabId: null }) + useSessionRuntimeStore.setState({ selections: {} }) }) afterEach(() => { useSessionStore.setState(initialState) useSettingsStore.setState({ permissionMode: 'default' }) useTabStore.setState({ tabs: [], activeTabId: null }) + useSessionRuntimeStore.setState({ selections: {} }) }) it('returns a new session id before the background refresh completes', async () => { @@ -165,6 +168,31 @@ describe('sessionStore', () => { expect(useTabStore.getState().tabs[0]?.title).toBe('使用bash写一个shell,随便写点什么东西') }) + it('syncs transcript runtime metadata before a session is opened from the sidebar', async () => { + useSessionRuntimeStore.getState().setSelection('session-runtime-1', { + providerId: null, + modelId: 'gpt-5.4', + effortLevel: 'max', + }) + listMock.mockResolvedValue({ + sessions: [{ + ...makeSession('session-runtime-1', '2026-07-13T05:57:05.818Z'), + runtimeProviderId: 'provider-latest', + runtimeModelId: 'anthropic/claude-opus-4.7', + effortLevel: 'max', + }], + total: 1, + }) + + await useSessionStore.getState().fetchSessions() + + expect(useSessionRuntimeStore.getState().selections['session-runtime-1']).toEqual({ + providerId: 'provider-latest', + modelId: 'anthropic/claude-opus-4.7', + effortLevel: 'max', + }) + }) + it('updates a session message count without changing other metadata', () => { useSessionStore.setState({ sessions: [makeSession('session-count-1', '2026-05-07T00:00:00.000Z', 'Working session')], diff --git a/desktop/src/stores/sessionStore.ts b/desktop/src/stores/sessionStore.ts index 6c37935d..4cf0b02b 100644 --- a/desktop/src/stores/sessionStore.ts +++ b/desktop/src/stores/sessionStore.ts @@ -68,6 +68,7 @@ export const useSessionStore = create((set, get) => ({ try { const { sessions: raw } = await sessionsApi.list(buildSessionListParams(project)) if (requestId !== fetchSessionsRequestId) return + useSessionRuntimeStore.getState().syncFromSessions(raw) let syncedSessions: SessionListItem[] = [] set((state) => { const sessions = mergeSessionList(raw, state.sessions) diff --git a/desktop/src/stores/tabStore.test.ts b/desktop/src/stores/tabStore.test.ts index 498a3148..bec25425 100644 --- a/desktop/src/stores/tabStore.test.ts +++ b/desktop/src/stores/tabStore.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { sessionsApi } from '../api/sessions' +import { useSessionRuntimeStore } from './sessionRuntimeStore' import { SETTINGS_TAB_ID, MARKET_TAB_ID, useTabStore } from './tabStore' vi.mock('../api/sessions', () => ({ @@ -12,6 +13,7 @@ describe('tabStore', () => { beforeEach(() => { useTabStore.setState({ tabs: [], activeTabId: null }) localStorage.clear() + useSessionRuntimeStore.setState({ selections: {} }) vi.mocked(sessionsApi.list).mockResolvedValue({ sessions: [] } as never) }) @@ -196,6 +198,36 @@ describe('tabStore', () => { expect(useTabStore.getState().activeTabId).toBe(MARKET_TAB_ID) }) + it('hydrates restored tabs with authoritative transcript runtime metadata', async () => { + useSessionRuntimeStore.getState().setSelection('session-1', { + providerId: null, + modelId: 'gpt-5.4', + effortLevel: 'max', + }) + localStorage.setItem('cc-haha-open-tabs', JSON.stringify({ + openTabs: [{ sessionId: 'session-1', title: 'Runtime session', type: 'session' }], + activeTabId: 'session-1', + })) + vi.mocked(sessionsApi.list).mockResolvedValue({ + sessions: [{ + id: 'session-1', + title: 'Runtime session', + runtimeProviderId: 'provider-latest', + runtimeModelId: 'anthropic/claude-opus-4.7', + effortLevel: 'max', + }], + total: 1, + } as never) + + await useTabStore.getState().restoreTabs() + + expect(useSessionRuntimeStore.getState().selections['session-1']).toEqual({ + providerId: 'provider-latest', + modelId: 'anthropic/claude-opus-4.7', + effortLevel: 'max', + }) + }) + it('canonicalizes mismatched persisted special tab ids and types during restore', async () => { localStorage.setItem('cc-haha-open-tabs', JSON.stringify({ openTabs: [ diff --git a/desktop/src/stores/tabStore.ts b/desktop/src/stores/tabStore.ts index 4c52a137..0900714d 100644 --- a/desktop/src/stores/tabStore.ts +++ b/desktop/src/stores/tabStore.ts @@ -2,6 +2,7 @@ import { create } from 'zustand' import { sessionsApi } from '../api/sessions' import { dropSession as dropVirtualHeightSession } from '../components/chat/virtualHeightCache' import { destroyTerminalRuntime } from '../lib/terminalRuntime' +import { useSessionRuntimeStore } from './sessionRuntimeStore' const TAB_STORAGE_KEY = 'cc-haha-open-tabs' @@ -351,6 +352,7 @@ export const useTabStore = create((set, get) => ({ ) { return } + useSessionRuntimeStore.getState().syncFromSessions(sessions) const existingIds = new Set(sessions.map((s) => s.id)) const validTabs: Tab[] = data.openTabs diff --git a/desktop/src/types/session.ts b/desktop/src/types/session.ts index f4de90e9..122eb2f3 100644 --- a/desktop/src/types/session.ts +++ b/desktop/src/types/session.ts @@ -1,5 +1,7 @@ // Source: src/server/services/sessionService.ts +import type { ReasoningEffortLevel } from './settings' + export type SessionListItem = { id: string title: string @@ -11,6 +13,9 @@ export type SessionListItem = { workDir: string | null workDirExists: boolean permissionMode?: string + runtimeProviderId?: string | null + runtimeModelId?: string + effortLevel?: ReasoningEffortLevel } export type MessageUsage = { diff --git a/src/server/__tests__/sessions.test.ts b/src/server/__tests__/sessions.test.ts index 1551ad59..93f68dfd 100644 --- a/src/server/__tests__/sessions.test.ts +++ b/src/server/__tests__/sessions.test.ts @@ -1686,6 +1686,29 @@ describe('SessionService', () => { expect((await service.getSessionLaunchInfo(sessionId))?.permissionMode).toBe('auto') }) + it('should expose the latest runtime selection in the session list', async () => { + const workDir = '/tmp/runtime-list-metadata' + const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' + await writeSessionFile(sanitizePath(workDir), sessionId, [ + makeSnapshotEntry(), + { + ...makeSessionMetaEntry(workDir), + runtimeProviderId: 'provider-latest', + runtimeModelId: 'anthropic/claude-opus-4.7', + effortLevel: 'max', + }, + makeUserEntry('Use the latest runtime metadata'), + ]) + + const listed = (await service.listSessions()).sessions.find((session) => session.id === sessionId) + + expect(listed).toMatchObject({ + runtimeProviderId: 'provider-latest', + runtimeModelId: 'anthropic/claude-opus-4.7', + effortLevel: 'max', + }) + }) + it('should not append duplicate runtime metadata when it already matches', async () => { const workDir = '/tmp/runtime-idempotent' const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' diff --git a/src/server/services/sessionService.ts b/src/server/services/sessionService.ts index 929291e6..b82c4807 100644 --- a/src/server/services/sessionService.ts +++ b/src/server/services/sessionService.ts @@ -61,6 +61,9 @@ export type SessionListItem = { workDir: string | null workDirExists: boolean permissionMode?: string + runtimeProviderId?: string | null + runtimeModelId?: string + effortLevel?: string } export type DeleteSessionFailure = { @@ -2459,6 +2462,11 @@ export class SessionService { workDir, workDirExists, permissionMode: summary.permissionMode, + ...(summary.runtimeProviderId !== undefined + ? { runtimeProviderId: summary.runtimeProviderId } + : {}), + ...(summary.runtimeModelId ? { runtimeModelId: summary.runtimeModelId } : {}), + ...(summary.effortLevel ? { effortLevel: summary.effortLevel } : {}), }) } catch { // Skip unreadable files