mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-15 12:53:31 +08:00
fix(desktop): refine and restore effort selection #1007
This commit is contained in:
parent
f85b93e9c2
commit
c550dbc07a
@ -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', () => {
|
||||
|
||||
@ -111,10 +111,21 @@ export function ReasoningEffortPopover({
|
||||
style={{ bottom: position.bottom, left: position.left, width: position.width }}
|
||||
>
|
||||
<div
|
||||
data-testid="reasoning-effort-label"
|
||||
className="mb-3 text-[15px] font-semibold text-[var(--color-text-secondary)]"
|
||||
data-testid="reasoning-effort-header"
|
||||
className="mb-3 flex items-baseline justify-between gap-3"
|
||||
>
|
||||
{labels[value]}
|
||||
<div
|
||||
data-testid="reasoning-effort-label"
|
||||
className="text-[15px] font-semibold text-[var(--color-text-secondary)]"
|
||||
>
|
||||
{labels[value]}
|
||||
</div>
|
||||
<div
|
||||
data-testid="reasoning-effort-context-label"
|
||||
className="text-[11px] font-medium tracking-wide text-[var(--color-text-tertiary)]"
|
||||
>
|
||||
{ariaLabel}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -126,7 +137,7 @@ export function ReasoningEffortPopover({
|
||||
aria-valuemax={maxIndex}
|
||||
aria-valuenow={selectedIndex}
|
||||
aria-valuetext={labels[value]}
|
||||
className="group relative flex h-11 touch-none cursor-pointer items-center outline-none focus-visible:ring-2 focus-visible:ring-[#3798f7] focus-visible:ring-offset-4 focus-visible:ring-offset-[var(--color-surface-container-lowest)]"
|
||||
className="group relative flex h-11 touch-none cursor-pointer items-center outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)] focus-visible:ring-offset-4 focus-visible:ring-offset-[var(--color-surface-container-lowest)]"
|
||||
onClick={(event) => selectFromClientX(event.clientX)}
|
||||
onPointerDown={(event) => {
|
||||
draggingRef.current = true
|
||||
@ -175,7 +186,7 @@ export function ReasoningEffortPopover({
|
||||
>
|
||||
<div
|
||||
data-testid="reasoning-effort-fill"
|
||||
className="h-full rounded-full bg-[#3798f7] transition-[width] duration-200 motion-reduce:transition-none"
|
||||
className="h-full rounded-full bg-[var(--color-brand)] transition-[width] duration-200 motion-reduce:transition-none"
|
||||
style={{ width: `${fillPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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<string, RuntimeSelection> {
|
||||
@ -66,4 +68,30 @@ export const useSessionRuntimeStore = create<SessionRuntimeStore>((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 }
|
||||
}),
|
||||
}))
|
||||
|
||||
@ -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')],
|
||||
|
||||
@ -68,6 +68,7 @@ export const useSessionStore = create<SessionStore>((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)
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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<TabStore>((set, get) => ({
|
||||
) {
|
||||
return
|
||||
}
|
||||
useSessionRuntimeStore.getState().syncFromSessions(sessions)
|
||||
const existingIds = new Set(sessions.map((s) => s.id))
|
||||
|
||||
const validTabs: Tab[] = data.openTabs
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user