From 467fbba587ec705f7132ec0daed5ff73973932d0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 13 Jul 2026 20:05:13 -0700 Subject: [PATCH 1/4] improvement(chat): show resource-type icons on inline workspace resource links --- .../components/chat-content/chat-content.tsx | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) 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 1283dc8617e..d9835fcc0dc 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 @@ -13,13 +13,14 @@ import 'prismjs/components/prism-markup' import '@sim/emcn/components/code/code.css' import { Checkbox, CopyCodeButton, cn, highlight, languages } from '@sim/emcn' import { extractTextContent } from '@/lib/core/utils/react-node-text' +import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon' import { type ContentSegment, PendingTagIndicator, parseSpecialTags, SpecialTags, } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' -import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types' +import type { ChatContextKind, MothershipResource } from '@/app/workspace/[workspaceId]/home/types' import { useSmoothText } from '@/hooks/use-smooth-text' import { sanitizeChatDisplayContent } from './chat-sanitize' @@ -132,6 +133,17 @@ function appendInlineReferenceMarkdown( type TdProps = ComponentPropsWithoutRef<'td'> type ThProps = ComponentPropsWithoutRef<'th'> +/** + * Maps a `#wsres-{type}-{ref}` link's resource type to the chat-context kind + * whose icon represents it, so inline resource references render the same + * type icon as the user-input context chips. + */ +const WSRES_LINK_KINDS: Record = { + workflow: 'workflow', + table: 'table', + file: 'file', +} + const MARKDOWN_COMPONENTS = { table({ children }: { children?: React.ReactNode }) { return ( @@ -202,10 +214,12 @@ const MARKDOWN_COMPONENTS = { }, a({ children, href }: { children?: React.ReactNode; href?: string }) { if (href?.startsWith('#wsres-')) { + const kind = WSRES_LINK_KINDS[href.match(/^#wsres-(\w+)-/)?.[1] ?? ''] + const label = extractTextContent(children) return ( { e.preventDefault() const match = href.match(/^#wsres-(\w+)-(.+)$/) @@ -224,6 +238,12 @@ const MARKDOWN_COMPONENTS = { } }} > + {kind && ( + + )} {children} ) From 9b97c16e29481a747e8ab8a6e56644631694227f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 13 Jul 2026 20:08:04 -0700 Subject: [PATCH 2/4] fix(chat): derive wsres click title from markdown label, not DOM textContent --- .../message-content/components/chat-content/chat-content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d9835fcc0dc..d954b17edcc 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 @@ -226,7 +226,7 @@ const MARKDOWN_COMPONENTS = { if (match) { const type = match[1] const ref = match[2] - const linkText = e.currentTarget.textContent || ref + const linkText = label || ref window.dispatchEvent( new CustomEvent('wsres-click', { detail: From 0c43b413d4d4b10352c1f2b0e33c1ac1b9ad9818 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 13 Jul 2026 20:19:01 -0700 Subject: [PATCH 3/4] fix(chat): keep dashed-underline affordance for unmapped wsres link types --- .../components/chat-content/chat-content.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 d954b17edcc..9883a2dba7a 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 @@ -219,7 +219,12 @@ const MARKDOWN_COMPONENTS = { return ( { e.preventDefault() const match = href.match(/^#wsres-(\w+)-(.+)$/) From 7134521e5f0bd4c86dacd310fbb9c93be40fa9df Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Jul 2026 15:46:40 -0700 Subject: [PATCH 4/4] fix(chat): parse wsres links once, derive file icons from the VFS path --- .../components/chat-content/chat-content.tsx | 48 ++++++++++++------- .../lib/copilot/tools/client/store-utils.ts | 16 +------ apps/sim/lib/copilot/vfs/path-utils.ts | 12 +++++ 3 files changed, 44 insertions(+), 32 deletions(-) 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 9883a2dba7a..f5ffd95f765 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 @@ -12,6 +12,7 @@ import 'prismjs/components/prism-css' import 'prismjs/components/prism-markup' import '@sim/emcn/components/code/code.css' import { Checkbox, CopyCodeButton, cn, highlight, languages } from '@sim/emcn' +import { decodeVfsSegmentSafe } from '@/lib/copilot/vfs/path-utils' import { extractTextContent } from '@/lib/core/utils/react-node-text' import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon' import { @@ -144,6 +145,20 @@ const WSRES_LINK_KINDS: Record = { file: 'file', } +/** + * Label used to pick a file link's extension-aware document icon. The visible + * link text can be a custom title without an extension, so prefer the file + * name carried in the link's VFS path (its last extension-bearing segment). + */ +function fileIconLabel(ref: string, fallback: string): string { + const segments = ref.split('/').filter(Boolean) + for (let i = segments.length - 1; i >= 0; i--) { + const decoded = decodeVfsSegmentSafe(segments[i]) + if (decoded.includes('.')) return decoded + } + return fallback +} + const MARKDOWN_COMPONENTS = { table({ children }: { children?: React.ReactNode }) { return ( @@ -214,7 +229,10 @@ const MARKDOWN_COMPONENTS = { }, a({ children, href }: { children?: React.ReactNode; href?: string }) { if (href?.startsWith('#wsres-')) { - const kind = WSRES_LINK_KINDS[href.match(/^#wsres-(\w+)-/)?.[1] ?? ''] + const match = href.match(/^#wsres-(\w+)-(.+)$/) + const type = match?.[1] + const ref = match?.[2] + const kind = type ? WSRES_LINK_KINDS[type] : undefined const label = extractTextContent(children) return ( { e.preventDefault() - const match = href.match(/^#wsres-(\w+)-(.+)$/) - if (match) { - const type = match[1] - const ref = match[2] - const linkText = label || ref - window.dispatchEvent( - new CustomEvent('wsres-click', { - detail: - type === 'file' - ? { type, path: ref, title: linkText } - : { type, id: ref, title: linkText }, - }) - ) - } + if (!type || !ref) return + const linkText = label || ref + window.dispatchEvent( + new CustomEvent('wsres-click', { + detail: + type === 'file' + ? { type, path: ref, title: linkText } + : { type, id: ref, title: linkText }, + }) + ) }} > - {kind && ( + {kind && ref && ( )} diff --git a/apps/sim/lib/copilot/tools/client/store-utils.ts b/apps/sim/lib/copilot/tools/client/store-utils.ts index 7364bbc1f87..f143602dcd2 100644 --- a/apps/sim/lib/copilot/tools/client/store-utils.ts +++ b/apps/sim/lib/copilot/tools/client/store-utils.ts @@ -6,7 +6,7 @@ import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resources/types' import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools' import { getReadTargetBlock } from '@/lib/copilot/tools/client/read-block' import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state' -import { decodeVfsSegment } from '@/lib/copilot/vfs/path-utils' +import { decodeVfsSegmentSafe } from '@/lib/copilot/vfs/path-utils' /** Respond tools are internal handoff tools shown with a friendly generic label. */ const HIDDEN_TOOL_SUFFIX = '_respond' @@ -82,20 +82,6 @@ function formatReadingLabel(target: string | undefined, state: ClientToolCallSta } } -/** - * VFS paths store each segment percent-encoded (see {@link encodeVfsSegment}), so - * a read on "My Report.txt" arrives as "files/My%20Report.txt". Decode for - * display so the user sees the real file name. Falls back to the raw segment when - * it is not valid encoding (e.g. a literal "%" that was never encoded). - */ -function decodeVfsSegmentSafe(segment: string): string { - try { - return decodeVfsSegment(segment) - } catch { - return segment - } -} - function describeReadTarget(path: string | undefined): string | undefined { if (!path) return undefined diff --git a/apps/sim/lib/copilot/vfs/path-utils.ts b/apps/sim/lib/copilot/vfs/path-utils.ts index 6f88e9507d9..daeb5c0a631 100644 --- a/apps/sim/lib/copilot/vfs/path-utils.ts +++ b/apps/sim/lib/copilot/vfs/path-utils.ts @@ -34,6 +34,18 @@ export function decodeVfsSegment(segment: string): string { } } +/** + * Decodes a VFS path segment for display, falling back to the raw segment when + * it is not valid encoding (e.g. a literal "%" that was never encoded). + */ +export function decodeVfsSegmentSafe(segment: string): string { + try { + return decodeVfsSegment(segment) + } catch { + return segment + } +} + export function encodeVfsPathSegments(segments: string[]): string { return segments.map(encodeVfsSegment).join('/') }