mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
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.
This commit is contained in:
parent
4c2881aaf0
commit
2dd8ee4a33
@ -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 })
|
||||
|
||||
@ -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<SessionStore>((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<SessionStore>((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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
desktop/src/stores/tabStore.test.ts
Normal file
22
desktop/src/stores/tabStore.test.ts
Normal file
@ -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')
|
||||
})
|
||||
})
|
||||
@ -49,8 +49,12 @@ export const useTabStore = create<TabStore>((set, get) => ({
|
||||
if (existing) {
|
||||
set({
|
||||
tabs: tabs.map((tab) =>
|
||||
tab.sessionId === sessionId && !(tab as Partial<Tab>).type
|
||||
? { ...tab, type }
|
||||
tab.sessionId === sessionId
|
||||
? {
|
||||
...tab,
|
||||
title,
|
||||
...(!(tab as Partial<Tab>).type ? { type } : {}),
|
||||
}
|
||||
: tab,
|
||||
),
|
||||
activeTabId: sessionId,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user