From ced8ae09e9f8261a1e1b7c61ad9ee84a134f1bb7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 6 Sep 2026 21:27:43 +0000 Subject: [PATCH 1/3] feat(activity): name bots and the system principal instead of "Automation" Every non-user actor read "Automation". Actor ids are now parsed once into user / bot / unknown, and names come from a table: the viewer is "You", users resolve through displayName, the system bot (bot_id 5759) reads "System", first-party bots read their constant names, team bots resolve through the bots list, and unparseable ids read "Unknown". The system bot also joins firstPartyBotName so channels stop showing "Bot" for it. Co-authored-by: teo --- .../activity/context/activity-context.tsx | 25 ++++++++++- .../src/features/activity/core/actor.test.ts | 30 +++++++++++++ apps/web/src/features/activity/core/actor.ts | 24 ++++++++++ .../activity/primitives/actor-name.test.ts | 45 ++++++++++++++++--- .../activity/primitives/actor-name.ts | 37 ++++++++++----- .../features/activity/tests/mock-context.ts | 8 +++- apps/web/src/lib/core/constant/macroSystem.ts | 25 +++++++++++ .../src/lib/queries/channel/message-sender.ts | 10 ++++- .../channel/tests/message-sender.test.ts | 16 +++++++ 9 files changed, 199 insertions(+), 21 deletions(-) create mode 100644 apps/web/src/features/activity/core/actor.test.ts create mode 100644 apps/web/src/features/activity/core/actor.ts create mode 100644 apps/web/src/lib/core/constant/macroSystem.ts diff --git a/apps/web/src/features/activity/context/activity-context.tsx b/apps/web/src/features/activity/context/activity-context.tsx index 2bbade0d5ab..c7c189476ea 100644 --- a/apps/web/src/features/activity/context/activity-context.tsx +++ b/apps/web/src/features/activity/context/activity-context.tsx @@ -3,6 +3,11 @@ import { tryMacroId, useDisplayName } from '@core/user'; import { useAllProperties } from '@property/editor/hooks/useAllProperties'; import { usePropertyEntityDisplay } from '@property/hooks'; import type { PropertyDefinitionDomain } from '@property/types'; +import { useBotsQuery } from '@queries/bots/bots'; +import { + firstPartyBotName, + getBotDisplayName, +} from '@queries/channel/message-sender'; import type { EntityType } from '@service-properties/generated/schemas/entityType'; import { getGraphqlSoupClient } from '@service-storage/graphql-soup'; import type { Client } from '@urql/core'; @@ -39,10 +44,16 @@ export type ActivityContext = { /** The signed-in user, so their own rows read "You". */ currentUserId: Accessor; /** - * Display name for an actor id. Resolves to `undefined` when the id is - * not a user (automation rows), `''` while loading, else the name. + * Display name for a user actor id. Resolves to `undefined` when the id + * is not a user, `''` while loading, else the name. */ displayName: (actorId: Accessor) => Accessor; + /** + * Display name for a bot by bare UUID. First-party bots resolve at once + * from constants; team bots resolve from the bots list, `undefined` while + * it loads, `Bot` when the list does not know the id. + */ + botName: (botId: Accessor) => Accessor; /** Name, icon, and link target for a referenced entity. */ entityDisplay: ( entityId: Accessor, @@ -74,6 +85,16 @@ function appActivityContext(): ActivityContext { const [name] = useDisplayName(id, { emailFallback: 'local-part' }); return name; }, + botName: (botId) => { + const bots = useBotsQuery(); + return () => { + const id = botId(); + const firstParty = firstPartyBotName(id); + if (firstParty) return firstParty; + if (!bots.isSuccess) return undefined; + return getBotDisplayName(`bot|${id}`, undefined, bots.data); + }; + }, entityDisplay: (entityId, entityType) => usePropertyEntityDisplay(entityId, entityType), propertyDefinition: (propertyId) => { diff --git a/apps/web/src/features/activity/core/actor.test.ts b/apps/web/src/features/activity/core/actor.test.ts new file mode 100644 index 00000000000..de74b63112b --- /dev/null +++ b/apps/web/src/features/activity/core/actor.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { parseActor } from './actor'; + +describe('parseActor', () => { + it('parses users from macro ids', () => { + expect(parseActor('macro|sarah@example.com')).toEqual({ + kind: 'user', + id: 'macro|sarah@example.com', + }); + }); + + it('parses bots from bot principals', () => { + expect(parseActor('bot|00000000-0000-0000-0000-00000000a1a1')).toEqual({ + kind: 'bot', + botId: '00000000-0000-0000-0000-00000000a1a1', + }); + }); + + it('keeps anything else raw', () => { + expect(parseActor('system:nightly')).toEqual({ + kind: 'unknown', + raw: 'system:nightly', + }); + expect(parseActor('bot|')).toEqual({ kind: 'unknown', raw: 'bot|' }); + expect(parseActor('macro|no-at-sign')).toEqual({ + kind: 'unknown', + raw: 'macro|no-at-sign', + }); + }); +}); diff --git a/apps/web/src/features/activity/core/actor.ts b/apps/web/src/features/activity/core/actor.ts new file mode 100644 index 00000000000..a0e97cbaa84 --- /dev/null +++ b/apps/web/src/features/activity/core/actor.ts @@ -0,0 +1,24 @@ +import { type MacroId, tryMacroId } from '@core/user/macroId'; + +/** + * Who performed an activity event, parsed once from the wire `actorId`. + * The backend emits `macro|` for users and `bot|` for bots + * (first-party agents, team bots, and the system principal). Anything else + * is preserved raw so the row can still say something honest. + */ +export type Actor = + | { kind: 'user'; id: MacroId } + | { kind: 'bot'; botId: string } + | { kind: 'unknown'; raw: string }; + +const BOT_PREFIX = 'bot|'; + +export function parseActor(actorId: string): Actor { + const user = tryMacroId(actorId); + if (user) return { kind: 'user', id: user }; + if (actorId.startsWith(BOT_PREFIX)) { + const botId = actorId.slice(BOT_PREFIX.length); + if (botId.length > 0) return { kind: 'bot', botId }; + } + return { kind: 'unknown', raw: actorId }; +} diff --git a/apps/web/src/features/activity/primitives/actor-name.test.ts b/apps/web/src/features/activity/primitives/actor-name.test.ts index 9a38a7a9e51..40a8663eebd 100644 --- a/apps/web/src/features/activity/primitives/actor-name.test.ts +++ b/apps/web/src/features/activity/primitives/actor-name.test.ts @@ -1,3 +1,6 @@ +import { MACRO_AGENT_PRINCIPAL_ID } from '@core/constant/macroAgent'; +import { MACRO_SYSTEM_PRINCIPAL_ID } from '@core/constant/macroSystem'; +import { createRoot } from 'solid-js'; import { describe, expect, it } from 'vitest'; import { createMockActivityContext, @@ -5,22 +8,52 @@ import { } from '../tests/mock-context'; import { createActorName } from './actor-name'; +function nameOf( + context: ReturnType, + actorId: string +): string { + return createRoot((dispose) => { + const name = createActorName(context, () => actorId)(); + dispose(); + return name; + }); +} + describe('createActorName', () => { const context = createMockActivityContext(); it('names the viewer "You"', () => { - expect(createActorName(context, () => MOCK_VIEWER_ID)()).toBe('You'); + expect(nameOf(context, MOCK_VIEWER_ID)).toBe('You'); }); it('resolves other users through displayName', () => { - expect(createActorName(context, () => 'macro|sarah@example.com')()).toBe( - 'sarah' + expect(nameOf(context, 'macro|sarah@example.com')).toBe('sarah'); + }); + + it('names the system principal "System"', () => { + expect(nameOf(context, MACRO_SYSTEM_PRINCIPAL_ID)).toBe('System'); + }); + + it('names first-party bots from their constants', () => { + expect(nameOf(context, MACRO_AGENT_PRINCIPAL_ID)).toBe('Macro'); + }); + + it('resolves team bots through botName', () => { + expect(nameOf(context, 'bot|deadbeef-0000-0000-0000-000000000001')).toBe( + 'Bot deadbeef' ); }); - it('labels non-user actors as automation', () => { - expect(createActorName(context, () => 'system:nightly')()).toBe( - 'Automation' + it('reads empty while a team bot name is still loading', () => { + const loading = createMockActivityContext({ + botName: () => () => undefined, + }); + expect(nameOf(loading, 'bot|deadbeef-0000-0000-0000-000000000001')).toBe( + '' ); }); + + it('never says "Automation" for ids it cannot parse', () => { + expect(nameOf(context, 'system:nightly')).toBe('Unknown'); + }); }); diff --git a/apps/web/src/features/activity/primitives/actor-name.ts b/apps/web/src/features/activity/primitives/actor-name.ts index f7e0d92013f..2c5c2139e9e 100644 --- a/apps/web/src/features/activity/primitives/actor-name.ts +++ b/apps/web/src/features/activity/primitives/actor-name.ts @@ -1,16 +1,33 @@ -import type { Accessor } from 'solid-js'; +import { type Accessor, createMemo } from 'solid-js'; +import { match } from 'ts-pattern'; import type { ActivityContext } from '../context/activity-context'; +import { parseActor } from '../core/actor'; -/** "You" for the viewer, "Automation" for non-user actors, else the name. */ +/** + * The name an activity row shows for its actor. The viewer reads "You", + * other users read their display name, bots read their bot name (the + * system principal reads "System"), and ids the app cannot parse read + * "Unknown". Resolvers are created lazily per actor kind so a user row + * never subscribes to the bots list. + */ export function createActorName( - context: Pick, + context: Pick, actorId: Accessor ): Accessor { - const remote = context.displayName(actorId); - return () => { - const name = remote(); - if (name === undefined) return 'Automation'; - if (actorId() === context.currentUserId()) return 'You'; - return name; - }; + const resolver = createMemo>(() => { + const id = actorId(); + if (id === context.currentUserId()) return () => 'You'; + return match(parseActor(id)) + .with({ kind: 'user' }, (actor) => { + const remote = context.displayName(() => actor.id); + return () => remote() ?? 'Unknown'; + }) + .with({ kind: 'bot' }, (actor) => { + const remote = context.botName(() => actor.botId); + return () => remote() ?? ''; + }) + .with({ kind: 'unknown' }, () => () => 'Unknown') + .exhaustive(); + }); + return () => resolver()(); } diff --git a/apps/web/src/features/activity/tests/mock-context.ts b/apps/web/src/features/activity/tests/mock-context.ts index 03444b570cc..6f1b17b6e0c 100644 --- a/apps/web/src/features/activity/tests/mock-context.ts +++ b/apps/web/src/features/activity/tests/mock-context.ts @@ -1,3 +1,4 @@ +import { firstPartyBotName } from '@queries/channel/message-sender'; import type { Client } from '@urql/core'; import type { ActivityContext } from '../context/activity-context'; import { createMockGraphql, type MockGraphql } from './mock-graphql'; @@ -11,7 +12,8 @@ export type MockActivityContext = ActivityContext & { /** * In-memory implementations of every activity dependency. Entities resolve * to `Entity ` and link as markdown blocks; actor ids of the form - * `macro|name@…` resolve to `name`, anything else reads as automation. + * `macro|name@…` resolve to `name`; bot ids resolve to their first-party + * name or `Bot `. */ export function createMockActivityContext( overrides: Partial = {} @@ -26,6 +28,10 @@ export function createMockActivityContext( if (!id.startsWith('macro|')) return () => undefined; return () => id.slice('macro|'.length).split('@')[0] ?? ''; }, + botName: (botId) => () => { + const id = botId(); + return firstPartyBotName(id) ?? `Bot ${id.split('-')[0]}`; + }, entityDisplay: (entityId) => ({ name: () => `Entity ${entityId()}`, icon: () => null, diff --git a/apps/web/src/lib/core/constant/macroSystem.ts b/apps/web/src/lib/core/constant/macroSystem.ts new file mode 100644 index 00000000000..b8f525361be --- /dev/null +++ b/apps/web/src/lib/core/constant/macroSystem.ts @@ -0,0 +1,25 @@ +/** + * Identity for the autonomous Macro platform principal. Mirrors + * `bot_id::MACRO_SYSTEM_BOT_ID` on the backend, which attributes actions the + * platform takes on its own (onboarding seeds, scheduled jobs) to this bot. + */ +export const MACRO_SYSTEM_BOT_ID = '00000000-0000-0000-0000-000000005759'; + +/** + * Canonical principal id for the system bot, matching the `bot|` form + * used for bot senders, participants, and activity actors everywhere else. + */ +export const MACRO_SYSTEM_PRINCIPAL_ID = `bot|${MACRO_SYSTEM_BOT_ID}`; + +/** Display name for the system principal. */ +export const MACRO_SYSTEM_NAME = 'System'; + +/** + * Whether an id refers to the system bot. Accepts both the bare UUID and the + * `bot|` participant/sender form. + */ +export function isMacroSystemId(id: string | undefined): boolean { + if (!id) return false; + const bare = id.startsWith('bot|') ? id.slice('bot|'.length) : id; + return bare === MACRO_SYSTEM_BOT_ID; +} diff --git a/apps/web/src/lib/queries/channel/message-sender.ts b/apps/web/src/lib/queries/channel/message-sender.ts index 22440dcd59f..f449013b391 100644 --- a/apps/web/src/lib/queries/channel/message-sender.ts +++ b/apps/web/src/lib/queries/channel/message-sender.ts @@ -2,6 +2,7 @@ import { CURSOR_BOT_NAME, isCursorBotId } from '@core/constant/cursorAgent'; import { isMacroAgentId, MACRO_AGENT_NAME } from '@core/constant/macroAgent'; import { isMacroCoderId, MACRO_CODER_NAME } from '@core/constant/macroCoder'; import { isMacroNewId, MACRO_NEW_NAME } from '@core/constant/macroNew'; +import { isMacroSystemId, MACRO_SYSTEM_NAME } from '@core/constant/macroSystem'; import type { ApiChannelMessage, ApiThreadReply, @@ -35,11 +36,16 @@ export function senderFromStorageId(senderId: string): ApiMessageSender { return { type: 'user', id: senderId }; } -function systemBotDisplayName(id: string): string | undefined { +/** + * Display name for a first-party bot (bare UUID or `bot|`), resolved + * from constants so it never waits on the bots list. Undefined for team bots. + */ +export function firstPartyBotName(id: string): string | undefined { if (isMacroAgentId(id)) return MACRO_AGENT_NAME; if (isMacroCoderId(id)) return MACRO_CODER_NAME; if (isMacroNewId(id)) return MACRO_NEW_NAME; if (isCursorBotId(id)) return CURSOR_BOT_NAME; + if (isMacroSystemId(id)) return MACRO_SYSTEM_NAME; return undefined; } @@ -51,7 +57,7 @@ export function getBotDisplayName( ): string | undefined { const parsed = sender ?? senderFromStorageId(senderId); const systemName = - systemBotDisplayName(parsed.id) ?? systemBotDisplayName(senderId); + firstPartyBotName(parsed.id) ?? firstPartyBotName(senderId); if (parsed.type !== 'bot' && !systemName) return undefined; diff --git a/apps/web/src/lib/queries/channel/tests/message-sender.test.ts b/apps/web/src/lib/queries/channel/tests/message-sender.test.ts index 2d34ebdde64..e351360e3cf 100644 --- a/apps/web/src/lib/queries/channel/tests/message-sender.test.ts +++ b/apps/web/src/lib/queries/channel/tests/message-sender.test.ts @@ -2,9 +2,15 @@ import { MACRO_CODER_NAME, MACRO_CODER_PRINCIPAL_ID, } from '@core/constant/macroCoder'; +import { + MACRO_SYSTEM_BOT_ID, + MACRO_SYSTEM_NAME, + MACRO_SYSTEM_PRINCIPAL_ID, +} from '@core/constant/macroSystem'; import { describe, expect, it } from 'vitest'; import { type ChannelMessageWithMaybeSender, + firstPartyBotName, getBotDisplayName, normalizeChannelMessageSender, senderFromStorageId, @@ -102,6 +108,16 @@ describe('message sender normalization', () => { expect(getBotDisplayName(MACRO_CODER_PRINCIPAL_ID)).toBe(MACRO_CODER_NAME); }); + it('names the system principal without channel bot data', () => { + expect(getBotDisplayName(MACRO_SYSTEM_PRINCIPAL_ID)).toBe( + MACRO_SYSTEM_NAME + ); + expect(firstPartyBotName(MACRO_SYSTEM_BOT_ID)).toBe(MACRO_SYSTEM_NAME); + expect(firstPartyBotName('00000000-0000-0000-0000-000000000002')).toBe( + undefined + ); + }); + it('uses a generic label rather than exposing an unknown bot UUID', () => { expect(getBotDisplayName('bot|00000000-0000-0000-0000-000000000002')).toBe( 'Bot' From 6d6be1a3b0427aedc12dcd49b3b6523c16fcdb9b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 9 Sep 2026 02:41:40 +0000 Subject: [PATCH 2/3] perf(activity): subscribe to the bots list once per context consumer, lazily, instead of per bot row Co-authored-by: teo --- .../context/activity-context.test.tsx | 64 +++++++++++++++++++ .../activity/context/activity-context.tsx | 32 +++++++--- 2 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 apps/web/src/features/activity/context/activity-context.test.tsx diff --git a/apps/web/src/features/activity/context/activity-context.test.tsx b/apps/web/src/features/activity/context/activity-context.test.tsx new file mode 100644 index 00000000000..78fe64b4bbc --- /dev/null +++ b/apps/web/src/features/activity/context/activity-context.test.tsx @@ -0,0 +1,64 @@ +import { MACRO_SYSTEM_BOT_ID } from '@core/constant/macroSystem'; +import { createRoot } from 'solid-js'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const useBotsQuery = vi.fn(); + +vi.mock('@queries/bots/bots', () => ({ + useBotsQuery: () => useBotsQuery(), +})); +vi.mock('@core/context/user', () => ({ useUserId: () => () => 'me' })); +vi.mock('@core/user', () => ({ + tryMacroId: () => undefined, + useDisplayName: () => [() => ''], +})); +vi.mock('@property/editor/hooks/useAllProperties', () => ({ + useAllProperties: () => () => [], +})); +vi.mock('@property/hooks', () => ({ usePropertyEntityDisplay: () => ({}) })); +vi.mock('@service-storage/graphql-soup', () => ({ + getGraphqlSoupClient: () => ({}), +})); + +const { useActivityContext } = await import('./activity-context'); + +const TEAM_BOT = '11111111-1111-4111-8111-111111111111'; +const OTHER_BOT = '22222222-2222-4222-8222-222222222222'; + +describe('appActivityContext.botName', () => { + beforeEach(() => { + useBotsQuery.mockReset(); + useBotsQuery.mockReturnValue({ + isSuccess: true, + data: [ + { id: TEAM_BOT, name: 'Triage' }, + { id: OTHER_BOT, name: 'Digest' }, + ], + }); + }); + + it('subscribes to the bots list once per consumer, however many bot rows it names', () => { + createRoot((dispose) => { + const context = useActivityContext(); + const first = context.botName(() => TEAM_BOT); + const second = context.botName(() => OTHER_BOT); + + expect(first()).toBe('Triage'); + expect(second()).toBe('Digest'); + expect(first()).toBe('Triage'); + expect(useBotsQuery).toHaveBeenCalledTimes(1); + dispose(); + }); + }); + + it('never fetches the bots list for first-party bots', () => { + createRoot((dispose) => { + const context = useActivityContext(); + const name = context.botName(() => MACRO_SYSTEM_BOT_ID); + + expect(name()).toBe('System'); + expect(useBotsQuery).not.toHaveBeenCalled(); + dispose(); + }); + }); +}); diff --git a/apps/web/src/features/activity/context/activity-context.tsx b/apps/web/src/features/activity/context/activity-context.tsx index c7c189476ea..ff7f3b52d9d 100644 --- a/apps/web/src/features/activity/context/activity-context.tsx +++ b/apps/web/src/features/activity/context/activity-context.tsx @@ -11,7 +11,14 @@ import { import type { EntityType } from '@service-properties/generated/schemas/entityType'; import { getGraphqlSoupClient } from '@service-storage/graphql-soup'; import type { Client } from '@urql/core'; -import { type Accessor, createContext, type JSX, useContext } from 'solid-js'; +import { + type Accessor, + createContext, + getOwner, + type JSX, + runWithOwner, + useContext, +} from 'solid-js'; /** Resolved display for one referenced entity: name, icon, and link target. */ export type EntityDisplay = { @@ -76,6 +83,13 @@ export function useActivityContext(): ActivityContext { function appActivityContext(): ActivityContext { const userId = useUserId(); + // One bots subscription per consumer, made under the consumer's owner the + // first time a bot row asks for a name and reused after that, so it is not + // rebuilt each time a recycled row changes actor and user-only surfaces + // never fetch the list at all. + const owner = getOwner(); + let bots: ReturnType | undefined; + const botsQuery = () => (bots ??= runWithOwner(owner, useBotsQuery)); return { graphql: () => getGraphqlSoupClient(), currentUserId: () => userId() ?? '', @@ -85,15 +99,13 @@ function appActivityContext(): ActivityContext { const [name] = useDisplayName(id, { emailFallback: 'local-part' }); return name; }, - botName: (botId) => { - const bots = useBotsQuery(); - return () => { - const id = botId(); - const firstParty = firstPartyBotName(id); - if (firstParty) return firstParty; - if (!bots.isSuccess) return undefined; - return getBotDisplayName(`bot|${id}`, undefined, bots.data); - }; + botName: (botId) => () => { + const id = botId(); + const firstParty = firstPartyBotName(id); + if (firstParty) return firstParty; + const list = botsQuery(); + if (!list?.isSuccess) return undefined; + return getBotDisplayName(`bot|${id}`, undefined, list.data); }, entityDisplay: (entityId, entityType) => usePropertyEntityDisplay(entityId, entityType), From 0f1e1d9d00256344ec960f8630219ed3ed316e98 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 9 Sep 2026 04:01:44 +0000 Subject: [PATCH 3/3] fix(activity): read team-bot rows as Bot when the bots list fails to load, not blank Co-authored-by: teo --- .../context/activity-context.test.tsx | 26 +++++++++++++++++++ .../activity/context/activity-context.tsx | 6 ++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/apps/web/src/features/activity/context/activity-context.test.tsx b/apps/web/src/features/activity/context/activity-context.test.tsx index 78fe64b4bbc..df50fb610ca 100644 --- a/apps/web/src/features/activity/context/activity-context.test.tsx +++ b/apps/web/src/features/activity/context/activity-context.test.tsx @@ -29,6 +29,7 @@ describe('appActivityContext.botName', () => { beforeEach(() => { useBotsQuery.mockReset(); useBotsQuery.mockReturnValue({ + isPending: false, isSuccess: true, data: [ { id: TEAM_BOT, name: 'Triage' }, @@ -51,6 +52,31 @@ describe('appActivityContext.botName', () => { }); }); + it('is undefined while the list loads and `Bot` once it has failed', () => { + useBotsQuery.mockReturnValue({ + isPending: true, + isSuccess: false, + data: undefined, + }); + createRoot((dispose) => { + const context = useActivityContext(); + expect(context.botName(() => TEAM_BOT)()).toBeUndefined(); + dispose(); + }); + + useBotsQuery.mockReturnValue({ + isPending: false, + isSuccess: false, + isError: true, + data: undefined, + }); + createRoot((dispose) => { + const context = useActivityContext(); + expect(context.botName(() => TEAM_BOT)()).toBe('Bot'); + dispose(); + }); + }); + it('never fetches the bots list for first-party bots', () => { createRoot((dispose) => { const context = useActivityContext(); diff --git a/apps/web/src/features/activity/context/activity-context.tsx b/apps/web/src/features/activity/context/activity-context.tsx index ff7f3b52d9d..9e71e643f56 100644 --- a/apps/web/src/features/activity/context/activity-context.tsx +++ b/apps/web/src/features/activity/context/activity-context.tsx @@ -58,7 +58,7 @@ export type ActivityContext = { /** * Display name for a bot by bare UUID. First-party bots resolve at once * from constants; team bots resolve from the bots list, `undefined` while - * it loads, `Bot` when the list does not know the id. + * it loads, `Bot` when the list does not know the id or failed to load. */ botName: (botId: Accessor) => Accessor; /** Name, icon, and link target for a referenced entity. */ @@ -104,8 +104,8 @@ function appActivityContext(): ActivityContext { const firstParty = firstPartyBotName(id); if (firstParty) return firstParty; const list = botsQuery(); - if (!list?.isSuccess) return undefined; - return getBotDisplayName(`bot|${id}`, undefined, list.data); + if (!list || list.isPending) return undefined; + return getBotDisplayName(`bot|${id}`, undefined, list.data ?? []); }, entityDisplay: (entityId, entityType) => usePropertyEntityDisplay(entityId, entityType),