From 2dd8ee4a330672c71eb9d138f312968c23e3a1ad 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, 7 May 2026 16:38:27 +0800 Subject: [PATCH] fix: keep open session tab titles in sync Open session tabs can outlive the title value they were created with, while the sidebar and session detail view read from the refreshed session list. Refresh existing tab titles when a session tab is reopened and after session list refreshes so the top tab bar cannot keep a stale generated title. Constraint: Sidebar filtering and session refresh should not leave already-open tabs with stale labels. Rejected: Only rely on WebSocket title update events | restored tabs and already-open tabs can miss later session-list corrections. Confidence: high Scope-risk: narrow Directive: Keep session tab titles derived from current session list data when available. Tested: cd desktop && bun run test -- --run src/stores/tabStore.test.ts src/stores/sessionStore.test.ts Tested: cd desktop && bun run lint Tested: cd desktop && bun run test -- --run Tested: agent-browser smoke on http://127.0.0.1:1420/ confirmed top tabs match sidebar and session detail titles. --- desktop/src/stores/sessionStore.test.ts | 24 ++++++++++++++++++++++++ desktop/src/stores/sessionStore.ts | 16 ++++++++++++++++ desktop/src/stores/tabStore.test.ts | 22 ++++++++++++++++++++++ desktop/src/stores/tabStore.ts | 8 ++++++-- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 desktop/src/stores/tabStore.test.ts diff --git a/desktop/src/stores/sessionStore.test.ts b/desktop/src/stores/sessionStore.test.ts index 6c91cb85..48d57d8d 100644 --- a/desktop/src/stores/sessionStore.test.ts +++ b/desktop/src/stores/sessionStore.test.ts @@ -15,6 +15,7 @@ vi.mock('../api/sessions', () => ({ })) import { useSessionStore } from './sessionStore' +import { useTabStore } from './tabStore' const initialState = useSessionStore.getState() @@ -43,10 +44,12 @@ describe('sessionStore', () => { selectedProjects: [], availableProjects: [], }) + useTabStore.setState({ tabs: [], activeTabId: null }) }) afterEach(() => { useSessionStore.setState(initialState) + useTabStore.setState({ tabs: [], activeTabId: null }) }) it('returns a new session id before the background refresh completes', async () => { @@ -111,6 +114,27 @@ describe('sessionStore', () => { expect(useSessionStore.getState().sessions[0]?.title).toBe('开始优化UI') }) + it('syncs refreshed session titles into already-open tabs', async () => { + useTabStore.getState().openTab('session-title-2', '```json {"title":') + listMock.mockResolvedValue({ + sessions: [{ + id: 'session-title-2', + title: '使用bash写一个shell,随便写点什么东西', + createdAt: '2026-05-07T00:00:00.000Z', + modifiedAt: '2026-05-07T00:00:01.000Z', + messageCount: 3, + projectPath: '', + workDir: '/workspace/project', + workDirExists: true, + }], + total: 1, + }) + + await useSessionStore.getState().fetchSessions() + + expect(useTabStore.getState().tabs[0]?.title).toBe('使用bash写一个shell,随便写点什么东西') + }) + it('forwards direct branch switch repository options when creating a session', async () => { createMock.mockResolvedValue({ sessionId: 'session-branch-switch', workDir: '/workspace/repo' }) listMock.mockResolvedValue({ sessions: [], total: 0 }) diff --git a/desktop/src/stores/sessionStore.ts b/desktop/src/stores/sessionStore.ts index cf11b11c..223aaa1e 100644 --- a/desktop/src/stores/sessionStore.ts +++ b/desktop/src/stores/sessionStore.ts @@ -1,6 +1,7 @@ import { create } from 'zustand' import { sessionsApi, type CreateSessionRepositoryOptions } from '../api/sessions' import { useSessionRuntimeStore } from './sessionRuntimeStore' +import { useTabStore } from './tabStore' import type { SessionListItem } from '../types/session' import { isPlaceholderSessionTitle } from '../lib/sessionTitle' @@ -37,6 +38,7 @@ export const useSessionStore = create((set, get) => ({ set({ isLoading: true, error: null }) try { const { sessions: raw } = await sessionsApi.list({ project, limit: 100 }) + let syncedSessions: SessionListItem[] = [] set((state) => { const currentById = new Map(state.sessions.map((session) => [session.id, session])) // Deduplicate by session ID - keep the most recently modified entry. @@ -50,9 +52,11 @@ export const useSessionStore = create((set, get) => ({ } } const sessions = [...byId.values()] + syncedSessions = sessions const availableProjects = [...new Set(sessions.map((s) => s.projectPath).filter(Boolean))].sort() return { sessions, availableProjects, isLoading: false } }) + syncOpenSessionTabTitles(syncedSessions) } catch (err) { set({ error: (err as Error).message, isLoading: false }) } @@ -126,3 +130,15 @@ function preserveLocalTitle( } return incoming } + +function syncOpenSessionTabTitles(sessions: SessionListItem[]): void { + const titleById = new Map(sessions.map((session) => [session.id, session.title])) + const { tabs, updateTabTitle } = useTabStore.getState() + for (const tab of tabs) { + if (tab.type !== 'session') continue + const title = titleById.get(tab.sessionId) + if (title && title !== tab.title) { + updateTabTitle(tab.sessionId, title) + } + } +} diff --git a/desktop/src/stores/tabStore.test.ts b/desktop/src/stores/tabStore.test.ts new file mode 100644 index 00000000..b90db356 --- /dev/null +++ b/desktop/src/stores/tabStore.test.ts @@ -0,0 +1,22 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { useTabStore } from './tabStore' + +describe('tabStore', () => { + beforeEach(() => { + useTabStore.setState({ tabs: [], activeTabId: null }) + localStorage.clear() + }) + + it('refreshes an existing tab title when opening the same session again', () => { + useTabStore.getState().openTab('session-1', '```json {"title":') + useTabStore.getState().openTab('session-1', '使用bash写一个shell,随便写点什么东西') + + expect(useTabStore.getState().tabs).toHaveLength(1) + expect(useTabStore.getState().tabs[0]).toMatchObject({ + sessionId: 'session-1', + title: '使用bash写一个shell,随便写点什么东西', + type: 'session', + }) + expect(useTabStore.getState().activeTabId).toBe('session-1') + }) +}) diff --git a/desktop/src/stores/tabStore.ts b/desktop/src/stores/tabStore.ts index edc9ac03..1d4d7ad9 100644 --- a/desktop/src/stores/tabStore.ts +++ b/desktop/src/stores/tabStore.ts @@ -49,8 +49,12 @@ export const useTabStore = create((set, get) => ({ if (existing) { set({ tabs: tabs.map((tab) => - tab.sessionId === sessionId && !(tab as Partial).type - ? { ...tab, type } + tab.sessionId === sessionId + ? { + ...tab, + title, + ...(!(tab as Partial).type ? { type } : {}), + } : tab, ), activeTabId: sessionId,