diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts index 01de253c..9cf8421a 100644 --- a/desktop/src/stores/chatStore.test.ts +++ b/desktop/src/stores/chatStore.test.ts @@ -19,6 +19,7 @@ const { updateTabTitleMock, updateTabStatusMock, updateSessionTitleMock, + updateSessionMessageCountMock, updateSessionPermissionModeMock, sessionStoreSnapshot, cliTaskStoreSnapshot, @@ -39,6 +40,7 @@ const { updateTabTitleMock: vi.fn(), updateTabStatusMock: vi.fn(), updateSessionTitleMock: vi.fn(), + updateSessionMessageCountMock: vi.fn(), updateSessionPermissionModeMock: vi.fn(), sessionStoreSnapshot: { sessions: [] as Array<{ @@ -105,6 +107,7 @@ vi.mock('./sessionStore', () => ({ getState: () => ({ sessions: sessionStoreSnapshot.sessions, updateSessionTitle: updateSessionTitleMock, + updateSessionMessageCount: updateSessionMessageCountMock, updateSessionPermissionMode: updateSessionPermissionModeMock, }), }, @@ -276,6 +279,7 @@ describe('chatStore history mapping', () => { updateTabTitleMock.mockReset() updateTabStatusMock.mockReset() updateSessionTitleMock.mockReset() + updateSessionMessageCountMock.mockReset() vi.mocked(sessionsApi.getMessages).mockReset() vi.mocked(sessionsApi.getMessages).mockResolvedValue({ messages: [] }) sessionStoreSnapshot.sessions = [] @@ -3000,6 +3004,8 @@ describe('chatStore history mapping', () => { expect(session?.streamingResponseChars).toBe(0) expect(session?.slashCommands).toEqual([]) expect(clearTasksMock).toHaveBeenCalledWith(TEST_SESSION_ID) + expect(updateSessionTitleMock).toHaveBeenCalledWith(TEST_SESSION_ID, 'New Session') + expect(updateSessionMessageCountMock).toHaveBeenCalledWith(TEST_SESSION_ID, 0) vi.advanceTimersByTime(60) expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.streamingText).toBe('') diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts index 91746e22..0122410e 100644 --- a/desktop/src/stores/chatStore.ts +++ b/desktop/src/stores/chatStore.ts @@ -2171,6 +2171,7 @@ export const useChatStore = create((set, get) => ({ clearPendingToolParentUseIds(sessionId) useCLITaskStore.getState().clearTasks(sessionId) useSessionStore.getState().updateSessionTitle(sessionId, 'New Session') + useSessionStore.getState().updateSessionMessageCount(sessionId, 0) useTabStore.getState().updateTabTitle(sessionId, 'New Session') useTabStore.getState().updateTabStatus(sessionId, 'idle') } diff --git a/desktop/src/stores/sessionStore.test.ts b/desktop/src/stores/sessionStore.test.ts index fdca971d..8d3fab12 100644 --- a/desktop/src/stores/sessionStore.test.ts +++ b/desktop/src/stores/sessionStore.test.ts @@ -165,6 +165,21 @@ describe('sessionStore', () => { expect(useTabStore.getState().tabs[0]?.title).toBe('使用bash写一个shell,随便写点什么东西') }) + 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')], + }) + + useSessionStore.getState().updateSessionMessageCount('session-count-1', 0) + + expect(useSessionStore.getState().sessions[0]).toMatchObject({ + id: 'session-count-1', + title: 'Working session', + messageCount: 0, + workDir: '/workspace/project', + }) + }) + it('requests a large default session page for noisy history directories', async () => { listMock.mockResolvedValue({ sessions: [makeSession('session-newest', '2026-05-07T00:00:03.000Z')], diff --git a/desktop/src/stores/sessionStore.ts b/desktop/src/stores/sessionStore.ts index b6690e7f..6c37935d 100644 --- a/desktop/src/stores/sessionStore.ts +++ b/desktop/src/stores/sessionStore.ts @@ -47,6 +47,7 @@ type SessionStore = { clearSessionSelection: () => void renameSession: (id: string, title: string) => Promise updateSessionTitle: (id: string, title: string) => void + updateSessionMessageCount: (id: string, messageCount: number) => void updateSessionPermissionMode: (id: string, mode: PermissionMode) => void setActiveSession: (id: string | null) => void } @@ -219,6 +220,14 @@ export const useSessionStore = create((set, get) => ({ })) }, + updateSessionMessageCount: (id, messageCount) => { + set((s) => ({ + sessions: s.sessions.map((session) => + session.id === id ? { ...session, messageCount } : session, + ), + })) + }, + updateSessionPermissionMode: (id, mode) => { set((s) => ({ sessions: s.sessions.map((session) => diff --git a/scripts/quality-gate/desktop-smoke/execute.test.ts b/scripts/quality-gate/desktop-smoke/execute.test.ts index 3d5f1b9f..b1e26374 100644 --- a/scripts/quality-gate/desktop-smoke/execute.test.ts +++ b/scripts/quality-gate/desktop-smoke/execute.test.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from 'bun:test' -import { buildDesktopSmokeBrowserEnv, resolveDesktopSmokeRuntimeSelection } from './execute' +import { + buildDesktopSmokeBrowserEnv, + desktopSmokeTextShowsProject, + resolveDesktopSmokeRuntimeSelection, +} from './execute' describe('desktop smoke runtime selection', () => { test('lets current-runtime use the desktop default active provider', () => { @@ -54,3 +58,20 @@ describe('desktop smoke browser environment', () => { }) }) }) + +describe('desktop smoke restored session detection', () => { + test('waits for the target project chip instead of the first empty-session textarea', () => { + expect(desktopSmokeTextShowsProject([ + '新建会话', + '随便问点什么...', + 'folder_open 选择项目...', + ].join('\n'), 'project')).toBe(false) + + expect(desktopSmokeTextShowsProject([ + 'Untitled Session', + '让 Claude 编辑、调试或解释代码...', + 'folder', + 'project', + ].join('\n'), 'project')).toBe(true) + }) +}) diff --git a/scripts/quality-gate/desktop-smoke/execute.ts b/scripts/quality-gate/desktop-smoke/execute.ts index 3b31426d..081c3ff7 100644 --- a/scripts/quality-gate/desktop-smoke/execute.ts +++ b/scripts/quality-gate/desktop-smoke/execute.ts @@ -2,7 +2,7 @@ import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, wr import { mkdtemp } from 'node:fs/promises' import { createServer } from 'node:net' import { tmpdir } from 'node:os' -import { join } from 'node:path' +import { basename, join } from 'node:path' import treeKill from 'tree-kill' import { changedFiles, writeDiffPatch } from '../baseline/execute' import type { BaselineTarget, LaneResult } from '../types' @@ -16,7 +16,7 @@ const PROMPT = [ 'When the tests pass, briefly say done.', ].join(' ') -type DesktopSmokeStage = 'open' | 'eval' | 'reload' | 'wait' | 'screenshot' | 'fill' | 'press' | 'verify' +type DesktopSmokeStage = 'open' | 'eval' | 'reload' | 'wait' | 'session-ready' | 'screenshot' | 'fill' | 'press' | 'verify' type DesktopSmokeFailureContext = { stage: DesktopSmokeStage @@ -334,6 +334,40 @@ async function waitForVerifiedProject( throw new Error(`Timed out waiting for desktop project verification: ${lastVerificationError}`) } +export function desktopSmokeTextShowsProject(text: string, projectName: string) { + const lines = text + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + return lines.includes(projectName) +} + +async function waitForDesktopSmokeSessionReady( + browserEnv: Record, + browserLogPath: string, + rootDir: string, + projectDir: string, + timeoutMs: number, +) { + const projectName = basename(projectDir) + const deadline = Date.now() + timeoutMs + let lastText = '' + while (Date.now() < deadline) { + const body = await runLoggedCommand(agentBrowserCommand(['get', 'text', '#content-area']), { + cwd: rootDir, + env: browserEnv, + logPath: browserLogPath, + timeoutMs: 15_000, + allowFailure: true, + maxLogChars: 4_000, + }) + lastText = `${body.stdout}\n${body.stderr}` + if (desktopSmokeTextShowsProject(lastText, projectName)) return + await Bun.sleep(1_000) + } + throw new Error(`Timed out waiting for desktop smoke session to restore project "${projectName}". Last content text: ${lastText.slice(0, 500)}`) +} + async function verifyProject(originalDir: string, projectDir: string, artifactDir: string) { await writeDiffPatch(originalDir, projectDir, join(artifactDir, 'diff.patch')) const changed = changedFiles(originalDir, projectDir) @@ -484,6 +518,21 @@ export async function executeDesktopSmoke( logPath: browserLogPath, timeoutMs: 30_000, }, browserStepContext) + await runBrowserStep('session-ready', ['get', 'text', '#content-area'], { + cwd: rootDir, + env: browserEnv, + logPath: browserLogPath, + timeoutMs: 15_000, + allowFailure: true, + maxLogChars: 4_000, + }, browserStepContext) + await waitForDesktopSmokeSessionReady( + browserEnv, + browserLogPath, + rootDir, + projectDir, + 30_000, + ) await runBrowserStep('screenshot', ['screenshot', join(artifactDir, 'initial.png')], { cwd: rootDir, env: browserEnv,