fix: split sandbox files into saved and temporary tabs (#40478)

This commit is contained in:
Joel
2026-08-11 14:46:01 +08:00
committed by GitHub
parent 7162450e83
commit 44c5f1c011
28 changed files with 266 additions and 215 deletions
@@ -19,29 +19,16 @@ describe('AgentWorkingDirectoryBreadcrumb', () => {
expect(screen.getByRole('button', { name: '.' })).toHaveAttribute('aria-current', 'page')
})
it('should render home as the current path when path is home', () => {
it('should render the saved-files root path', () => {
render(<AgentWorkingDirectoryBreadcrumb path="~" onPathChange={vi.fn()} />)
expect(
screen.getByRole('button', {
name: 'agentV2.agentDetail.configure.workingDirectory.home',
}),
).toHaveAttribute('aria-current', 'page')
expect(
screen.queryByRole('button', {
name: 'agentV2.agentDetail.configure.workingDirectory.workingDirectory',
}),
).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: '~' })).toHaveAttribute('aria-current', 'page')
})
it('should render the workspace cwd directly and show tilde as home', () => {
it('should render the saved-files prefix before its path segments', () => {
render(<AgentWorkingDirectoryBreadcrumb path="~/web-game" onPathChange={vi.fn()} />)
expect(
screen.getByRole('button', {
name: 'agentV2.agentDetail.configure.workingDirectory.home',
}),
).toBeInTheDocument()
expect(screen.getByRole('button', { name: '~' })).toBeInTheDocument()
expect(
screen.getByRole('button', {
name: 'web-game',
@@ -52,17 +39,8 @@ describe('AgentWorkingDirectoryBreadcrumb', () => {
it('should collapse middle breadcrumb layers when path is deeper than three layers', () => {
render(<AgentWorkingDirectoryBreadcrumb path="~/web-game/src/app" onPathChange={vi.fn()} />)
expect(
screen.getByRole('button', {
name: 'agentV2.agentDetail.configure.workingDirectory.home',
}),
).toBeInTheDocument()
expect(screen.getByRole('button', { name: '~' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: '...' })).toBeInTheDocument()
expect(
screen.queryByRole('button', {
name: 'agentV2.agentDetail.configure.workingDirectory.workingDirectory',
}),
).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'web-game' })).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'src' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'app' })).toHaveAttribute('aria-current', 'page')
@@ -70,16 +48,12 @@ describe('AgentWorkingDirectoryBreadcrumb', () => {
})
describe('User Interactions', () => {
it('should request home path when home is clicked', async () => {
it('should request the saved-files root when its prefix is clicked', async () => {
const user = userEvent.setup()
const handlePathChange = vi.fn()
render(<AgentWorkingDirectoryBreadcrumb path="~/web-game" onPathChange={handlePathChange} />)
await user.click(
screen.getByRole('button', {
name: 'agentV2.agentDetail.configure.workingDirectory.home',
}),
)
await user.click(screen.getByRole('button', { name: '~' }))
expect(handlePathChange).toHaveBeenCalledWith('~')
})
@@ -88,12 +62,12 @@ describe('AgentWorkingDirectoryBreadcrumb', () => {
const user = userEvent.setup()
const handlePathChange = vi.fn()
render(
<AgentWorkingDirectoryBreadcrumb path="~/web-game/src" onPathChange={handlePathChange} />,
<AgentWorkingDirectoryBreadcrumb path="./web-game/src" onPathChange={handlePathChange} />,
)
await user.click(screen.getByRole('button', { name: 'web-game' }))
expect(handlePathChange).toHaveBeenCalledWith('~/web-game')
expect(handlePathChange).toHaveBeenCalledWith('./web-game')
})
it('should request a hidden breadcrumb path from the ellipsis menu', async () => {
@@ -59,8 +59,8 @@ const previewSourceCases = [
...agentSource,
callerId: 'conversation-2',
} satisfies AgentWorkingDirectorySource,
imagePaths: ['workspace/chart-a.png', 'workspace/chart-b.png'],
nonImagePath: 'workspace/model.bin',
imagePaths: ['chart-a.png', 'chart-b.png'],
nonImagePath: 'model.bin',
previewClient: mocks.sandboxFileDownloadClientPost,
urls: [
'https://example.com/agent-chart-a.png',
@@ -249,7 +249,7 @@ function mockFileListEntries(
queryOptions.mockImplementation(({ input }: QueryOptionsInput) => ({
queryKey: [`${source.type}-sandbox-files`, input, entries],
queryFn: async () => ({
path: input.query?.path ?? (source.type === 'agent' ? '~/workspace' : '.'),
path: input.query?.path ?? '~',
entries,
}),
}))
@@ -312,12 +312,12 @@ describe('AgentWorkingDirectoryPanel', () => {
mocks.sandboxFilesQueryOptions.mockImplementation(({ input }: QueryOptionsInput) => ({
queryKey: ['sandbox-files', input],
queryFn: async () => ({
path: input.query?.path ?? '~/workspace',
path: input.query?.path ?? '~',
entries: [
{ name: 'workspace/report.md', type: 'file' },
{ name: 'workspace/notes.md', type: 'file' },
{ name: 'workspace/chart.png', type: 'file' },
{ name: 'workspace/model.bin', type: 'file' },
{ name: 'report.md', type: 'file' },
{ name: 'notes.md', type: 'file' },
{ name: 'chart.png', type: 'file' },
{ name: 'model.bin', type: 'file' },
],
}),
}))
@@ -350,6 +350,78 @@ describe('AgentWorkingDirectoryPanel', () => {
)
})
it('should separate saved and temporary files by their sandbox path roots', async () => {
const user = userEvent.setup()
mocks.sandboxFilesQueryOptions.mockImplementation(({ input }: QueryOptionsInput) => ({
queryKey: ['sandbox-files-by-root', input],
queryFn: async () => ({
path: input.query?.path ?? '~',
entries:
input.query?.path === '.'
? [{ name: 'scratch.txt', type: 'file' }]
: [{ name: 'saved.txt', type: 'file' }],
}),
}))
renderWorkingDirectoryPanel()
const savedFilesTab = await screen.findByRole('tab', {
name: 'agentV2.agentDetail.configure.workingDirectory.savedFiles',
})
const temporaryFilesTab = screen.getByRole('tab', {
name: 'agentV2.agentDetail.configure.workingDirectory.temporaryFiles',
})
expect(savedFilesTab).toHaveAttribute('aria-selected', 'true')
expect(
await screen.findByRole('tabpanel', {
name: 'agentV2.agentDetail.configure.workingDirectory.savedFiles',
}),
).toBeInTheDocument()
await waitFor(() => {
expect(mocks.sandboxFilesQueryOptions).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
query: expect.objectContaining({ path: '~' }),
}),
}),
)
})
await user.click(temporaryFilesTab)
await waitFor(() => {
expect(
screen.getByRole('tab', {
name: 'agentV2.agentDetail.configure.workingDirectory.temporaryFiles',
}),
).toHaveAttribute('aria-selected', 'true')
})
expect(
screen.getByRole('tabpanel', {
name: 'agentV2.agentDetail.configure.workingDirectory.temporaryFiles',
}),
).toBeInTheDocument()
await waitFor(() => {
expect(mocks.sandboxFilesQueryOptions).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
query: expect.objectContaining({ path: '.' }),
}),
}),
)
})
await waitFor(() => {
expect(mocks.sandboxFileReadQueryOptions).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
query: expect.objectContaining({ path: './scratch.txt' }),
}),
}),
)
})
expect(mocks.sandboxInfoQueryOptions).not.toHaveBeenCalled()
})
it('should download the selected working directory file from the preview header download action', async () => {
const user = userEvent.setup()
const download = createDeferred<{ url: string }>()
@@ -381,7 +453,7 @@ describe('AgentWorkingDirectoryPanel', () => {
body: {
caller_type: 'conversation',
caller_id: 'conversation-1',
path: '~/workspace/notes.md',
path: '~/notes.md',
},
})
expect(mocks.downloadUrl).toHaveBeenCalledWith({
@@ -425,7 +497,7 @@ describe('AgentWorkingDirectoryPanel', () => {
body: {
caller_type: 'conversation',
caller_id: 'conversation-1',
path: '~/workspace/model.bin',
path: '~/model.bin',
},
})
expect(mocks.downloadUrl).toHaveBeenCalledWith({
@@ -458,7 +530,7 @@ describe('AgentWorkingDirectoryPanel', () => {
body: {
caller_type: 'conversation',
caller_id: 'conversation-1',
path: '~/workspace/chart.png',
path: '~/chart.png',
},
})
expect(
@@ -9,13 +9,14 @@ import {
} from '@langgenius/dify-ui/dropdown-menu'
import { useTranslation } from 'react-i18next'
const AGENT_WORKING_DIRECTORY_HOME_PATH = '~'
const AGENT_WORKING_DIRECTORY_ROOT_PATH = '.'
export const AGENT_SAVED_FILES_ROOT_PATH = '~'
export const AGENT_TEMPORARY_FILES_ROOT_PATH = '.'
export type AgentWorkingDirectoryPath =
| typeof AGENT_WORKING_DIRECTORY_HOME_PATH
| typeof AGENT_WORKING_DIRECTORY_ROOT_PATH
| string
export type AgentWorkingDirectoryRootPath =
| typeof AGENT_SAVED_FILES_ROOT_PATH
| typeof AGENT_TEMPORARY_FILES_ROOT_PATH
export type AgentWorkingDirectoryPath = AgentWorkingDirectoryRootPath | string
type AgentWorkingDirectoryBreadcrumbItemData = {
iconClassName: string
@@ -24,76 +25,48 @@ type AgentWorkingDirectoryBreadcrumbItemData = {
}
const normalizeWorkingDirectoryPath = (path: AgentWorkingDirectoryPath) => {
if (path === AGENT_WORKING_DIRECTORY_ROOT_PATH || path === AGENT_WORKING_DIRECTORY_HOME_PATH)
return path
if (path === AGENT_TEMPORARY_FILES_ROOT_PATH || path === AGENT_SAVED_FILES_ROOT_PATH) return path
if (path.startsWith('~/'))
return `${AGENT_WORKING_DIRECTORY_HOME_PATH}/${path.slice(2).replace(/^\/+|\/+$/g, '')}`
return `${AGENT_SAVED_FILES_ROOT_PATH}/${path.slice(2).replace(/^\/+|\/+$/g, '')}`
return path.replace(/^\.\/+/, '').replace(/^\/+|\/+$/g, '')
}
function buildPathFromSegments(segments: string[], options: { startsFromHome: boolean }) {
if (options.startsFromHome)
return segments.length
? `${AGENT_WORKING_DIRECTORY_HOME_PATH}/${segments.join('/')}`
: AGENT_WORKING_DIRECTORY_HOME_PATH
return segments.length ? segments.join('/') : AGENT_WORKING_DIRECTORY_ROOT_PATH
function buildPathFromSegments(rootPath: AgentWorkingDirectoryRootPath, segments: string[]) {
return segments.length ? `${rootPath}/${segments.join('/')}` : rootPath
}
function getBreadcrumbItems({
homeLabel,
path,
}: {
homeLabel: string
path: AgentWorkingDirectoryPath
}): AgentWorkingDirectoryBreadcrumbItemData[] {
function getBreadcrumbItems(
path: AgentWorkingDirectoryPath,
): AgentWorkingDirectoryBreadcrumbItemData[] {
const normalizedPath = normalizeWorkingDirectoryPath(path)
const normalizedHomeLabel = homeLabel === 'home' ? 'Home' : homeLabel
if (normalizedPath === AGENT_WORKING_DIRECTORY_HOME_PATH) {
return [
{
iconClassName: 'i-ri-folder-3-line',
label: normalizedHomeLabel,
path: AGENT_WORKING_DIRECTORY_HOME_PATH,
},
]
}
if (normalizedPath === AGENT_WORKING_DIRECTORY_ROOT_PATH) {
return [
{
iconClassName: 'i-ri-folder-3-line',
label: AGENT_WORKING_DIRECTORY_ROOT_PATH,
path: AGENT_WORKING_DIRECTORY_ROOT_PATH,
},
]
}
const startsFromHome = normalizedPath.startsWith(`${AGENT_WORKING_DIRECTORY_HOME_PATH}/`)
const segments = startsFromHome
? normalizedPath.slice(2).split('/').filter(Boolean)
: normalizedPath.split('/').filter(Boolean)
const rootPath =
normalizedPath === AGENT_SAVED_FILES_ROOT_PATH ||
normalizedPath.startsWith(`${AGENT_SAVED_FILES_ROOT_PATH}/`)
? AGENT_SAVED_FILES_ROOT_PATH
: AGENT_TEMPORARY_FILES_ROOT_PATH
const segments =
rootPath === AGENT_SAVED_FILES_ROOT_PATH
? normalizedPath.slice(2).split('/').filter(Boolean)
: normalizedPath
.replace(/^\.\/?/, '')
.split('/')
.filter(Boolean)
const rootItem: AgentWorkingDirectoryBreadcrumbItemData = {
iconClassName: 'i-ri-folder-3-line',
label: startsFromHome ? normalizedHomeLabel : segments[0]!,
path: buildPathFromSegments(startsFromHome ? [] : segments.slice(0, 1), { startsFromHome }),
label: rootPath,
path: rootPath,
}
return [
rootItem,
...segments.slice(startsFromHome ? 0 : 1).map((segment, index) => {
const pathSegments = startsFromHome
? segments.slice(0, index + 1)
: segments.slice(0, index + 2)
...segments.map((segment, index) => {
return {
iconClassName: 'i-ri-folder-3-line',
label: segment,
path: buildPathFromSegments(pathSegments, { startsFromHome }),
path: buildPathFromSegments(rootPath, segments.slice(0, index + 1)),
}
}),
]
@@ -148,10 +121,7 @@ export function AgentWorkingDirectoryBreadcrumb({
onPathChange: (path: AgentWorkingDirectoryPath) => void
}) {
const { t } = useTranslation('agentV2')
const items = getBreadcrumbItems({
homeLabel: t(($) => $['agentDetail.configure.workingDirectory.home']),
path,
})
const items = getBreadcrumbItems(path)
const { hiddenItems, visibleItems } = getVisibleBreadcrumbItems(items)
const renderSeparator = (key: string) => (
@@ -6,11 +6,14 @@ import type {
SandboxReadResponse,
} from '@dify/contracts/api/console/agent/types.gen'
import type { AgentSkillDetailDownloadAction } from '../orchestrate/skills/detail-dialog'
import type { AgentWorkingDirectoryPath } from './working-directory-breadcrumb'
import type {
AgentWorkingDirectoryPath,
AgentWorkingDirectoryRootPath,
} from './working-directory-breadcrumb'
import type { AgentFileNode } from '@/features/agent-v2/agent-composer/form-state'
import { Dialog } from '@langgenius/dify-ui/dialog'
import { Tabs, TabsIndicator, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs'
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { skipToken, useMutation, useQueries, useQuery } from '@tanstack/react-query'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -18,7 +21,11 @@ import { consoleClient, consoleQuery } from '@/service/client'
import { downloadUrl } from '@/utils/download'
import { getFileIconType } from '../orchestrate/files/file-icon'
import { AgentSkillDetailDialog } from '../orchestrate/skills/detail-dialog'
import { AgentWorkingDirectoryBreadcrumb } from './working-directory-breadcrumb'
import {
AGENT_SAVED_FILES_ROOT_PATH,
AGENT_TEMPORARY_FILES_ROOT_PATH,
AgentWorkingDirectoryBreadcrumb,
} from './working-directory-breadcrumb'
type AgentWorkingDirectoryPanelProps = {
source: AgentWorkingDirectorySource
@@ -45,31 +52,37 @@ type SandboxErrorPayload = {
code?: string
}
const normalizeSandboxPath = (path: string) => {
const normalizedPath = path
const getSandboxRootPath = (path: string): AgentWorkingDirectoryRootPath =>
path === AGENT_SAVED_FILES_ROOT_PATH || path.startsWith(`${AGENT_SAVED_FILES_ROOT_PATH}/`)
? AGENT_SAVED_FILES_ROOT_PATH
: AGENT_TEMPORARY_FILES_ROOT_PATH
const getSandboxRelativePath = (path: string) => {
const relativePath = path
.replace(/^~(?:\/|$)/, '')
.replace(/^\.\//, '')
.replace(/^\/+|\/+$/g, '')
return normalizedPath === '.' ? '' : normalizedPath
return relativePath === AGENT_TEMPORARY_FILES_ROOT_PATH ? '' : relativePath
}
const toSandboxHomePath = (path: string) => {
if (path === '.') return '.'
const toSandboxApiPath = (path: string) => {
const rootPath = getSandboxRootPath(path)
const relativePath = getSandboxRelativePath(path)
const normalizedPath = normalizeSandboxPath(path)
return normalizedPath ? `~/${normalizedPath}` : '~'
return relativePath ? `${rootPath}/${relativePath}` : rootPath
}
const toSandboxApiPath = toSandboxHomePath
const joinSandboxPath = (basePath: string, name: string) => {
const normalizedBasePath = normalizeSandboxPath(basePath)
return normalizedBasePath ? `${normalizedBasePath}/${name}` : name
const rootPath = getSandboxRootPath(basePath)
const baseRelativePath = getSandboxRelativePath(basePath)
const relativePath = [baseRelativePath, name].filter(Boolean).join('/')
return relativePath ? `${rootPath}/${relativePath}` : rootPath
}
function getSandboxEntryRelativePathSegments(entryName: string, basePath: string) {
const normalizedBasePath = normalizeSandboxPath(basePath)
const normalizedEntryName = normalizeSandboxPath(entryName)
const normalizedBasePath = getSandboxRelativePath(basePath)
const normalizedEntryName = getSandboxRelativePath(entryName)
if (!normalizedEntryName) return []
@@ -91,14 +104,14 @@ function buildSandboxFileTree(
basePath = '.',
options: { nestRootPath?: string; nestUnderBasePath?: boolean } = {},
): AgentFileNode[] {
const normalizedBasePath = normalizeSandboxPath(basePath)
const normalizedNestRootPath = normalizeSandboxPath(options.nestRootPath ?? '.')
const normalizedBasePath = getSandboxRelativePath(basePath)
const normalizedNestRootPath = getSandboxRelativePath(options.nestRootPath ?? '.')
const rootFiles: AgentFileNode[] = []
let baseFolder: AgentFileNode | undefined
if (options.nestUnderBasePath && normalizedBasePath) {
let currentFiles = rootFiles
let currentPath = normalizedNestRootPath
let currentPath = toSandboxApiPath(options.nestRootPath ?? basePath)
const basePathSegments = normalizedBasePath.split('/').filter(Boolean)
const nestRootPathSegments = normalizedNestRootPath.split('/').filter(Boolean)
const nestedBasePathSegments =
@@ -128,7 +141,7 @@ function buildSandboxFileTree(
if (!pathSegments.length) continue
let currentFiles = baseFolder?.children ?? rootFiles
let currentPath = normalizedBasePath
let currentPath = toSandboxApiPath(basePath)
pathSegments.forEach((segment, index) => {
const isLeaf = index === pathSegments.length - 1
@@ -225,17 +238,6 @@ async function isNoActiveBindingError(error: unknown) {
const isNotFoundResponse = (error: unknown) => error instanceof Response && error.status === 404
function isSandboxPathWithinDirectory(path: string, directory: string) {
const normalizedPath = normalizeSandboxPath(path)
const normalizedDirectory = normalizeSandboxPath(directory)
if (!normalizedDirectory) return true
return (
normalizedPath === normalizedDirectory || normalizedPath.startsWith(`${normalizedDirectory}/`)
)
}
export function AgentWorkingDirectoryPanel({
source,
onOpenChange,
@@ -243,55 +245,30 @@ export function AgentWorkingDirectoryPanel({
}: AgentWorkingDirectoryPanelProps) {
const { t } = useTranslation('agentV2')
const { t: tCommon } = useTranslation('common')
const [selectedDirectoryPath, setSelectedDirectoryPath] = useState<AgentWorkingDirectoryPath>()
const [selectedDirectoryPath, setSelectedDirectoryPath] = useState<AgentWorkingDirectoryPath>(
AGENT_SAVED_FILES_ROOT_PATH,
)
const [selectedFileId, setSelectedFileId] = useState<string>()
const [loadedFolderPaths, setLoadedFolderPaths] = useState<string[]>([])
const [openFolderPaths, setOpenFolderPaths] = useState<string[]>([])
const [pendingOpenFolderPaths, setPendingOpenFolderPaths] = useState<string[]>([])
const [downloadActionLoadingTarget, setDownloadActionLoadingTarget] =
useState<AgentSkillDetailDownloadAction | null>(null)
const sandboxInfoQueryOptions = consoleQuery.agent.byAgentId.sandbox.get.queryOptions({
input:
source.type === 'agent'
? {
const directoryPath = selectedDirectoryPath
const selectedRootPath = getSandboxRootPath(directoryPath)
const getFileListQueryOptions = (path: string) =>
source.type === 'agent'
? consoleQuery.agent.byAgentId.sandbox.files.get.queryOptions({
input: {
params: {
agent_id: source.agentId,
},
query: {
caller_type: source.callerType,
caller_id: source.callerId,
path: toSandboxApiPath(path),
},
}
: skipToken,
context: {
silent: true,
},
})
const sandboxInfoQuery = useQuery({
...sandboxInfoQueryOptions,
enabled: open && source.type === 'agent',
retry: false,
})
const isSandboxInfoLoading = source.type === 'agent' && sandboxInfoQuery.isPending
const workspaceDirectoryPath = sandboxInfoQuery.data?.workspace_cwd
const directoryPath = selectedDirectoryPath ?? workspaceDirectoryPath ?? '.'
const showReturnToWorkspaceButton =
!!workspaceDirectoryPath && !isSandboxPathWithinDirectory(directoryPath, workspaceDirectoryPath)
const getFileListQueryOptions = (path: string) =>
source.type === 'agent'
? consoleQuery.agent.byAgentId.sandbox.files.get.queryOptions({
input: !isSandboxInfoLoading
? {
params: {
agent_id: source.agentId,
},
query: {
caller_type: source.callerType,
caller_id: source.callerId,
path: toSandboxApiPath(path),
},
}
: skipToken,
},
context: {
silent: true,
},
@@ -368,18 +345,18 @@ export function AgentWorkingDirectoryPanel({
(files, query, index) => {
return mergeSandboxFileTree(
files,
buildSandboxFileTree(query.data?.entries, loadedFolderPaths[index] ?? query.data?.path, {
buildSandboxFileTree(query.data?.entries, loadedFolderPaths[index], {
nestRootPath: directoryPath,
nestUnderBasePath: true,
}),
)
},
buildSandboxFileTree(fileListQuery.data?.entries, fileListQuery.data?.path),
buildSandboxFileTree(fileListQuery.data?.entries, directoryPath),
)
const selectedWorkingDirectoryFile =
findReadableFile(workingDirectoryFiles, selectedFileId) ??
findFirstReadableFile(workingDirectoryFiles)
const isFileListLoading = isSandboxInfoLoading || fileListQuery.isPending
const isFileListLoading = fileListQuery.isPending
const loadingFolderPaths = new Set(
loadedFolderPaths.filter((path, index) => expandedFolderQueries[index]?.isPending),
)
@@ -569,46 +546,56 @@ export function AgentWorkingDirectoryPanel({
detail={{
description: t(($) => $['agentDetail.configure.workingDirectory.description']),
fileCount: countReadableFiles(workingDirectoryFiles),
fileListHeader: isSandboxInfoLoading ? (
<h3
id="agent-skill-detail-files-heading"
className="px-4 pt-3.5 pb-3 system-xl-semibold text-text-primary"
>
{t(($) => $['agentDetail.configure.workingDirectory.fileSystem'])}
</h3>
) : (
fileListHeader: (
<div className="flex shrink-0 flex-col">
<div className="flex items-center gap-1 px-4 pt-3.5 pb-3">
<h3
id="agent-skill-detail-files-heading"
className="min-w-0 flex-1 system-xl-semibold text-text-primary"
>
{t(($) => $['agentDetail.configure.workingDirectory.fileSystem'])}
</h3>
{showReturnToWorkspaceButton && (
<Tooltip>
<TooltipTrigger
aria-label={t(
($) => $['agentDetail.configure.workingDirectory.returnToWorkspace'],
)}
className="flex size-6 shrink-0 items-center justify-center rounded-md p-1 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
onClick={() => handleDirectoryPathChange(workspaceDirectoryPath)}
>
<span aria-hidden className="i-ri-arrow-go-back-line size-3.5" />
</TooltipTrigger>
<TooltipContent placement="top">
{t(($) => $['agentDetail.configure.workingDirectory.returnToWorkspace'])}
</TooltipContent>
</Tooltip>
)}
</div>
<AgentWorkingDirectoryBreadcrumb
path={directoryPath}
onPathChange={handleDirectoryPathChange}
/>
<h3
id="agent-skill-detail-files-heading"
className="px-4 pt-3.5 pb-2 system-xl-semibold text-text-primary"
>
{t(($) => $['agentDetail.configure.workingDirectory.fileSystem'])}
</h3>
<Tabs
value={selectedRootPath}
onValueChange={(path) =>
handleDirectoryPathChange(path as AgentWorkingDirectoryPath)
}
>
<TabsList className="relative h-9 gap-4 border-b-[0.5px] border-divider-regular px-4">
<TabsTab
value={AGENT_SAVED_FILES_ROOT_PATH}
className="h-full min-w-0 pt-0 pb-0 system-sm-semibold data-active:border-transparent"
>
{t(($) => $['agentDetail.configure.workingDirectory.savedFiles'])}
</TabsTab>
<TabsTab
value={AGENT_TEMPORARY_FILES_ROOT_PATH}
className="h-full min-w-0 pt-0 pb-0 system-sm-semibold data-active:border-transparent"
>
{t(($) => $['agentDetail.configure.workingDirectory.temporaryFiles'])}
</TabsTab>
<TabsIndicator
className="pointer-events-none absolute bottom-0 left-0 h-0 border-b-2 border-components-tab-active transition-[translate,width] duration-150 ease-in-out motion-reduce:transition-none"
style={{
translate: 'var(--active-tab-left)',
width: 'var(--active-tab-width)',
}}
/>
</TabsList>
<TabsPanel value={AGENT_SAVED_FILES_ROOT_PATH} tabIndex={-1}>
<AgentWorkingDirectoryBreadcrumb
path={directoryPath}
onPathChange={handleDirectoryPathChange}
/>
</TabsPanel>
<TabsPanel value={AGENT_TEMPORARY_FILES_ROOT_PATH} tabIndex={-1}>
<AgentWorkingDirectoryBreadcrumb
path={directoryPath}
onPathChange={handleDirectoryPathChange}
/>
</TabsPanel>
</Tabs>
</div>
),
fileListLoading: isSandboxInfoLoading,
fileListPanelClassName: 'w-[360px]',
fileListTreeClassName: 'px-0',
fileListTreeListClassName: 'px-1',
@@ -657,7 +644,7 @@ export function AgentWorkingDirectoryPanel({
: paths.filter((path) => path !== file.id),
)
},
onFolderDoubleClick: ({ file }) => handleDirectoryPathChange(toSandboxHomePath(file.id)),
onFolderDoubleClick: ({ file }) => handleDirectoryPathChange(file.id),
onSelectFile: (selectedFile) => setSelectedFileId(selectedFile.id),
renderFolderSuffix: ({ file }) =>
loadingFolderPaths.has(file.id) ? (
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "الرئيسية",
"agentDetail.configure.workingDirectory.open": "فتح دليل العمل",
"agentDetail.configure.workingDirectory.returnToWorkspace": "العودة إلى دليل العمل",
"agentDetail.configure.workingDirectory.savedFiles": "الملفات المحفوظة",
"agentDetail.configure.workingDirectory.temporaryFiles": "الملفات المؤقتة",
"agentDetail.configure.workingDirectory.title": "دليل العمل",
"agentDetail.configure.workingDirectory.treeLabel": "ملفات دليل العمل",
"agentDetail.configure.workingDirectory.workingDirectory": "دليل العمل",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "Start",
"agentDetail.configure.workingDirectory.open": "Arbeitsverzeichnis öffnen",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Zurück zum Arbeitsverzeichnis",
"agentDetail.configure.workingDirectory.savedFiles": "Gespeicherte Dateien",
"agentDetail.configure.workingDirectory.temporaryFiles": "Temporäre Dateien",
"agentDetail.configure.workingDirectory.title": "Arbeitsverzeichnis",
"agentDetail.configure.workingDirectory.treeLabel": "Dateien im Arbeitsverzeichnis",
"agentDetail.configure.workingDirectory.workingDirectory": "Arbeitsverzeichnis",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "home",
"agentDetail.configure.workingDirectory.open": "Open working directory",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Back to working directory",
"agentDetail.configure.workingDirectory.savedFiles": "Saved Files",
"agentDetail.configure.workingDirectory.temporaryFiles": "Temporary Files",
"agentDetail.configure.workingDirectory.title": "Working directory",
"agentDetail.configure.workingDirectory.treeLabel": "Working directory files",
"agentDetail.configure.workingDirectory.workingDirectory": "working directory",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "inicio",
"agentDetail.configure.workingDirectory.open": "Abrir directorio de trabajo",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Volver al directorio de trabajo",
"agentDetail.configure.workingDirectory.savedFiles": "Archivos guardados",
"agentDetail.configure.workingDirectory.temporaryFiles": "Archivos temporales",
"agentDetail.configure.workingDirectory.title": "Directorio de trabajo",
"agentDetail.configure.workingDirectory.treeLabel": "Archivos del directorio de trabajo",
"agentDetail.configure.workingDirectory.workingDirectory": "directorio de trabajo",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "خانه",
"agentDetail.configure.workingDirectory.open": "باز کردن پوشه کاری",
"agentDetail.configure.workingDirectory.returnToWorkspace": "بازگشت به پوشه کاری",
"agentDetail.configure.workingDirectory.savedFiles": "فایل‌های ذخیره‌شده",
"agentDetail.configure.workingDirectory.temporaryFiles": "فایل‌های موقت",
"agentDetail.configure.workingDirectory.title": "پوشه کاری",
"agentDetail.configure.workingDirectory.treeLabel": "فایل‌های پوشه کاری",
"agentDetail.configure.workingDirectory.workingDirectory": "پوشه کاری",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "accueil",
"agentDetail.configure.workingDirectory.open": "Ouvrir le répertoire de travail",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Retour au répertoire de travail",
"agentDetail.configure.workingDirectory.savedFiles": "Fichiers enregistrés",
"agentDetail.configure.workingDirectory.temporaryFiles": "Fichiers temporaires",
"agentDetail.configure.workingDirectory.title": "Répertoire de travail",
"agentDetail.configure.workingDirectory.treeLabel": "Fichiers du répertoire de travail",
"agentDetail.configure.workingDirectory.workingDirectory": "répertoire de travail",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "होम",
"agentDetail.configure.workingDirectory.open": "वर्किंग डायरेक्टरी खोलें",
"agentDetail.configure.workingDirectory.returnToWorkspace": "वर्किंग डायरेक्टरी पर वापस जाएँ",
"agentDetail.configure.workingDirectory.savedFiles": "सहेजी गई फ़ाइलें",
"agentDetail.configure.workingDirectory.temporaryFiles": "अस्थायी फ़ाइलें",
"agentDetail.configure.workingDirectory.title": "वर्किंग डायरेक्टरी",
"agentDetail.configure.workingDirectory.treeLabel": "वर्किंग डायरेक्टरी फाइलें",
"agentDetail.configure.workingDirectory.workingDirectory": "वर्किंग डायरेक्टरी",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "beranda",
"agentDetail.configure.workingDirectory.open": "Buka direktori kerja",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Kembali ke direktori kerja",
"agentDetail.configure.workingDirectory.savedFiles": "File tersimpan",
"agentDetail.configure.workingDirectory.temporaryFiles": "File sementara",
"agentDetail.configure.workingDirectory.title": "Direktori kerja",
"agentDetail.configure.workingDirectory.treeLabel": "File direktori kerja",
"agentDetail.configure.workingDirectory.workingDirectory": "direktori kerja",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "home",
"agentDetail.configure.workingDirectory.open": "Apri directory di lavoro",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Torna alla directory di lavoro",
"agentDetail.configure.workingDirectory.savedFiles": "File salvati",
"agentDetail.configure.workingDirectory.temporaryFiles": "File temporanei",
"agentDetail.configure.workingDirectory.title": "Directory di lavoro",
"agentDetail.configure.workingDirectory.treeLabel": "File della directory di lavoro",
"agentDetail.configure.workingDirectory.workingDirectory": "directory di lavoro",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "ホーム",
"agentDetail.configure.workingDirectory.open": "作業ディレクトリを開く",
"agentDetail.configure.workingDirectory.returnToWorkspace": "作業ディレクトリに戻る",
"agentDetail.configure.workingDirectory.savedFiles": "保存済みファイル",
"agentDetail.configure.workingDirectory.temporaryFiles": "一時ファイル",
"agentDetail.configure.workingDirectory.title": "作業ディレクトリ",
"agentDetail.configure.workingDirectory.treeLabel": "作業ディレクトリのファイル",
"agentDetail.configure.workingDirectory.workingDirectory": "作業ディレクトリ",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "홈",
"agentDetail.configure.workingDirectory.open": "작업 디렉터리 열기",
"agentDetail.configure.workingDirectory.returnToWorkspace": "작업 디렉터리로 돌아가기",
"agentDetail.configure.workingDirectory.savedFiles": "저장된 파일",
"agentDetail.configure.workingDirectory.temporaryFiles": "임시 파일",
"agentDetail.configure.workingDirectory.title": "작업 디렉터리",
"agentDetail.configure.workingDirectory.treeLabel": "작업 디렉터리 파일",
"agentDetail.configure.workingDirectory.workingDirectory": "작업 디렉터리",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "home",
"agentDetail.configure.workingDirectory.open": "ເປີດໂຟນເດີເຮັດວຽກ",
"agentDetail.configure.workingDirectory.returnToWorkspace": "ກັບຄືນໄປໂຟນເດີເຮັດວຽກ",
"agentDetail.configure.workingDirectory.savedFiles": "ໄຟລ໌ທີ່ບັນທຶກໄວ້",
"agentDetail.configure.workingDirectory.temporaryFiles": "ໄຟລ໌ຊົ່ວຄາວ",
"agentDetail.configure.workingDirectory.title": "ໂຟນເດີເຮັດວຽກ (Working directory)",
"agentDetail.configure.workingDirectory.treeLabel": "ໄຟລ໌ໃນໂຟນເດີເຮັດວຽກ",
"agentDetail.configure.workingDirectory.workingDirectory": "working directory",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "home",
"agentDetail.configure.workingDirectory.open": "Werkmap openen",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Terug naar werkmap",
"agentDetail.configure.workingDirectory.savedFiles": "Opgeslagen bestanden",
"agentDetail.configure.workingDirectory.temporaryFiles": "Tijdelijke bestanden",
"agentDetail.configure.workingDirectory.title": "Werkmap",
"agentDetail.configure.workingDirectory.treeLabel": "Bestanden in de werkmap",
"agentDetail.configure.workingDirectory.workingDirectory": "werkmap",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "home",
"agentDetail.configure.workingDirectory.open": "Otwórz katalog roboczy",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Wróć do katalogu roboczego",
"agentDetail.configure.workingDirectory.savedFiles": "Zapisane pliki",
"agentDetail.configure.workingDirectory.temporaryFiles": "Pliki tymczasowe",
"agentDetail.configure.workingDirectory.title": "Katalog roboczy",
"agentDetail.configure.workingDirectory.treeLabel": "Pliki katalogu roboczego",
"agentDetail.configure.workingDirectory.workingDirectory": "katalog roboczy",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "início",
"agentDetail.configure.workingDirectory.open": "Abrir diretório de trabalho",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Voltar ao diretório de trabalho",
"agentDetail.configure.workingDirectory.savedFiles": "Arquivos salvos",
"agentDetail.configure.workingDirectory.temporaryFiles": "Arquivos temporários",
"agentDetail.configure.workingDirectory.title": "Diretório de trabalho",
"agentDetail.configure.workingDirectory.treeLabel": "Arquivos do diretório de trabalho",
"agentDetail.configure.workingDirectory.workingDirectory": "diretório de trabalho",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "acasă",
"agentDetail.configure.workingDirectory.open": "Deschide directorul de lucru",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Înapoi la directorul de lucru",
"agentDetail.configure.workingDirectory.savedFiles": "Fișiere salvate",
"agentDetail.configure.workingDirectory.temporaryFiles": "Fișiere temporare",
"agentDetail.configure.workingDirectory.title": "Director de lucru",
"agentDetail.configure.workingDirectory.treeLabel": "Fișiere din directorul de lucru",
"agentDetail.configure.workingDirectory.workingDirectory": "director de lucru",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "главная",
"agentDetail.configure.workingDirectory.open": "Открыть рабочий каталог",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Вернуться в рабочий каталог",
"agentDetail.configure.workingDirectory.savedFiles": "Сохранённые файлы",
"agentDetail.configure.workingDirectory.temporaryFiles": "Временные файлы",
"agentDetail.configure.workingDirectory.title": "Рабочий каталог",
"agentDetail.configure.workingDirectory.treeLabel": "Файлы рабочего каталога",
"agentDetail.configure.workingDirectory.workingDirectory": "рабочий каталог",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "domov",
"agentDetail.configure.workingDirectory.open": "Odpri delovni imenik",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Nazaj v delovni imenik",
"agentDetail.configure.workingDirectory.savedFiles": "Shranjene datoteke",
"agentDetail.configure.workingDirectory.temporaryFiles": "Začasne datoteke",
"agentDetail.configure.workingDirectory.title": "Delovni imenik",
"agentDetail.configure.workingDirectory.treeLabel": "Datoteke delovnega imenika",
"agentDetail.configure.workingDirectory.workingDirectory": "delovni imenik",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "หน้าแรก",
"agentDetail.configure.workingDirectory.open": "เปิดไดเรกทอรีทำงาน",
"agentDetail.configure.workingDirectory.returnToWorkspace": "กลับไปยังไดเรกทอรีทำงาน",
"agentDetail.configure.workingDirectory.savedFiles": "ไฟล์ที่บันทึก",
"agentDetail.configure.workingDirectory.temporaryFiles": "ไฟล์ชั่วคราว",
"agentDetail.configure.workingDirectory.title": "ไดเรกทอรีทำงาน",
"agentDetail.configure.workingDirectory.treeLabel": "ไฟล์ในไดเรกทอรีทำงาน",
"agentDetail.configure.workingDirectory.workingDirectory": "ไดเรกทอรีทำงาน",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "ana sayfa",
"agentDetail.configure.workingDirectory.open": "Çalışma dizinini aç",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Çalışma dizinine dön",
"agentDetail.configure.workingDirectory.savedFiles": "Kaydedilen dosyalar",
"agentDetail.configure.workingDirectory.temporaryFiles": "Geçici dosyalar",
"agentDetail.configure.workingDirectory.title": "Çalışma dizini",
"agentDetail.configure.workingDirectory.treeLabel": "Çalışma dizini dosyaları",
"agentDetail.configure.workingDirectory.workingDirectory": "çalışma dizini",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "головна",
"agentDetail.configure.workingDirectory.open": "Відкрити робочий каталог",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Повернутися до робочого каталогу",
"agentDetail.configure.workingDirectory.savedFiles": "Збережені файли",
"agentDetail.configure.workingDirectory.temporaryFiles": "Тимчасові файли",
"agentDetail.configure.workingDirectory.title": "Робочий каталог",
"agentDetail.configure.workingDirectory.treeLabel": "Файли робочого каталогу",
"agentDetail.configure.workingDirectory.workingDirectory": "робочий каталог",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "trang chủ",
"agentDetail.configure.workingDirectory.open": "Mở thư mục làm việc",
"agentDetail.configure.workingDirectory.returnToWorkspace": "Quay lại thư mục làm việc",
"agentDetail.configure.workingDirectory.savedFiles": "Tệp đã lưu",
"agentDetail.configure.workingDirectory.temporaryFiles": "Tệp tạm thời",
"agentDetail.configure.workingDirectory.title": "Thư mục làm việc",
"agentDetail.configure.workingDirectory.treeLabel": "Tệp trong thư mục làm việc",
"agentDetail.configure.workingDirectory.workingDirectory": "thư mục làm việc",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "home",
"agentDetail.configure.workingDirectory.open": "打开工作目录",
"agentDetail.configure.workingDirectory.returnToWorkspace": "返回工作目录",
"agentDetail.configure.workingDirectory.savedFiles": "已保存文件",
"agentDetail.configure.workingDirectory.temporaryFiles": "临时文件",
"agentDetail.configure.workingDirectory.title": "工作目录",
"agentDetail.configure.workingDirectory.treeLabel": "工作目录文件",
"agentDetail.configure.workingDirectory.workingDirectory": "工作目录",
+2
View File
@@ -273,6 +273,8 @@
"agentDetail.configure.workingDirectory.home": "home",
"agentDetail.configure.workingDirectory.open": "開啟工作目錄",
"agentDetail.configure.workingDirectory.returnToWorkspace": "返回工作目錄",
"agentDetail.configure.workingDirectory.savedFiles": "已儲存檔案",
"agentDetail.configure.workingDirectory.temporaryFiles": "暫存檔案",
"agentDetail.configure.workingDirectory.title": "工作目錄",
"agentDetail.configure.workingDirectory.treeLabel": "工作目錄檔案",
"agentDetail.configure.workingDirectory.workingDirectory": "工作目錄",