Skip to content

Commit 7134521

Browse files
committed
fix(chat): parse wsres links once, derive file icons from the VFS path
1 parent 0c43b41 commit 7134521

3 files changed

Lines changed: 44 additions & 32 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'prismjs/components/prism-css'
1212
import 'prismjs/components/prism-markup'
1313
import '@sim/emcn/components/code/code.css'
1414
import { Checkbox, CopyCodeButton, cn, highlight, languages } from '@sim/emcn'
15+
import { decodeVfsSegmentSafe } from '@/lib/copilot/vfs/path-utils'
1516
import { extractTextContent } from '@/lib/core/utils/react-node-text'
1617
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
1718
import {
@@ -144,6 +145,20 @@ const WSRES_LINK_KINDS: Record<string, ChatContextKind | undefined> = {
144145
file: 'file',
145146
}
146147

148+
/**
149+
* Label used to pick a file link's extension-aware document icon. The visible
150+
* link text can be a custom title without an extension, so prefer the file
151+
* name carried in the link's VFS path (its last extension-bearing segment).
152+
*/
153+
function fileIconLabel(ref: string, fallback: string): string {
154+
const segments = ref.split('/').filter(Boolean)
155+
for (let i = segments.length - 1; i >= 0; i--) {
156+
const decoded = decodeVfsSegmentSafe(segments[i])
157+
if (decoded.includes('.')) return decoded
158+
}
159+
return fallback
160+
}
161+
147162
const MARKDOWN_COMPONENTS = {
148163
table({ children }: { children?: React.ReactNode }) {
149164
return (
@@ -214,7 +229,10 @@ const MARKDOWN_COMPONENTS = {
214229
},
215230
a({ children, href }: { children?: React.ReactNode; href?: string }) {
216231
if (href?.startsWith('#wsres-')) {
217-
const kind = WSRES_LINK_KINDS[href.match(/^#wsres-(\w+)-/)?.[1] ?? '']
232+
const match = href.match(/^#wsres-(\w+)-(.+)$/)
233+
const type = match?.[1]
234+
const ref = match?.[2]
235+
const kind = type ? WSRES_LINK_KINDS[type] : undefined
218236
const label = extractTextContent(children)
219237
return (
220238
<a
@@ -227,25 +245,21 @@ const MARKDOWN_COMPONENTS = {
227245
)}
228246
onClick={(e) => {
229247
e.preventDefault()
230-
const match = href.match(/^#wsres-(\w+)-(.+)$/)
231-
if (match) {
232-
const type = match[1]
233-
const ref = match[2]
234-
const linkText = label || ref
235-
window.dispatchEvent(
236-
new CustomEvent('wsres-click', {
237-
detail:
238-
type === 'file'
239-
? { type, path: ref, title: linkText }
240-
: { type, id: ref, title: linkText },
241-
})
242-
)
243-
}
248+
if (!type || !ref) return
249+
const linkText = label || ref
250+
window.dispatchEvent(
251+
new CustomEvent('wsres-click', {
252+
detail:
253+
type === 'file'
254+
? { type, path: ref, title: linkText }
255+
: { type, id: ref, title: linkText },
256+
})
257+
)
244258
}}
245259
>
246-
{kind && (
260+
{kind && ref && (
247261
<ContextMentionIcon
248-
context={{ kind, label }}
262+
context={{ kind, label: kind === 'file' ? fileIconLabel(ref, label) : label }}
249263
className='size-[14px] flex-shrink-0 text-[var(--text-icon)]'
250264
/>
251265
)}

apps/sim/lib/copilot/tools/client/store-utils.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resources/types'
66
import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools'
77
import { getReadTargetBlock } from '@/lib/copilot/tools/client/read-block'
88
import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state'
9-
import { decodeVfsSegment } from '@/lib/copilot/vfs/path-utils'
9+
import { decodeVfsSegmentSafe } from '@/lib/copilot/vfs/path-utils'
1010

1111
/** Respond tools are internal handoff tools shown with a friendly generic label. */
1212
const HIDDEN_TOOL_SUFFIX = '_respond'
@@ -82,20 +82,6 @@ function formatReadingLabel(target: string | undefined, state: ClientToolCallSta
8282
}
8383
}
8484

85-
/**
86-
* VFS paths store each segment percent-encoded (see {@link encodeVfsSegment}), so
87-
* a read on "My Report.txt" arrives as "files/My%20Report.txt". Decode for
88-
* display so the user sees the real file name. Falls back to the raw segment when
89-
* it is not valid encoding (e.g. a literal "%" that was never encoded).
90-
*/
91-
function decodeVfsSegmentSafe(segment: string): string {
92-
try {
93-
return decodeVfsSegment(segment)
94-
} catch {
95-
return segment
96-
}
97-
}
98-
9985
function describeReadTarget(path: string | undefined): string | undefined {
10086
if (!path) return undefined
10187

apps/sim/lib/copilot/vfs/path-utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ export function decodeVfsSegment(segment: string): string {
3434
}
3535
}
3636

37+
/**
38+
* Decodes a VFS path segment for display, falling back to the raw segment when
39+
* it is not valid encoding (e.g. a literal "%" that was never encoded).
40+
*/
41+
export function decodeVfsSegmentSafe(segment: string): string {
42+
try {
43+
return decodeVfsSegment(segment)
44+
} catch {
45+
return segment
46+
}
47+
}
48+
3749
export function encodeVfsPathSegments(segments: string[]): string {
3850
return segments.map(encodeVfsSegment).join('/')
3951
}

0 commit comments

Comments
 (0)