Skip to content

Commit 68f5857

Browse files
committed
local filesystem fixes
1 parent a8da276 commit 68f5857

2 files changed

Lines changed: 86 additions & 1 deletion

File tree

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export const CREDENTIAL_TAG_TYPES = [
6666
'credential_id',
6767
'link',
6868
'secret_input',
69+
'folder_access',
6970
] as const
7071

7172
export type CredentialTagType = (typeof CREDENTIAL_TAG_TYPES)[number]
@@ -195,6 +196,11 @@ function isCredentialTagData(value: unknown): value is CredentialTagData {
195196
}
196197
return typeof value.name === 'string' && value.name.trim().length > 0
197198
}
199+
// folder_access is a value-less action chip (optional `name` hints at the
200+
// folder the user is being asked to grant, e.g. "Desktop").
201+
if (value.type === 'folder_access') {
202+
return value.name === undefined || typeof value.name === 'string'
203+
}
198204
if (value.redacted === true) return value.value === undefined || typeof value.value === 'string'
199205
return typeof value.value === 'string'
200206
}
@@ -734,13 +740,91 @@ function SecretInputDisplay({ data }: { data: CredentialTagData }) {
734740
)
735741
}
736742

743+
/**
744+
* Folder icon for the local-folder grant chip (matches the credential chip
745+
* icon sizing).
746+
*/
747+
const FolderGrantIcon = ({ className }: { className?: string }) => (
748+
<svg className={className} viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
749+
<path
750+
d='M1.5 4.5A1.5 1.5 0 0 1 3 3h3.2l1.6 1.8H13A1.5 1.5 0 0 1 14.5 6.3v5.2A1.5 1.5 0 0 1 13 13H3a1.5 1.5 0 0 1-1.5-1.5v-7z'
751+
stroke='currentColor'
752+
strokeWidth='1.2'
753+
strokeLinejoin='round'
754+
/>
755+
</svg>
756+
)
757+
758+
/**
759+
* Inline grant chip rendered for
760+
* `<credential>{"type":"folder_access","name":"Desktop"}</credential>`.
761+
* Clicking opens the desktop app's native folder picker (read-only grant,
762+
* same flow as the local_mount_directory tool). Renders nothing outside the
763+
* desktop app — there is no local filesystem bridge to grant against.
764+
*/
765+
function FolderAccessDisplay({ data }: { data: CredentialTagData }) {
766+
const [picking, setPicking] = useState(false)
767+
const [grantedName, setGrantedName] = useState<string | null>(null)
768+
769+
const bridge = typeof window !== 'undefined' ? window.simDesktop : undefined
770+
if (!bridge?.localFilesystem) return null
771+
772+
const hint = (data.name ?? '').trim()
773+
const label = grantedName
774+
? `Access granted — ${grantedName}`
775+
: hint
776+
? `Grant access to ${hint}`
777+
: 'Grant access to a local folder'
778+
779+
const handleClick = async () => {
780+
if (picking || grantedName) return
781+
setPicking(true)
782+
try {
783+
const response = await bridge.localFilesystem({ operation: 'mount_directory' })
784+
if (response.ok && 'mount' in response.data && response.data.mount) {
785+
setGrantedName(response.data.mount.name)
786+
toast.success(`Granted access to ${response.data.mount.name}`)
787+
}
788+
} catch {
789+
toast.error("Couldn't open the folder picker. Please try again.")
790+
} finally {
791+
setPicking(false)
792+
}
793+
}
794+
795+
return (
796+
<button
797+
type='button'
798+
onClick={() => void handleClick()}
799+
disabled={picking || grantedName !== null}
800+
className={cn(
801+
'flex w-full items-center gap-2 rounded-lg border border-[var(--divider)] px-3 py-2.5 text-left transition-colors',
802+
grantedName === null && !picking && 'hover-hover:bg-[var(--surface-5)]',
803+
picking && 'opacity-60'
804+
)}
805+
>
806+
<FolderGrantIcon className='size-[16px] shrink-0' />
807+
<span className='flex-1 text-[var(--text-body)] text-sm'>
808+
{picking ? 'Choose a folder…' : label}
809+
</span>
810+
{grantedName === null && (
811+
<ArrowRight className='size-[16px] shrink-0 text-[var(--text-icon)]' />
812+
)}
813+
</button>
814+
)
815+
}
816+
737817
function CredentialDisplay({ data }: { data: CredentialTagData }) {
738818
const { canEdit } = useUserPermissionsContext()
739819

740820
if (data.type === 'secret_input') {
741821
return <SecretInputDisplay data={data} />
742822
}
743823

824+
if (data.type === 'folder_access') {
825+
return <FolderAccessDisplay data={data} />
826+
}
827+
744828
if (data.type === 'link') {
745829
// Connecting a credential mutates the workspace — hide it from read-only members.
746830
if (!data.provider || !canEdit) return null

apps/sim/lib/copilot/tools/local-filesystem.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ export function buildLocalFilesystemToolSchemas() {
4444
{
4545
name: LOCAL_FILESYSTEM_TOOL_NAMES.mountDirectory,
4646
description:
47-
'Open the native folder picker so the user can grant the desktop app read-only access to one local directory. The grant is encrypted with the operating-system keychain and remembered across app restarts when secure storage is available. Returns an opaque localfs:// root URI; it never returns the absolute host path. Call this only when no suitable remembered mount exists.',
47+
'Open the native folder picker so the user can grant the desktop app read-only access to one local directory. The grant is encrypted with the operating-system keychain and remembered across app restarts when secure storage is available. Returns an opaque localfs:// root URI; it never returns the absolute host path. ' +
48+
'PREFER THE INLINE GRANT CHIP over calling this tool: when the user asks about their local files (their desktop, downloads, a folder on their machine) and local_list_mounts shows no suitable mount, respond with ONE short sentence plus `<credential>{"type":"folder_access","name":"Desktop"}</credential>` (set "name" to the folder they mentioned, or omit it) — this renders a grant button the user clicks, exactly like a credential connect chip. Never reply that you lack access to local files, and never make the user ask twice: check mounts, then emit the chip. Call this tool directly only when the user has just explicitly asked you to open the picker.',
4849
input_schema: objectSchema({}),
4950
executeLocally: true,
5051
},

0 commit comments

Comments
 (0)