From 2f7466e2ddf1f6faf35b809f524fac643c992f64 Mon Sep 17 00:00:00 2001 From: paisley <8197966+su8su@users.noreply.github.com> Date: Tue, 19 May 2026 19:04:17 +0800 Subject: [PATCH] fix session rename (#1046) --- src/stores/chat.ts | 29 +++++--- src/stores/chat/session-actions.ts | 2 +- .../chat-store-session-label-fetch.test.ts | 68 +++++++++++++++++++ 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/stores/chat.ts b/src/stores/chat.ts index 3acfa6c..dcbca0b 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -25,6 +25,7 @@ import { clearSessionLabelHydrationTracking, finishSessionLabelHydration, getSessionLabelHydrationCandidate, + getSessionLabelHydrationVersion, } from './chat/session-label-hydration'; import { DEFAULT_CANONICAL_PREFIX, @@ -147,14 +148,15 @@ function applySessionLabelSummaries( for (const summary of summaries) { const labelText = toSessionLabel(summary.firstUserText || ''); - if (labelText) { - if (nextLabels[summary.sessionKey] !== labelText) { - if (nextLabels === state.sessionLabels) { - nextLabels = { ...state.sessionLabels }; - } - nextLabels[summary.sessionKey] = labelText; - changed = true; + // Only auto-hydrate missing labels. Existing entries include user renames + // and must not be overwritten by transcript-derived titles. + const existingLabel = nextLabels[summary.sessionKey]?.trim(); + if (labelText && !existingLabel) { + if (nextLabels === state.sessionLabels) { + nextLabels = { ...state.sessionLabels }; } + nextLabels[summary.sessionKey] = labelText; + changed = true; } if (typeof summary.lastTimestamp === 'number' && Number.isFinite(summary.lastTimestamp)) { @@ -2204,9 +2206,18 @@ export const useChatStore = create((set, get) => ({ throw err; } + const session = get().sessions.find((entry) => entry.key === key); + if (session) { + finishSessionLabelHydration( + key, + getSessionLabelHydrationVersion(session, get().sessionLastActivity), + 'backend-label', + ); + } + set((s) => ({ - sessions: s.sessions.map((session) => - session.key === key ? { ...session, label: normalized } : session, + sessions: s.sessions.map((entry) => + entry.key === key ? { ...entry, label: normalized } : entry, ), sessionLabels: { ...s.sessionLabels, [key]: normalized }, })); diff --git a/src/stores/chat/session-actions.ts b/src/stores/chat/session-actions.ts index 1b9d541..0f04bea 100644 --- a/src/stores/chat/session-actions.ts +++ b/src/stores/chat/session-actions.ts @@ -193,7 +193,7 @@ export function createSessionActions( const labelText = firstUser ? getMessageText(firstUser.content).trim() : ''; set((s) => { const next: Partial = {}; - if (labelText) { + if (labelText && !s.sessionLabels[session.key]?.trim()) { const truncated = labelText.length > 50 ? `${labelText.slice(0, 50)}…` : labelText; next.sessionLabels = { ...s.sessionLabels, [session.key]: truncated }; } diff --git a/tests/unit/chat-store-session-label-fetch.test.ts b/tests/unit/chat-store-session-label-fetch.test.ts index 17c89f4..612aafe 100644 --- a/tests/unit/chat-store-session-label-fetch.test.ts +++ b/tests/unit/chat-store-session-label-fetch.test.ts @@ -409,4 +409,72 @@ describe('chat store session label summary hydration', () => { }); expect(useChatStore.getState().sessionLabels['agent:main:session-a']).toBe('new label'); }); + + it('preserves user-renamed labels when visible session summaries refresh', async () => { + gatewayRpcMock.mockImplementation(async (method: string, params?: Record) => { + if (method === 'sessions.list') { + return { + sessions: [ + { key: 'agent:main:session-a', displayName: 'Session A', updatedAt: 1000 }, + { key: 'agent:main:main', displayName: 'Main', updatedAt: 1001 }, + ], + }; + } + + if (method === 'chat.history') { + return { + messages: [{ role: 'user', content: 'visible chat', timestamp: Date.now() }], + }; + } + + throw new Error(`Unexpected gateway RPC: ${method} ${JSON.stringify(params)}`); + }); + + hostApiFetchMock.mockImplementation(async (path: string) => { + if (path === '/api/sessions/summaries') { + return { + success: true, + summaries: [ + { + sessionKey: 'agent:main:session-a', + firstUserText: 'original first message', + lastTimestamp: 1_700_000_000_000, + }, + ], + }; + } + return { success: true, summaries: [] }; + }); + + const { useChatStore } = await import('@/stores/chat'); + useChatStore.setState({ + currentSessionKey: 'agent:main:main', + currentAgentId: 'main', + sessions: [ + { key: 'agent:main:session-a', displayName: 'Session A', updatedAt: 1000 }, + { key: 'agent:main:main', displayName: 'Main', updatedAt: 1001 }, + ], + messages: [], + sessionLabels: { 'agent:main:session-a': 'Custom name' }, + sessionLastActivity: {}, + sending: false, + activeRunId: null, + streamingText: '', + streamingMessage: null, + streamingTools: [], + pendingFinal: false, + lastUserMessageAt: null, + pendingToolImages: [], + error: null, + loading: false, + thinkingLevel: null, + runError: null, + }); + + await useChatStore.getState().loadHistory(false); + await Promise.resolve(); + await Promise.resolve(); + + expect(useChatStore.getState().sessionLabels['agent:main:session-a']).toBe('Custom name'); + }); });