diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/mention-row-content.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/mention-row-content.test.tsx
new file mode 100644
index 00000000000..4e8384d804e
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/mention-row-content.test.tsx
@@ -0,0 +1,67 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { MentionRowContent } from '@/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/mention-row-content'
+
+let container: HTMLDivElement
+let root: Root
+
+/** Stands in for a family renderer that pins trailing content with `ml-auto`, as the log row does. */
+function LogLikeRow() {
+ return (
+ <>
+ Daily digest
+
+ 2m ago
+
+ >
+ )
+}
+
+function renderRow(node: React.ReactNode) {
+ act(() => {
+ root.render()
+ })
+}
+
+beforeEach(() => {
+ container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+})
+
+afterEach(() => {
+ act(() => root.unmount())
+ container.remove()
+})
+
+describe('MentionRowContent', () => {
+ it('leaves a location-less row unwrapped so `ml-auto` still reaches the row edge', () => {
+ renderRow(
+
+
+
+ )
+
+ const trailing = container.querySelector('[data-testid="trailing"]')
+ expect(trailing).not.toBeNull()
+ expect(trailing?.parentElement?.tagName).toBe('BUTTON')
+ })
+
+ it('wraps and caps the name only when a location follows it', () => {
+ renderRow(
+
+ Enterprise
+
+ )
+
+ const name = container.querySelector('button > span')
+ expect(name?.className).toContain('max-w-[65%]')
+ expect(name?.className).toContain('flex-shrink-0')
+ expect(container.textContent).toContain('Files')
+ expect(container.textContent).toContain('Growth')
+ })
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/mention-row-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/mention-row-content.tsx
new file mode 100644
index 00000000000..aa1500d4786
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/mention-row-content.tsx
@@ -0,0 +1,69 @@
+'use client'
+
+import type { ReactNode } from 'react'
+import { chipContentGap, cn, OverflowText } from '@sim/emcn'
+import { FolderPathLabel } from '@/components/ui'
+import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
+import type { FolderMentionLocation } from '@/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/resource-mention-items'
+
+export interface MentionRowContentProps {
+ /** The resource family's own row rendering, a fragment of the row's flex children. */
+ children: ReactNode
+ /** Present only for folder rows, which need a location to disambiguate same-named siblings. */
+ location?: FolderMentionLocation
+ workspaceName?: string
+}
+
+/**
+ * Body of one flat mention row.
+ *
+ * Rows without a location render their family output as direct children of the row
+ * button, unwrapped. That is load-bearing rather than incidental: renderers such as
+ * the log row pin trailing content with `ml-auto`, which only reaches the row's right
+ * edge while the button is its flex parent. Wrapping every row would silently pull
+ * those timestamps back beside the name.
+ */
+export function MentionRowContent({ children, location, workspaceName }: MentionRowContentProps) {
+ const workspaceLabel = workspaceName ? (
+
+ ) : null
+
+ if (!location) {
+ return (
+ <>
+ {children}
+ {workspaceLabel}
+ >
+ )
+ }
+
+ return (
+ <>
+ {/* Capped rather than shrinkable so a long name cannot squeeze out the segment
+ that tells two same-named folders apart. */}
+
+ {children}
+
+
+
+ {workspaceLabel}
+
+ >
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx
index 3759f6e3d81..dadb1654859 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx
@@ -10,7 +10,6 @@ import {
DropdownMenuSearchInput,
DropdownMenuTrigger,
dropdownMenuRowClass,
- OverflowText,
} from '@sim/emcn'
import { IdentityTile } from '@/components/identity-tile/identity-tile'
import { getWorkspaceInitial } from '@/lib/workspaces/initials'
@@ -32,7 +31,9 @@ import {
MENTION_PREVIEW_DEFAULT_LIMIT,
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
import type { PlusMenuHandle } from '@/app/workspace/[workspaceId]/home/components/user-input/components/constants'
+import { MentionRowContent } from '@/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/mention-row-content'
import {
+ buildFolderMentionLocationMap,
buildMentionPreview,
type ResourceMentionCandidate,
resourceMentionMatches,
@@ -218,6 +219,11 @@ export const PlusMenuDropdown = React.memo(
terminalTabs,
])
+ const folderMentionLocations = useMemo(
+ () => buildFolderMentionLocationMap(visibleResources),
+ [visibleResources]
+ )
+
/**
* Built from this workspace's own inventory, which has no foldered families in an
* organization chat: there each workspace submenu builds its own sections, because
@@ -481,6 +487,7 @@ export const PlusMenuDropdown = React.memo(
const config = type === 'workspace' ? null : getResourceConfig(type)
const workspaceName = 'workspaceName' in item ? item.workspaceName : undefined
const isActive = index === activeIndex
+ const location = folderMentionLocations.get(candidateKey(candidate))
/* Items arrive grouped by family (one group per type, ordered by
RESOURCE_MENU_ORDER), so a type change marks a section boundary.
Deriving the heading from the flat list keeps `activeIndex` — and
@@ -506,25 +513,26 @@ export const PlusMenuDropdown = React.memo(
isActive && 'bg-[var(--surface-hover)]'
)}
>
- {candidate.type === 'workspace' ? (
- <>
-
-
- >
- ) : (
- getResourceConfig(candidate.type).renderDropdownItem({
- item: candidate.item,
- })
- )}
- {typeof workspaceName === 'string' && (
-
- )}
+
+ {candidate.type === 'workspace' ? (
+ <>
+
+
+ >
+ ) : (
+ getResourceConfig(candidate.type).renderDropdownItem({
+ item: candidate.item,
+ })
+ )}
+
)
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/resource-mention-items.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/resource-mention-items.ts
index 4a7e45c129f..9e35e9e1cae 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/resource-mention-items.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/resource-mention-items.ts
@@ -1,6 +1,7 @@
import type { BrowserTabState } from '@sim/browser-protocol'
import type { TerminalTabState } from '@sim/terminal-protocol'
import { browserTabTitle } from '@/lib/browser-agent/tab-label'
+import { folderAncestorChain } from '@/lib/folders/tree'
import { terminalResourceId } from '@/lib/terminal/resource-id'
import { terminalTabTitle } from '@/lib/terminal/tab-label'
import type { AvailableItem } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/resource-folder-tree'
@@ -33,6 +34,69 @@ export function withFolderMentions(
)
}
+export interface FolderMentionLocation {
+ familyType: 'workflow' | 'file' | 'table' | 'knowledgebase'
+ parentNames: string[]
+}
+
+interface FolderMentionNode {
+ id: string
+ name: string
+ parentId: string | null
+ familyType: FolderMentionLocation['familyType']
+ workspaceId: string
+}
+
+function folderFamilyType(
+ type: MothershipResourceType,
+ item: AvailableItem
+): FolderMentionLocation['familyType'] | null {
+ if (type === 'folder') {
+ if (item.mentionFamily === 'Table folders') return 'table'
+ if (item.mentionFamily === 'Knowledge base folders') return 'knowledgebase'
+ return 'workflow'
+ }
+ if (type === 'filefolder') return 'file'
+ return null
+}
+
+/** Builds display-only locations for the folder rows in the flat resource picker. */
+export function buildFolderMentionLocationMap(
+ groups: readonly ResourceMentionGroup[]
+): Map {
+ const locations = new Map()
+
+ for (const group of groups) {
+ if (group.type !== 'folder' && group.type !== 'filefolder') continue
+
+ const nodes = new Map()
+ for (const item of group.items) {
+ const familyType = folderFamilyType(group.type, item)
+ if (!familyType) continue
+ const workspaceId = typeof item.workspaceId === 'string' ? item.workspaceId : ''
+ nodes.set(`${familyType}:${workspaceId}:${item.id}`, {
+ id: item.id,
+ name: item.name,
+ parentId: typeof item.parentId === 'string' ? item.parentId : null,
+ familyType,
+ workspaceId,
+ })
+ }
+
+ for (const node of nodes.values()) {
+ const { familyType, workspaceId } = node
+ const parentNames = folderAncestorChain(node.parentId, (id) =>
+ nodes.get(`${familyType}:${workspaceId}:${id}`)
+ )
+ .filter((parent) => parent.id !== node.id)
+ .map((parent) => parent.name)
+ locations.set(`${group.type}:${workspaceId}:${node.id}`, { familyType, parentNames })
+ }
+ }
+
+ return locations
+}
+
/** A family query such as "browser" keeps that resource's live tabs visible. */
export function resourceMentionMatches(item: AvailableItem, query: string): boolean {
const normalized = query.toLowerCase().trim()
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx
index c0ce35be12d..0f77a5f6495 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx
@@ -6,6 +6,7 @@ import { OverflowText } from '@sim/emcn'
import { File, Workflow } from '@sim/emcn/icons'
import { Command } from 'cmdk'
import { IdentityTile } from '@/components/identity-tile/identity-tile'
+import { FolderPathLabel } from '@/components/ui'
import { getWorkspaceInitial } from '@/lib/workspaces/initials'
import type { CommandItemProps } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
import { COMMAND_ITEM_CLASSNAME } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
@@ -23,28 +24,6 @@ function ItemMeta({ meta }: ItemMetaProps) {
return {meta}
}
-interface ItemFolderPathProps {
- folderPath: string[]
-}
-
-/** Trailing folder-path receipt whose head segments yield space to the leaf. */
-function ItemFolderPath({ folderPath }: ItemFolderPathProps) {
- return (
-
- {folderPath.length > 1 && (
- <>
-
- /
- >
- )}
-
-
- )
-}
-
/** Structural equality for the optional folder-path prop in memo comparators. */
function sameFolderPath(prev?: string[], next?: string[]): boolean {
return (
@@ -171,7 +150,7 @@ export const MemoizedWorkflowItem = memo(
{meta ? (
) : folderPath && folderPath.length > 0 ? (
-
+
) : null}
)
@@ -208,7 +187,7 @@ export const MemoizedFileItem = memo(
{meta ? (
) : folderPath && folderPath.length > 0 ? (
-
+
) : null}
)
@@ -330,7 +309,7 @@ export const MemoizedIconItem = memo(
{meta ? (
) : folderPath && folderPath.length > 0 ? (
-
+
) : null}
)
diff --git a/apps/sim/components/ui/folder-path-label.test.ts b/apps/sim/components/ui/folder-path-label.test.ts
new file mode 100644
index 00000000000..e479e87a6df
--- /dev/null
+++ b/apps/sim/components/ui/folder-path-label.test.ts
@@ -0,0 +1,28 @@
+/**
+ * @vitest-environment node
+ */
+import { describe, expect, it } from 'vitest'
+import { collapseFolderPath } from '@/components/ui/folder-path-label'
+
+describe('collapseFolderPath', () => {
+ it('leaves a shallow chain untouched', () => {
+ expect(collapseFolderPath([])).toEqual([])
+ expect(collapseFolderPath(['Growth'])).toEqual(['Growth'])
+ expect(collapseFolderPath(['Growth', 'Campaigns', 'Q3'])).toEqual(['Growth', 'Campaigns', 'Q3'])
+ })
+
+ it('drops whole ancestors rather than clipping one mid-word', () => {
+ expect(collapseFolderPath(['Growth', 'Campaigns', 'Paid', 'Q3'])).toEqual(['Growth', '…', 'Q3'])
+ })
+
+ it('keeps the root and the leaf however deep the chain runs', () => {
+ const deep = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
+ expect(collapseFolderPath(deep)).toEqual(['A', '…', 'G'])
+ })
+
+ it('does not mutate the input', () => {
+ const segments = ['A', 'B', 'C', 'D']
+ collapseFolderPath(segments)
+ expect(segments).toEqual(['A', 'B', 'C', 'D'])
+ })
+})
diff --git a/apps/sim/components/ui/folder-path-label.tsx b/apps/sim/components/ui/folder-path-label.tsx
new file mode 100644
index 00000000000..445674f44cc
--- /dev/null
+++ b/apps/sim/components/ui/folder-path-label.tsx
@@ -0,0 +1,69 @@
+import { cn, OverflowText } from '@sim/emcn'
+
+/**
+ * Ancestors kept before the path collapses. Three is the widest chain that still
+ * reads at the ~30% of a menu row this label is allowed to occupy.
+ */
+const MAX_VISIBLE_SEGMENTS = 3
+const ELLIPSIS = '…'
+
+/**
+ * Collapses a root-first folder chain so an over-long path drops whole ancestors
+ * instead of clipping one mid-word: `Growth / … / Q3` rather than `Growth / Mark…`.
+ *
+ * The root orients and the leaf disambiguates, so those are the two that survive;
+ * everything between them is what the reader can least act on.
+ */
+export function collapseFolderPath(segments: readonly string[]): string[] {
+ if (segments.length <= MAX_VISIBLE_SEGMENTS) return [...segments]
+ return [segments[0], ELLIPSIS, segments[segments.length - 1]]
+}
+
+interface FolderPathLabelProps {
+ /** Root-first ancestor names of the row's resource. */
+ segments: readonly string[]
+ /**
+ * Pinned lead-in that never clips — the resource family (`Files`, `Workflows`)
+ * when the label doubles as the row's disambiguator.
+ */
+ prefix?: string
+ className?: string
+}
+
+/**
+ * Right-aligned location receipt for a menu row. Head segments yield their space
+ * first so the leaf — the segment that tells two same-named rows apart — is the
+ * last thing to clip.
+ */
+export function FolderPathLabel({ segments, prefix, className }: FolderPathLabelProps) {
+ const visible = collapseFolderPath(segments)
+ const leaf = visible.at(-1)
+ const head = visible.slice(0, -1)
+ const hasLeadIn = Boolean(prefix) || head.length > 0
+
+ if (!hasLeadIn && !leaf) return null
+
+ return (
+
+ {prefix && {prefix}}
+ {prefix && head.length > 0 && / }
+ {head.length > 0 && (
+
+ {head.join(' / ')}
+
+ )}
+ {leaf && (
+ <>
+ {hasLeadIn && / }
+
+ >
+ )}
+
+ )
+}
diff --git a/apps/sim/components/ui/index.ts b/apps/sim/components/ui/index.ts
index f343ed3b309..0610db18634 100644
--- a/apps/sim/components/ui/index.ts
+++ b/apps/sim/components/ui/index.ts
@@ -1,4 +1,5 @@
export { ActivityStatus } from '@/components/ui/activity-status'
+export { FolderPathLabel } from './folder-path-label'
export { GeneratedPasswordInput } from './generated-password-input'
export { Progress } from './progress'
export {