From 782c32c1fcd76e5aae5eee519a798993142e96c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Thu, 2 Jul 2026 23:06:26 +0800 Subject: [PATCH] fix(desktop): keep cleared session metadata in sync Reset session messageCount when /clear is confirmed so active headers and sidebars do not keep stale message totals after the transcript is cleared. Also wait for the restored project chip in the live desktop smoke lane before filling the prompt, preventing the test from racing against EmptySession and creating a default-home session. Tested: cd desktop && bun run test -- --run src/stores/chatStore.test.ts src/stores/sessionStore.test.ts Tested: bun test scripts/quality-gate/desktop-smoke/execute.test.ts Tested: bun run check:desktop Tested: SKIP_INSTALL=1 MAC_TARGETS=zip desktop/scripts/build-macos-arm64.sh Tested: bun run quality:smoke --provider-model codingplan:main:codingplan-main Not-tested: bun run verify Confidence: high Scope-risk: narrow --- desktop/src/stores/chatStore.test.ts | 6 +++ desktop/src/stores/chatStore.ts | 1 + desktop/src/stores/sessionStore.test.ts | 15 ++++++ desktop/src/stores/sessionStore.ts | 9 ++++ .../desktop-smoke/execute.test.ts | 23 +++++++- scripts/quality-gate/desktop-smoke/execute.ts | 53 ++++++++++++++++++- 6 files changed, 104 insertions(+), 3 deletions(-) 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,