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..ef8c1a1ff35
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/mention-row-content.tsx
@@ -0,0 +1,40 @@
+'use client'
+
+import type { ReactNode } from 'react'
+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
+}
+
+/**
+ * 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 }: MentionRowContentProps) {
+ if (!location) return <>{children}>
+
+ return (
+ <>
+ {/* Capped rather than shrinkable so a long name cannot squeeze out the segment
+ that tells two same-named folders apart. */}
+
+ {children}
+
+
+ >
+ )
+}
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 a16be8f1c04..bd6fa5f247b 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
@@ -15,7 +15,9 @@ import {
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown'
import { getResourceConfig } 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,
resourceMentionMatches,
withDesktopTabMentions,
} from '@/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/resource-mention-items'
@@ -119,6 +121,11 @@ export const PlusMenuDropdown = React.memo(
return attachable.filter(({ type }) => !MENTION_ONLY_RESOURCE_TYPES.has(type))
}, [availableResources, browserTabs, isMention, terminalTabs])
+ const folderMentionLocations = useMemo(
+ () => buildFolderMentionLocationMap(visibleResources),
+ [visibleResources]
+ )
+
const treeSections = useResourceTreeSections({
groups: visibleResources,
structureFolders,
@@ -298,7 +305,9 @@ export const PlusMenuDropdown = React.memo(
// Plus-click shows short fixed labels (Workflows, Tables, …) — let it size
// to its content via the emcn DropdownMenuContent default max-w.
// Mention mode renders resource names directly, so widen for breathing room.
- isMention && 'max-w-[min(300px,calc(100vw-32px))]'
+ // Wide enough that a folder row fits its name and its right-aligned
+ // location column without either collapsing to a stub.
+ isMention && 'w-[min(380px,calc(100vw-32px))] max-w-[calc(100vw-32px)]'
)}
onCloseAutoFocus={handleCloseAutoFocus}
onOpenAutoFocus={handleOpenAutoFocus}
@@ -334,6 +343,7 @@ export const PlusMenuDropdown = React.memo(
filteredItems.map(({ type, item }, index) => {
const config = getResourceConfig(type)
const isActive = index === activeIndex
+ const location = folderMentionLocations.get(`${type}:${item.id}`)
return (
)
})
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/resource-mention-items.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/resource-mention-items.test.ts
index bed14025882..bf23bdf83be 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/resource-mention-items.test.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/resource-mention-items.test.ts
@@ -4,6 +4,7 @@ import {
TERMINAL_SESSION_RESOURCE_ID,
} from '@/lib/copilot/resources/types'
import {
+ buildFolderMentionLocationMap,
resourceMentionMatches,
withDesktopTabMentions,
} from '@/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/resource-mention-items'
@@ -20,6 +21,88 @@ const groups = [
},
]
+describe('buildFolderMentionLocationMap', () => {
+ it('distinguishes same-named top-level workflow and file folders by family', () => {
+ const locations = buildFolderMentionLocationMap([
+ {
+ type: 'folder',
+ items: [{ id: 'enterprise', name: 'Enterprise', parentId: null }],
+ },
+ {
+ type: 'filefolder',
+ items: [{ id: 'enterprise', name: 'Enterprise', parentId: null }],
+ },
+ ])
+
+ expect(locations.get('folder:enterprise')).toEqual({
+ familyType: 'workflow',
+ parentNames: [],
+ })
+ expect(locations.get('filefolder:enterprise')).toEqual({
+ familyType: 'file',
+ parentNames: [],
+ })
+ })
+
+ it('returns root-first parents without repeating the current folder name', () => {
+ const locations = buildFolderMentionLocationMap([
+ {
+ type: 'folder',
+ items: [
+ { id: 'engineering', name: 'Engineering', parentId: null },
+ { id: 'accounts', name: 'Accounts', parentId: 'engineering' },
+ { id: 'enterprise', name: 'Enterprise', parentId: 'accounts' },
+ ],
+ },
+ ])
+
+ expect(locations.get('folder:enterprise')).toEqual({
+ familyType: 'workflow',
+ parentNames: ['Engineering', 'Accounts'],
+ })
+ })
+
+ it('falls back to the family when a parent is missing', () => {
+ const locations = buildFolderMentionLocationMap([
+ {
+ type: 'filefolder',
+ items: [{ id: 'enterprise', name: 'Enterprise', parentId: 'missing' }],
+ },
+ ])
+
+ expect(locations.get('filefolder:enterprise')).toEqual({
+ familyType: 'file',
+ parentNames: [],
+ })
+ })
+
+ it('terminates cyclic ancestry without repeating the current folder', () => {
+ const locations = buildFolderMentionLocationMap([
+ {
+ type: 'folder',
+ items: [
+ { id: 'enterprise', name: 'Enterprise', parentId: 'accounts' },
+ { id: 'accounts', name: 'Accounts', parentId: 'enterprise' },
+ ],
+ },
+ ])
+
+ expect(locations.get('folder:enterprise')).toEqual({
+ familyType: 'workflow',
+ parentNames: ['Accounts'],
+ })
+ })
+
+ it('does not add locations for non-folder resources', () => {
+ const locations = buildFolderMentionLocationMap([
+ { type: 'workflow', items: [{ id: 'workflow-1', name: 'Enterprise' }] },
+ { type: 'file', items: [{ id: 'file-1', name: 'Enterprise' }] },
+ ])
+
+ expect(locations.size).toBe(0)
+ })
+})
+
describe('withDesktopTabMentions', () => {
it('keeps Browser and Terminal as flat resource mentions with no live tabs', () => {
const result = withDesktopTabMentions(groups, [], [])
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 bbbe84ad29e..a8faa8be477 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
@@ -4,6 +4,7 @@ import {
BROWSER_SESSION_RESOURCE_ID,
TERMINAL_SESSION_RESOURCE_ID,
} from '@/lib/copilot/resources/types'
+import { folderAncestorChain } from '@/lib/folders/tree'
import type { AvailableItem } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/resource-folder-tree'
import { browserTabTitle } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-tab-label'
import type { MothershipResourceType } from '@/app/workspace/[workspaceId]/home/types'
@@ -15,6 +16,57 @@ export interface ResourceMentionGroup {
export type ResourceMentionLevel = 'resource' | 'tab'
+export interface FolderMentionLocation {
+ familyType: 'workflow' | 'file'
+ parentNames: string[]
+}
+
+interface FolderMentionNode {
+ id: string
+ name: string
+ parentId: string | null
+}
+
+function folderFamilyType(
+ type: MothershipResourceType
+): FolderMentionLocation['familyType'] | null {
+ if (type === 'folder') 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) {
+ const familyType = folderFamilyType(group.type)
+ if (!familyType) continue
+
+ const nodes = new Map(
+ group.items.map((item) => [
+ item.id,
+ {
+ id: item.id,
+ name: item.name,
+ parentId: typeof item.parentId === 'string' ? item.parentId : null,
+ },
+ ])
+ )
+
+ for (const node of nodes.values()) {
+ const parentNames = folderAncestorChain(node.parentId, (id) => nodes.get(id))
+ .filter((parent) => parent.id !== node.id)
+ .map((parent) => parent.name)
+ locations.set(`${group.type}:${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 c67b2089917..02c3a3accdc 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
@@ -4,6 +4,7 @@ import type { ComponentType } from 'react'
import { memo } from 'react'
import { File, Workflow } from '@sim/emcn/icons'
import { Command } from 'cmdk'
+import { FolderPathLabel } from '@/components/ui'
import { HEX_COLOR_REGEX } from '@/lib/branding'
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,27 +24,6 @@ function ItemMeta({ meta }: ItemMetaProps) {
)
}
-interface ItemFolderPathProps {
- folderPath: string[]
-}
-
-/** Trailing folder-path receipt whose head segments yield space to the leaf. */
-function ItemFolderPath({ folderPath }: ItemFolderPathProps) {
- return (
-
- {folderPath.length > 1 && (
- <>
-
- {folderPath.slice(0, -1).join(' / ')}
-
- /
- >
- )}
- {folderPath[folderPath.length - 1]}
-
- )
-}
-
/** Structural equality for the optional folder-path prop in memo comparators. */
function sameFolderPath(prev?: string[], next?: string[]): boolean {
return (
@@ -167,7 +147,7 @@ export const MemoizedWorkflowItem = memo(
{meta ? (
) : folderPath && folderPath.length > 0 ? (
-
+
) : null}
)
@@ -204,7 +184,7 @@ export const MemoizedFileItem = memo(
{meta ? (
) : folderPath && folderPath.length > 0 ? (
-
+
) : null}
)
@@ -349,7 +329,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..3bf9bca267d
--- /dev/null
+++ b/apps/sim/components/ui/folder-path-label.tsx
@@ -0,0 +1,64 @@
+import { cn } 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]]
+}
+
+export 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}}
+ {head.length > 0 && (
+
+ {prefix ? ` / ${head.join(' / ')}` : head.join(' / ')}
+
+ )}
+ {leaf && (
+ <>
+ {hasLeadIn && / }
+ {leaf}
+ >
+ )}
+
+ )
+}
diff --git a/apps/sim/components/ui/index.ts b/apps/sim/components/ui/index.ts
index 234f6f50a60..fc7e4cb55b1 100644
--- a/apps/sim/components/ui/index.ts
+++ b/apps/sim/components/ui/index.ts
@@ -1,4 +1,9 @@
export { Button, buttonVariants } from './button'
+export {
+ collapseFolderPath,
+ FolderPathLabel,
+ type FolderPathLabelProps,
+} from './folder-path-label'
export { GeneratedPasswordInput } from './generated-password-input'
export { Progress } from './progress'
export {