From aecf596db95ea836370a13fb84c119d8e2501cfd Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Wed, 30 Sep 2026 10:31:18 -0500 Subject: [PATCH] refactor(bots): share the stale Bot Chat tile probe between the roster click and tip open isStaleBotChatTile(canonicalIds) replaces the copy in openStoredBotChat, whose try/catch duplicated the optional call; the stale-tiles test no longer contradicts its own comment. --- .../bot-row-opens-canonical-chat.test.ts | 3 ++- .../canonical-chat-stale-tiles.test.ts | 3 +-- .../src/plugins/hermes-bots/canonical-chat.ts | 17 +++++++---------- .../hermes-bots/cold-bot-switch-pending.test.ts | 3 ++- .../src/plugins/hermes-bots/roster-actions.ts | 9 ++------- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/apps/desktop/src/plugins/hermes-bots/bot-row-opens-canonical-chat.test.ts b/apps/desktop/src/plugins/hermes-bots/bot-row-opens-canonical-chat.test.ts index a7350ac7c8..e95383728a 100644 --- a/apps/desktop/src/plugins/hermes-bots/bot-row-opens-canonical-chat.test.ts +++ b/apps/desktop/src/plugins/hermes-bots/bot-row-opens-canonical-chat.test.ts @@ -18,9 +18,10 @@ const { openBotCanonicalChat, prepareBotSource } = vi.hoisted(() => ({ prepareBotSource: vi.fn() })) -vi.mock('./canonical-chat', () => ({ +vi.mock('./canonical-chat', async () => ({ CANONICAL_CHAT_TITLE: 'Bot Chat', ensureBotMetadata: vi.fn(async () => ({})), + isStaleBotChatTile: (await vi.importActual('./canonical-chat')).isStaleBotChatTile, notifyBotOpenFailure: vi.fn(), openBotCanonicalChat, prepareBotSource, diff --git a/apps/desktop/src/plugins/hermes-bots/canonical-chat-stale-tiles.test.ts b/apps/desktop/src/plugins/hermes-bots/canonical-chat-stale-tiles.test.ts index 704c9df08d..e426f3c0ee 100644 --- a/apps/desktop/src/plugins/hermes-bots/canonical-chat-stale-tiles.test.ts +++ b/apps/desktop/src/plugins/hermes-bots/canonical-chat-stale-tiles.test.ts @@ -74,8 +74,7 @@ describe('120810: compressed Bot Chat discards the stale tile', () => { // Live tip and registry row are never stale. expect(probe({ storedSessionId: 'tip-9', workspaceTabTitle: 'Bot Chat' })).toBe(false) expect(probe({ storedSessionId: 'root-1', workspaceTabTitle: 'Bot Chat' })).toBe(false) - // User + threads are never stale. - expect(probe({ storedSessionId: 'side-1', workspaceTabTitle: 'Bot Chat' })).toBe(true) + // A user + thread carries its own title, so it is never stale. expect(probe({ storedSessionId: 'side-1', workspaceTabTitle: 'my thread' })).toBe(false) // Tip opens after the probe. expect(hostMock.openSession.mock.calls[0][0]).toBe('tip-9') diff --git a/apps/desktop/src/plugins/hermes-bots/canonical-chat.ts b/apps/desktop/src/plugins/hermes-bots/canonical-chat.ts index 050b235f7c..b40c94ccf0 100644 --- a/apps/desktop/src/plugins/hermes-bots/canonical-chat.ts +++ b/apps/desktop/src/plugins/hermes-bots/canonical-chat.ts @@ -60,6 +60,12 @@ interface CanonicalChatRow extends CanonicalSession { readonly live_message_count?: SessionListRow['live_message_count'] } +/** A Bot Chat tile left on an old compression segment: titled as the canonical + * chat but keyed to none of the lineage ids the owner currently resolves to. */ +export const isStaleBotChatTile = + (canonicalIds: readonly string[]) => (tile: { storedSessionId: string; workspaceTabTitle?: string }) => + tile.workspaceTabTitle === CANONICAL_CHAT_TITLE && !canonicalIds.includes(String(tile.storedSessionId)) + /** Should the open wait for a painted transcript? The paintable row count * decides when the gateway reports it; the denormalized total is the only * fallback older gateways offer, and no count at all means the row is @@ -153,16 +159,7 @@ async function openStoredBotChat( // old-segment tile is discarded here or it survives beside the tip // (hermes-agent#120810). Same owner-scoped probe the roster click runs. const canonicalIds = [...new Set([summary?.id, storedId].filter(Boolean).map(String))] - - try { - host.focusOpenWorkspaceSession?.( - ownerKey, - tile => tile.workspaceTabTitle === CANONICAL_CHAT_TITLE && !canonicalIds.includes(String(tile.storedSessionId)), - canonicalIds - ) - } catch { - // Older shells: the tip open below still lands. - } + host.focusOpenWorkspaceSession?.(ownerKey, isStaleBotChatTile(canonicalIds), canonicalIds) await host.openSession(storedId, { ...(route diff --git a/apps/desktop/src/plugins/hermes-bots/cold-bot-switch-pending.test.ts b/apps/desktop/src/plugins/hermes-bots/cold-bot-switch-pending.test.ts index 0fb6131d49..6819f2bce6 100644 --- a/apps/desktop/src/plugins/hermes-bots/cold-bot-switch-pending.test.ts +++ b/apps/desktop/src/plugins/hermes-bots/cold-bot-switch-pending.test.ts @@ -16,9 +16,10 @@ const { openBotCanonicalChat, prepareBotSource } = vi.hoisted(() => ({ prepareBotSource: vi.fn() })) -vi.mock('./canonical-chat', () => ({ +vi.mock('./canonical-chat', async () => ({ CANONICAL_CHAT_TITLE: 'Bot Chat', ensureBotMetadata: vi.fn(async () => ({})), + isStaleBotChatTile: (await vi.importActual('./canonical-chat')).isStaleBotChatTile, notifyBotOpenFailure: vi.fn(), openBotCanonicalChat, prepareBotSource, diff --git a/apps/desktop/src/plugins/hermes-bots/roster-actions.ts b/apps/desktop/src/plugins/hermes-bots/roster-actions.ts index a5b587128c..c834d674c5 100644 --- a/apps/desktop/src/plugins/hermes-bots/roster-actions.ts +++ b/apps/desktop/src/plugins/hermes-bots/roster-actions.ts @@ -18,7 +18,7 @@ import { rosterWatermarks, saveSelectedRosterBot } from './bot-state' -import { CANONICAL_CHAT_TITLE, notifyBotOpenFailure, openBotCanonicalChat, prepareBotSource } from './canonical-chat' +import { isStaleBotChatTile, notifyBotOpenFailure, openBotCanonicalChat, prepareBotSource } from './canonical-chat' import { $botMeta, botActivitySession, botRosterKey, botSelectionKey, newBotChat } from './data' import { $groupChats, $groupChatWorkspace } from './group-chat' import { openGroupChat } from './group-chat-view' @@ -197,13 +197,8 @@ function focusExistingBotTab(bot: RosterRow): null | { registryId: string; store return null } - const isStaleTile = (tile: { storedSessionId: string; workspaceTabTitle?: string }) => - typeof tile.workspaceTabTitle === 'string' && - tile.workspaceTabTitle === CANONICAL_CHAT_TITLE && - !canonicalIds.includes(String(tile.storedSessionId)) - try { - const focused = host.focusOpenWorkspaceSession(botWorkspaceOwnerKey(bot), isStaleTile, canonicalIds) + const focused = host.focusOpenWorkspaceSession(botWorkspaceOwnerKey(bot), isStaleBotChatTile(canonicalIds), canonicalIds) return typeof focused === 'string' && focused ? { registryId: String(canonical!.id), storedSessionId: focused }