diff --git a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz index 2635ee5..a67a814 100644 Binary files a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz and b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz differ diff --git a/public/app.js b/public/app.js index 563cf9b..e0170c0 100644 --- a/public/app.js +++ b/public/app.js @@ -2294,11 +2294,14 @@ function mergeSessionListSnapshot(snapshot) { if (!snapshot?.sessionId) return; + const hasSnapshotCwd = Object.prototype.hasOwnProperty.call(snapshot, 'cwd'); + const snapshotCwd = hasSnapshotCwd ? String(snapshot.cwd || '').trim() : ''; + const snapshotProjectName = String(snapshot.projectName || '').trim(); const nextMeta = { id: snapshot.sessionId, sessionId: snapshot.sessionId, - cwd: snapshot.cwd || '', - projectName: snapshot.cwd ? getPathLeaf(snapshot.cwd) : snapshot.projectName || '', + cwd: hasSnapshotCwd ? snapshotCwd : '', + projectName: hasSnapshotCwd ? (snapshotCwd ? getPathLeaf(snapshotCwd) : '') : snapshotProjectName, title: snapshot.title || '新会话', agent: normalizeAgent(snapshot.agent), updated: snapshot.updated || new Date().toISOString(), @@ -2322,8 +2325,12 @@ return { ...session, ...nextMeta, - cwd: nextMeta.cwd || session.cwd || '', - projectName: nextMeta.cwd ? getPathLeaf(nextMeta.cwd) : session.projectName || nextMeta.projectName || '', + // 服务端显式传空 cwd 代表项目归属已清除,不能再沿用旧项目名; + // 只有完全未提供 cwd 时,才保留缓存中的路径作为兼容降级。 + cwd: hasSnapshotCwd ? nextMeta.cwd : session.cwd || '', + projectName: hasSnapshotCwd + ? nextMeta.projectName + : nextMeta.projectName || session.projectName || (session.cwd ? getPathLeaf(session.cwd) : ''), title: nextMeta.title || session.title, titleSource: nextMeta.titleSource || session.titleSource || null, createdFromKind: nextMeta.createdFromKind || session.createdFromKind || null, diff --git a/scripts/regression.js b/scripts/regression.js index 10cdc6a..562c482 100644 --- a/scripts/regression.js +++ b/scripts/regression.js @@ -2609,6 +2609,84 @@ function assertSessionItemTooltipContract() { assert(!createItemSource.includes('item.title = sessionCwd'), 'Session card should not fall back to a project-only tooltip'); } +function assertSessionProjectSnapshotContract() { + const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8'); + const mergeSource = extractFunctionSource(frontendSource, 'mergeSessionListSnapshot'); + const api = new Function(` + let sessions = []; + function normalizeAgent(agent) { + return ['claude', 'codex', 'codexapp'].includes(agent) ? agent : 'claude'; + } + function getPathLeaf(input) { + const normalized = String(input || '').replace(/\\\\/g, '/').replace(/\\/+$/, ''); + return normalized.split('/').filter(Boolean).pop() || ''; + } + function compareSessionUpdatedDesc(a, b) { + return new Date(b.updated || 0) - new Date(a.updated || 0); + } + ${mergeSource} + return { + mergeSessionListSnapshot, + setSessions(next) { sessions = next.map((item) => ({ ...item })); }, + getSession(id) { return sessions.find((item) => item.id === id) || null; }, + }; + `)(); + + api.setSessions([{ + id: 'snapshot-session', + agent: 'codexapp', + cwd: '/home/hdzx/2026/english', + projectName: 'english', + title: '输入保护', + updated: '2026-08-19T01:25:00.000Z', + }]); + api.mergeSessionListSnapshot({ + sessionId: 'snapshot-session', + cwd: '', + projectName: '', + title: '输入保护', + updated: '2026-08-19T01:26:00.000Z', + }); + const cleared = api.getSession('snapshot-session'); + assert(cleared.cwd === '', 'Explicit empty snapshot cwd should clear the stale session cwd'); + assert(cleared.projectName === '', 'Explicit empty snapshot cwd should clear the stale project name'); + + api.setSessions([{ + id: 'snapshot-session', + agent: 'codexapp', + cwd: '/home/hdzx', + projectName: 'hdzx', + title: '输入保护', + updated: '2026-08-19T01:25:00.000Z', + }]); + api.mergeSessionListSnapshot({ + sessionId: 'snapshot-session', + cwd: '/home/hdzx/2026/english', + projectName: 'hdzx', + title: '输入保护', + updated: '2026-08-19T01:27:00.000Z', + }); + const changed = api.getSession('snapshot-session'); + assert(changed.cwd === '/home/hdzx/2026/english', 'Snapshot cwd changes should replace the previous project cwd'); + assert(changed.projectName === 'english', 'Snapshot cwd changes should derive the displayed project from the new cwd'); + + api.setSessions([{ + id: 'snapshot-session', + agent: 'codexapp', + cwd: '/home/hdzx/2026/english', + projectName: 'english', + title: '输入保护', + updated: '2026-08-19T01:25:00.000Z', + }]); + api.mergeSessionListSnapshot({ + sessionId: 'snapshot-session', + title: '输入保护', + updated: '2026-08-19T01:28:00.000Z', + }); + const omitted = api.getSession('snapshot-session'); + assert(omitted.cwd === '/home/hdzx/2026/english' && omitted.projectName === 'english', 'Snapshots that omit cwd should preserve the cached project metadata'); +} + function assertSidebarTitleRefreshStormContract() { const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8'); const createItemSource = extractFunctionSource(frontendSource, 'createSessionListItem'); @@ -5897,6 +5975,11 @@ async function main() { console.log('Session item tooltip regression checks passed.'); return; } + if (regressionTarget === 'session-project-snapshot') { + assertSessionProjectSnapshotContract(); + console.log('Session project snapshot regression checks passed.'); + return; + } if (regressionTarget === 'sidebar-title-refresh-storm') { assertSidebarTitleRefreshStormContract(); assertCcwebMcpChildUpdateCoalescingContract(); @@ -5977,6 +6060,7 @@ async function main() { assertFrontendPrimaryCodexAppUiContract(); assertSetTitleMcpContract(); assertSessionItemTooltipContract(); + assertSessionProjectSnapshotContract(); assertCcwebMcpChildUpdateCoalescingContract(); assertTitleHistoryOutlineContract(); assertSessionSwitchResilienceContract();