From 52a7a88a33217f46d51aa90cfb919d22cf894d88 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 27 Sep 2026 18:07:28 -0700 Subject: [PATCH 1/4] fix(chat): keep find stable across footnotes and pending chats, and match mention chips next to punctuation --- .../components/chat-content/chat-content.tsx | 18 ++- .../mothership-chat/chat-find-text.ts | 1 + .../mothership-chat/use-chat-find.test.tsx | 128 ++++++++++++++++++ .../mothership-chat/use-chat-find.ts | 10 +- .../user-message-content/utils.test.ts | 34 +++++ .../components/user-message-content/utils.ts | 5 +- 6 files changed, 188 insertions(+), 8 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find.test.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.test.ts diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx index a7c79cc6fde..2eb61214768 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx @@ -366,7 +366,15 @@ const MARKDOWN_COMPONENTS = { ) }, - a({ children, href }: { children?: React.ReactNode; href?: string }) { + a({ + children, + href, + 'data-footnote-ref': footnoteRef, + }: { + children?: React.ReactNode + href?: string + 'data-footnote-ref'?: boolean + }) { if (href?.startsWith(SOURCE_LINK_PREFIX)) { return ( @@ -393,7 +401,13 @@ const MARKDOWN_COMPONENTS = { ) } return ( - + {children} ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/chat-find-text.ts b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/chat-find-text.ts index 125d10ca61d..41fe11f36cd 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/chat-find-text.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/chat-find-text.ts @@ -22,6 +22,7 @@ export function getChatFindText(markdown: string): string { return node.value if (node.type === 'image' || node.type === 'imageReference') return '\uffff' if (node.type === 'break') return '\n' + if (node.type === 'footnoteDefinition') return '' if (!('children' in node)) return '' return node.children .map(text) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find.test.tsx new file mode 100644 index 00000000000..4fcf14cabf3 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find.test.tsx @@ -0,0 +1,128 @@ +/** + * @vitest-environment jsdom + */ +import { act, useRef } from 'react' +import type { Virtualizer } from '@tanstack/react-virtual' +import { createRoot } from 'react-dom/client' +import { renderToStaticMarkup } from 'react-dom/server' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { ChatContent } from '@/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content' +import { useChatFind } from '@/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find' +import type { ChatMessage } from '@/app/workspace/[workspaceId]/home/types' + +type ChatFind = ReturnType + +interface HarnessProps { + chatId?: string + messages: ChatMessage[] + onRender: (find: ChatFind) => void +} + +// double-cast-allowed: the hook only calls scrollToIndex on the virtualizer +const virtualizer = { scrollToIndex: () => {} } as unknown as Virtualizer + +function Harness({ chatId, messages, onRender }: HarnessProps) { + const containerRef = useRef(null) + const scrollElementRef = useRef(null) + const find = useChatFind({ + chatId, + messages, + hiddenUserByIndex: [], + containerRef, + scrollElementRef, + virtualizer, + }) + onRender(find) + return ( +
+
+ {messages.map((message, index) => ( +
), + }} + /> + ))} +
+
+ ) +} + +const cleanups: (() => void)[] = [] + +function renderFind(props: Omit) { + let find: ChatFind | undefined + const host = document.createElement('div') + document.body.append(host) + const root = createRoot(host) + const render = (next: Omit) => + act(() => root.render( (find = value)} />)) + cleanups.push(() => { + act(() => root.unmount()) + host.remove() + }) + render(props) + return { + get current() { + if (!find) throw new Error('Hook was not rendered') + return find + }, + rerender: render, + search(query: string) { + act(() => { + host.firstElementChild?.dispatchEvent( + new KeyboardEvent('keydown', { key: 'f', metaKey: true, bubbles: true }) + ) + }) + act(() => find?.onQueryChange(query)) + }, + } +} + +function assistant(id: string, content: string): ChatMessage { + return { id, role: 'assistant', content } +} + +class TestHighlight extends Set {} + +beforeEach(() => { + vi.stubGlobal('Highlight', TestHighlight) + vi.stubGlobal('CSS', { highlights: new Map() }) + Range.prototype.getBoundingClientRect = () => new DOMRect() +}) + +afterEach(() => { + for (const cleanup of cleanups.splice(0)) cleanup() + vi.unstubAllGlobals() + Reflect.deleteProperty(Range.prototype, 'getBoundingClientRect') +}) + +describe('useChatFind', () => { + it('keeps the open search when a pending chat is persisted, and resets on a chat switch', () => { + const messages = [assistant('a', 'The answer is here.')] + const find = renderFind({ messages }) + find.search('answer') + expect(find.current).toMatchObject({ isOpen: true, query: 'answer', count: 1 }) + + find.rerender({ chatId: 'chat-1', messages }) + expect(find.current).toMatchObject({ isOpen: true, query: 'answer', count: 1 }) + + find.rerender({ chatId: 'chat-2', messages }) + expect(find.current).toMatchObject({ isOpen: false, query: '', count: 0 }) + }) + + it('counts the same footnoted matches in the index as it highlights in the rendered message', async () => { + const find = renderFind({ + chatId: 'chat', + messages: [assistant('a', 'Chapter 1 cites this[^1].\n\n[^1]: Page 1.')], + }) + find.search('1') + await act(() => new Promise((resolve) => requestAnimationFrame(() => resolve()))) + + const highlighted = CSS.highlights.get('chat-find') + expect(find.current.count).toBe(1) + expect(highlighted?.size).toBe(find.current.count) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find.ts b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find.ts index fe6eb35545a..837b1b0cbb0 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find.ts @@ -23,7 +23,7 @@ import type { ChatMessage } from '@/app/workspace/[workspaceId]/home/types' const MATCH_LIMIT = 500 const EXCLUDED_CONTENT = - 'button, [aria-hidden="true"], [data-agent-group], [data-chat-activity], [data-interaction-card], [data-chat-find-ignore]' + 'button, [aria-hidden="true"], [data-agent-group], [data-chat-activity], [data-interaction-card], [data-chat-find-ignore], [data-footnote-ref], [data-footnotes]' const TEXT_BLOCK = 'p, h1, h2, h3, h4, h5, h6, pre, li, td, th' interface UseChatFindProps { @@ -127,9 +127,11 @@ export function useChatFind({ const [index, setIndex] = useState(0) if (scope !== chatId) { setScope(chatId) - setIsOpen(false) - setQuery('') - setIndex(0) + if (scope !== undefined) { + setIsOpen(false) + setQuery('') + setIndex(0) + } } const deferredQuery = useDeferredValue(query) const term = isOpen ? deferredQuery.trim() : '' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.test.ts new file mode 100644 index 00000000000..5b70f6efbc7 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from 'vitest' +import { computeMentionRanges } from '@/app/workspace/[workspaceId]/home/components/user-message-content/utils' +import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types' + +vi.mock('@/blocks/integration-matcher', () => ({ + getIntegrationMatcher: () => ({ regex: null, byName: new Map() }), +})) + +function spans(text: string, contexts: ChatMessageContext[]): string[] { + return computeMentionRanges(text, contexts).map((range) => text.slice(range.start, range.end)) +} + +describe('computeMentionRanges', () => { + const workflow: ChatMessageContext = { kind: 'workflow', label: 'Workflow' } + + it('matches a mention followed by punctuation', () => { + expect(spans('Run @Workflow, then stop.', [workflow])).toEqual(['@Workflow']) + expect(spans('(see @Workflow)', [workflow])).toEqual(['@Workflow']) + }) + + it('matches every repeat of a mention separated by one space', () => { + expect(spans('@Workflow @Workflow', [workflow])).toEqual(['@Workflow', '@Workflow']) + }) + + it('does not match a mention that is a prefix of a longer name', () => { + expect(spans('@Workflow-2', [workflow])).toEqual([]) + }) + + it('matches slash commands by their slash prefix', () => { + expect(spans('/research the topic', [{ kind: 'slash_command', label: 'research' }])).toEqual([ + '/research', + ]) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts index 7a85720f5dd..2efb863e367 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts @@ -28,9 +28,10 @@ export function computeMentionRanges(text: string, contexts: ChatMessageContext[ for (const rawCtx of contexts) { if (!rawCtx.label) continue const ctx = withResolvedBlockType(rawCtx) - const prefix = ctx.kind === 'skill' || ctx.kind === 'mcp' ? '/' : '@' + const prefix = + ctx.kind === 'skill' || ctx.kind === 'mcp' || ctx.kind === 'slash_command' ? '/' : '@' const token = `${prefix}${ctx.label}` - const pattern = new RegExp(`(^|\\s)(${escapeRegExp(token)})(\\s|$)`, 'g') + const pattern = new RegExp(`(^|\\s)(${escapeRegExp(token)})(?=[\\s.,;:!?)\\]]|$)`, 'g') let match: RegExpExecArray | null while ((match = pattern.exec(text)) !== null) { const leadingSpace = match[1] From 4a5d1e8c92b2025497cadb504604e55110a93dbd Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 27 Sep 2026 18:13:14 -0700 Subject: [PATCH 2/4] fix(chat): keep the longest overlapping mention chip and centralize the integration matcher mock --- .../home/components/composer/composer.test.tsx | 5 ++--- .../home/organization-home.test.tsx | 3 ++- .../components/mothership-chat/use-chat-find.ts | 1 + .../prompt-editor/use-prompt-editor.test.tsx | 5 ++--- .../user-message-content/utils.test.ts | 17 ++++++++++++++--- .../components/user-message-content/utils.ts | 8 ++++++-- packages/testing/src/mocks/index.ts | 1 + .../src/mocks/integration-matcher.mock.ts | 17 +++++++++++++++++ 8 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 packages/testing/src/mocks/integration-matcher.mock.ts diff --git a/apps/sim/app/o/[organizationId]/home/components/composer/composer.test.tsx b/apps/sim/app/o/[organizationId]/home/components/composer/composer.test.tsx index 9484a22271e..6ddf920ea42 100644 --- a/apps/sim/app/o/[organizationId]/home/components/composer/composer.test.tsx +++ b/apps/sim/app/o/[organizationId]/home/components/composer/composer.test.tsx @@ -5,6 +5,7 @@ import { deploymentShapeMock, deploymentShapeMockFns, } from '@sim/testing/mocks/deployment-shape.mock' +import { integrationMatcherMock } from '@sim/testing/mocks/integration-matcher.mock' import { organizationProviderMock, organizationProviderMockFns, @@ -57,9 +58,7 @@ vi.mock('@/hooks/queries/skills', () => ({ }), })) vi.mock('@/hooks/queries/mcp', () => ({ useMcpToolServers: () => ({ data: [] }) })) -vi.mock('@/blocks/integration-matcher', () => ({ - getIntegrationMatcher: () => ({ regex: null, byName: new Map() }), -})) +vi.mock('@/blocks/integration-matcher', () => integrationMatcherMock) vi.mock( '@/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown', async () => { diff --git a/apps/sim/app/o/[organizationId]/home/organization-home.test.tsx b/apps/sim/app/o/[organizationId]/home/organization-home.test.tsx index 0561cfcc0e9..f7da9cd270f 100644 --- a/apps/sim/app/o/[organizationId]/home/organization-home.test.tsx +++ b/apps/sim/app/o/[organizationId]/home/organization-home.test.tsx @@ -7,6 +7,7 @@ import { deploymentShapeMock, deploymentShapeMockFns, } from '@sim/testing/mocks/deployment-shape.mock' +import { integrationMatcherMock } from '@sim/testing/mocks/integration-matcher.mock' import { kbConnectorsQueriesMock, kbConnectorsQueriesMockFns, @@ -46,7 +47,7 @@ vi.mock('@/app/workspace/[workspaceId]/providers/feature-flags-provider', () => useFeatureFlag: (name: string) => (name === 'mothership-plan-mode' ? mocks.plan : false), })) vi.mock('@/lib/core/config/deployment-shape', () => deploymentShapeMock) -vi.mock('@/blocks/integration-matcher', () => ({ mentionifyIntegrations: (text: string) => text })) +vi.mock('@/blocks/integration-matcher', () => integrationMatcherMock) vi.mock('next/navigation', () => nextNavigationMock) vi.mock('@tanstack/react-query', () => reactQueryMock) vi.mock('@/app/workspace/[workspaceId]/home/hooks/use-resource-panel', () => ({ diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find.ts b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find.ts index 837b1b0cbb0..5611a35e080 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find.ts @@ -127,6 +127,7 @@ export function useChatFind({ const [index, setIndex] = useState(0) if (scope !== chatId) { setScope(chatId) + // A pending chat adopting its id (undefined → id) is the same conversation. if (scope !== undefined) { setIsOpen(false) setQuery('') diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/use-prompt-editor.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/use-prompt-editor.test.tsx index e0abea2236c..b3f1316c96a 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/use-prompt-editor.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/use-prompt-editor.test.tsx @@ -2,14 +2,13 @@ * @vitest-environment jsdom */ import { act, type ReactNode } from 'react' +import { integrationMatcherMock } from '@sim/testing/mocks/integration-matcher.mock' import { createRoot, type Root } from 'react-dom/client' import { describe, expect, it, vi } from 'vitest' vi.mock('@/hooks/queries/skills', () => ({ useSkills: () => ({ data: [] }) })) vi.mock('@/hooks/queries/mcp', () => ({ useMcpToolServers: () => ({ data: [] }) })) -vi.mock('@/blocks/integration-matcher', () => ({ - getIntegrationMatcher: () => ({ regex: null, byName: new Map() }), -})) +vi.mock('@/blocks/integration-matcher', () => integrationMatcherMock) import { SIM_SELECTION_MIME } from '@/lib/mothership/chat/selection-clipboard' import { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.test.ts index 5b70f6efbc7..84c10196142 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.test.ts @@ -1,10 +1,9 @@ +import { integrationMatcherMock } from '@sim/testing/mocks/integration-matcher.mock' import { describe, expect, it, vi } from 'vitest' import { computeMentionRanges } from '@/app/workspace/[workspaceId]/home/components/user-message-content/utils' import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types' -vi.mock('@/blocks/integration-matcher', () => ({ - getIntegrationMatcher: () => ({ regex: null, byName: new Map() }), -})) +vi.mock('@/blocks/integration-matcher', () => integrationMatcherMock) function spans(text: string, contexts: ChatMessageContext[]): string[] { return computeMentionRanges(text, contexts).map((range) => text.slice(range.start, range.end)) @@ -26,6 +25,18 @@ describe('computeMentionRanges', () => { expect(spans('@Workflow-2', [workflow])).toEqual([]) }) + it('keeps only the longest of overlapping mentions', () => { + const report: ChatMessageContext = { kind: 'file', label: 'report' } + const reportPdf: ChatMessageContext = { kind: 'file', label: 'report.pdf' } + expect(spans('compare @report.pdf with @report', [report, reportPdf])).toEqual([ + '@report.pdf', + '@report', + ]) + expect( + spans('@Workflow 2 and @Workflow', [workflow, { kind: 'workflow', label: 'Workflow 2' }]) + ).toEqual(['@Workflow 2', '@Workflow']) + }) + it('matches slash commands by their slash prefix', () => { expect(spans('/research the topic', [{ kind: 'slash_command', label: 'research' }])).toEqual([ '/research', diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts index 2efb863e367..f33636316b6 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts @@ -45,8 +45,12 @@ export function computeMentionRanges(text: string, contexts: ChatMessageContext[ ranges.push(range) } - ranges.sort((a, b) => a.start - b.start) - return ranges + ranges.sort((a, b) => a.start - b.start || b.end - a.end) + const merged: MentionRange[] = [] + for (const range of ranges) { + if (range.start >= (merged.at(-1)?.end ?? 0)) merged.push(range) + } + return merged } /** diff --git a/packages/testing/src/mocks/index.ts b/packages/testing/src/mocks/index.ts index a1a3e947d8d..c118f2c6922 100644 --- a/packages/testing/src/mocks/index.ts +++ b/packages/testing/src/mocks/index.ts @@ -383,6 +383,7 @@ export { inputValidationMock, inputValidationMockFns, } from './input-validation.mock' +export { integrationMatcherMock } from './integration-matcher.mock' export { integrationsAvailabilityMock, integrationsAvailabilityMockFns, diff --git a/packages/testing/src/mocks/integration-matcher.mock.ts b/packages/testing/src/mocks/integration-matcher.mock.ts new file mode 100644 index 00000000000..6c54be43429 --- /dev/null +++ b/packages/testing/src/mocks/integration-matcher.mock.ts @@ -0,0 +1,17 @@ +import { vi } from 'vitest' + +/** + * Static mock module for `@/blocks/integration-matcher` that knows no integrations, so tests never + * load the block registry: the matcher has no regex and `mentionifyIntegrations` returns its input. + * + * @example + * ```ts + * import { integrationMatcherMock } from '@sim/testing/mocks/integration-matcher.mock' + * + * vi.mock('@/blocks/integration-matcher', () => integrationMatcherMock) + * ``` + */ +export const integrationMatcherMock = { + getIntegrationMatcher: vi.fn(() => ({ regex: null, byName: new Map() })), + mentionifyIntegrations: vi.fn((text: string) => text), +} From b7c2dc5cdf8e1473bb09ae718f0479bca0460ee0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 27 Sep 2026 18:13:37 -0700 Subject: [PATCH 3/4] fix(chat): avoid Array.at in the mention range merge --- .../[workspaceId]/home/components/user-message-content/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts index f33636316b6..a7e4464abfd 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts @@ -48,7 +48,7 @@ export function computeMentionRanges(text: string, contexts: ChatMessageContext[ ranges.sort((a, b) => a.start - b.start || b.end - a.end) const merged: MentionRange[] = [] for (const range of ranges) { - if (range.start >= (merged.at(-1)?.end ?? 0)) merged.push(range) + if (range.start >= (merged[merged.length - 1]?.end ?? 0)) merged.push(range) } return merged } From f884d83137123724716f6ce74564c7a93251e4a2 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 27 Sep 2026 18:29:03 -0700 Subject: [PATCH 4/4] fix(chat): only treat a period as a mention boundary when no name continues after it --- .../home/components/user-message-content/utils.test.ts | 5 +++++ .../home/components/user-message-content/utils.ts | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.test.ts index 84c10196142..49ef5a9902c 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.test.ts @@ -17,6 +17,11 @@ describe('computeMentionRanges', () => { expect(spans('(see @Workflow)', [workflow])).toEqual(['@Workflow']) }) + it('treats a period as a boundary only when no name continues after it', () => { + expect(spans('see @Workflow.', [workflow])).toEqual(['@Workflow']) + expect(spans('open @report.pdf', [{ kind: 'file', label: 'report' }])).toEqual([]) + }) + it('matches every repeat of a mention separated by one space', () => { expect(spans('@Workflow @Workflow', [workflow])).toEqual(['@Workflow', '@Workflow']) }) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts index a7e4464abfd..2f56cc6c6a3 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/utils.ts @@ -31,7 +31,10 @@ export function computeMentionRanges(text: string, contexts: ChatMessageContext[ const prefix = ctx.kind === 'skill' || ctx.kind === 'mcp' || ctx.kind === 'slash_command' ? '/' : '@' const token = `${prefix}${ctx.label}` - const pattern = new RegExp(`(^|\\s)(${escapeRegExp(token)})(?=[\\s.,;:!?)\\]]|$)`, 'g') + const pattern = new RegExp( + `(^|\\s)(${escapeRegExp(token)})(?=[\\s,;:!?)\\]]|\\.(?![\\w-])|$)`, + 'g' + ) let match: RegExpExecArray | null while ((match = pattern.exec(text)) !== null) { const leadingSpace = match[1]