diff --git a/apps/desktop/src/plugins/hermes-bots/plugin.mentions.test.ts b/apps/desktop/src/plugins/hermes-bots/plugin.mentions.test.ts index de02c50025..bc05ef6433 100644 --- a/apps/desktop/src/plugins/hermes-bots/plugin.mentions.test.ts +++ b/apps/desktop/src/plugins/hermes-bots/plugin.mentions.test.ts @@ -21,6 +21,8 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import type { RosterRow } from './types' + interface MentionCompletionItem { display: string insert: string @@ -58,7 +60,7 @@ const ROSTER = { } const { cache, hostMock, live } = vi.hoisted(() => { - const live = { focused: 'default', profile: 'default' } + const live = { focused: 'default', profile: 'default', stored: null as string | null } return { cache: new Map(), @@ -69,7 +71,7 @@ const { cache, hostMock, live } = vi.hoisted(() => { state: { connectionId: { get: () => 'local', listen: () => () => undefined }, focusedSessionProfile: { get: () => live.focused, listen: () => () => undefined }, - focusedStoredSessionId: { get: () => null, listen: () => () => undefined }, + focusedStoredSessionId: { get: () => live.stored, listen: () => () => undefined }, gateway: { get: () => null, listen: () => () => undefined }, profile: { get: () => live.profile, listen: () => () => undefined } } @@ -173,6 +175,54 @@ beforeEach(() => { vi.clearAllMocks() live.focused = 'default' live.profile = 'default' + live.stored = null +}) + +describe('Bot Chat reset guard', () => { + it.each([ + { connectionId: 'local', sourceScoped: true }, + { connectionId: 'remote', remoteSource: true, sourceScoped: true }, + {} + ])('compacts only the selected bot canonical chat: %j', async source => { + const { handler } = await contributions() + const { $lastRoster } = await import('./data') + const { saveSelectedRosterBot } = await import('./bot-state') + + const selected: RosterRow = { + ...source, + name: 'writer', + canonical_session: { id: 'bot-root', resolved_id: 'bot-tip' } + } + + // An identically named bot on another connection must not win the lookup. + $lastRoster.set([ + { name: 'writer', connectionId: 'other', sourceScoped: true, canonical_session: { id: 'other-chat' } }, + selected + ]) + saveSelectedRosterBot(selected) + + for (const stored of ['bot-root', 'bot-tip']) { + live.stored = stored + + for (const text of ['/new', '/reset']) { + hostMock.notify.mockClear() + expect(await handler({ text })).toEqual({ text: '/compact' }) + expect(hostMock.notify).toHaveBeenCalledOnce() + expect(hostMock.notify).toHaveBeenCalledWith(expect.objectContaining({ title: 'This chat never resets' })) + } + } + + for (const stored of ['scratch-chat', 'other-chat', null]) { + live.stored = stored + + for (const text of ['/new', '/reset']) { + hostMock.notify.mockClear() + const draft = { text } + expect(await handler(draft)).toBe(draft) + expect(hostMock.notify).not.toHaveBeenCalled() + } + } + }) }) describe('@-mention completions', () => { diff --git a/apps/desktop/src/plugins/hermes-bots/plugin.tsx b/apps/desktop/src/plugins/hermes-bots/plugin.tsx index ffdcaeb8fa..761776fa23 100644 --- a/apps/desktop/src/plugins/hermes-bots/plugin.tsx +++ b/apps/desktop/src/plugins/hermes-bots/plugin.tsx @@ -37,6 +37,7 @@ import { $lastRoster, botHandle, botMentionTag, + botSelectionKey, cachedUnionRoster, isActiveRosterBot, migrateBotMeta, @@ -665,7 +666,7 @@ export default { // server-side by name), matching either the durable row id or // the compression-lineage tip currently on screen. const roster = $lastRoster.get() - const row = Array.isArray(roster) ? roster.find(bot => bot?.name === activeBot) : null + const row = Array.isArray(roster) ? roster.find(bot => botSelectionKey(bot) === activeBot) : null // The STORED id, which is the id space canonical_session is keyed // in. `host.state.activeSessionId` is the runtime id and could